"""Character sheet as DOCX and PDF.""" import io from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.shared import Pt from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import mm from reportlab.platypus import Paragraph, SimpleDocTemplate, Table, TableStyle from .derive import compute, kit_items SOURCE = {"noxium": "Noxium", "diesium": "Diesium", "hybrid": "Hybrid", "precyber": "Pre-meteor cybernetics"} KIND = {"mutation": "Mutation", "implant": "Implant"} def _sheet(ch: dict, rules: dict) -> dict: """Prepares all data once for both formats.""" c = compute(ch, rules) prof = next((p for p in rules["professions"] if p["id"] == ch.get("profession")), None) talents = [t for t in rules["talents"] if t["id"] in ch.get("talents", [])] cls = next((k for k in rules.get("classes", []) if k["id"] == ch.get("class_id")), None) sub = next((x for x in (cls or {}).get("subclasses", []) if x["id"] == ch.get("subclass_id")), None) gear_rows = [] for label, item, qty in kit_items(ch, rules): name = item["name"] + (f" x{qty}" if qty > 1 else "") gear_rows.append([label, name, item["stat"], f"{round(item['kg'] * qty, 1)} kg"]) preset = next((x["name"] for x in rules.get("presets", []) if x["id"] == ch.get("preset")), "Custom") return { "c": c, "preset": preset, "class": " / ".join(x["name"] for x in (cls, sub) if x) or "-", "profession": prof["name"] if prof else "-", "talents": talents, "gear_rows": gear_rows, "other": (ch.get("gear") or {}).get("other", ""), "attr_rows": [[a["name"], str(c["attrs"][a["id"]]), str(c["mods"][a["id"]])] for a in rules["attributes"]], "derived_rows": [[d["name"], str(c["derived"][d["id"]])] for d in rules["derived"]], "skill_rows": [ [s["name"], "/".join(a["short"] for a in rules["attributes"] if a["id"] in s["attrs"]), str(s["points"]), {1: "Proficient", 2: "Expertise"}.get(s["pips"]) or ("" if s["usable"] else "Locked"), str(s["total"])] for s in c["skills"] ], "augment_rows": [[a["name"], KIND.get(a["kind"], a["kind"]), SOURCE.get(a["source"]) or "-", str(a["cost"])] for a in c["augments"]], } def to_docx(ch: dict, rules: dict) -> bytes: s = _sheet(ch, rules) c = s["c"] doc = Document() doc.styles["Normal"].font.name = "Calibri" doc.styles["Normal"].font.size = Pt(10) title = doc.add_heading(ch.get("name") or "Nameless", 0) title.alignment = WD_ALIGN_PARAGRAPH.LEFT doc.add_paragraph(f"Arcane Wasteland | Player: {ch.get('player') or '-'} | Profession: {s['profession']} | Class: {s['class']} | Rules: {s['preset']}") if ch.get("concept"): doc.add_paragraph().add_run(ch["concept"]).italic = True def table(head, rows): t = doc.add_table(rows=1, cols=len(head)) t.style = "Light Grid Accent 1" for i, h in enumerate(head): t.rows[0].cells[i].text = h for r in rows: cells = t.add_row().cells for i, v in enumerate(r): cells[i].text = v doc.add_heading("Attributes", 1) table(["Attribute", "Value", "Mod"], s["attr_rows"]) doc.add_heading("Derived stats", 1) table(["Stat", ""], s["derived_rows"]) doc.add_heading("Skills", 1) if s["skill_rows"]: table(["Skill", "Attr.", "Points", "Training", "Target"], s["skill_rows"]) else: doc.add_paragraph("No skill points spent.") doc.add_heading("Talents", 1) for t in s["talents"]: doc.add_paragraph(f"{t['name']}: {t['desc']}", style="List Bullet") if not s["talents"]: doc.add_paragraph("-") doc.add_heading(f"Mutations and implants ({c['capacity_used']} / {c['capacity_max']} capacity)", 1) if s["augment_rows"]: table(["Name", "Type", "Source", "Cost"], s["augment_rows"]) else: doc.add_paragraph("-") doc.add_heading(f"Equipment (carried {c['weight']} / {c['carry_max']} kg)", 1) if s["gear_rows"]: table(["Category", "Item", "Damage / Armor / Type", "Weight"], s["gear_rows"]) else: doc.add_paragraph("-") if s["other"]: p = doc.add_paragraph() p.add_run("Other: ").bold = True p.add_run(s["other"]) for key, head in (("background", "Background"), ("notes", "Notes")): if ch.get(key): doc.add_heading(head, 1) doc.add_paragraph(ch[key]) buf = io.BytesIO() doc.save(buf) return buf.getvalue() def to_pdf(ch: dict, rules: dict) -> bytes: s = _sheet(ch, rules) c = s["c"] buf = io.BytesIO() doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=18 * mm, rightMargin=18 * mm, topMargin=16 * mm, bottomMargin=16 * mm, title=ch.get("name") or "Character", author=ch.get("player") or "") st = getSampleStyleSheet() h1 = ParagraphStyle("h1", parent=st["Heading1"], fontSize=22, spaceAfter=2) h2 = ParagraphStyle("h2", parent=st["Heading2"], fontSize=12, spaceBefore=10, spaceAfter=4, textColor=colors.HexColor("#2f5d3a")) body = ParagraphStyle("b", parent=st["BodyText"], fontSize=9.5, leading=12.5) def esc(t): return (t or "").replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
") def tbl(head, rows, widths=None): t = Table([head] + rows, colWidths=widths, repeatRows=1) t.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2f5d3a")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("FONTSIZE", (0, 0), (-1, -1), 9), ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#b9c4bb")), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f1f5f2")]), ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3), ])) return t el = [Paragraph(esc(ch.get("name") or "Nameless"), h1), Paragraph(f"Arcane Wasteland  |  Player: {esc(ch.get('player') or '-')}  |  " f"Profession: {esc(s['profession'])}  |  Class: {esc(s['class'])}  |  Rules: {esc(s['preset'])}", body)] if ch.get("concept"): el.append(Paragraph(f"{esc(ch['concept'])}", body)) el += [Paragraph("Attributes", h2), tbl(["Attribute", "Value", "Mod"], s["attr_rows"], [80 * mm, 30 * mm, 30 * mm]), Paragraph("Derived stats", h2), tbl(["Stat", ""], s["derived_rows"], [80 * mm, 30 * mm]), Paragraph("Skills", h2)] if s["skill_rows"]: el.append(tbl(["Skill", "Attr.", "Points", "Training", "Target"], s["skill_rows"], [60 * mm, 22 * mm, 22 * mm, 28 * mm, 25 * mm])) else: el.append(Paragraph("No skill points spent.", body)) el.append(Paragraph("Talents", h2)) for t in s["talents"]: el.append(Paragraph(f"{esc(t['name'])}: {esc(t['desc'])}", body)) if not s["talents"]: el.append(Paragraph("-", body)) el.append(Paragraph(f"Mutations and implants ({c['capacity_used']} / {c['capacity_max']} capacity)", h2)) if s["augment_rows"]: el.append(tbl(["Name", "Type", "Source", "Cost"], s["augment_rows"], [60 * mm, 30 * mm, 50 * mm, 22 * mm])) else: el.append(Paragraph("-", body)) el.append(Paragraph(f"Equipment (carried {c['weight']} / {c['carry_max']} kg)", h2)) if s["gear_rows"]: el.append(tbl(["Category", "Item", "Damage / Armor / Type", "Weight"], s["gear_rows"], [30 * mm, 60 * mm, 50 * mm, 22 * mm])) else: el.append(Paragraph("-", body)) if s["other"]: el.append(Paragraph(f"Other: {esc(s['other'])}", body)) for key, head in (("background", "Background"), ("notes", "Notes")): if ch.get(key): el += [Paragraph(head, h2), Paragraph(esc(ch[key]), body)] doc.build(el) return buf.getvalue()