1"""Markdown rendering: one section per character, plus the shared tables."""
3from collections
import OrderedDict
5from .ds1
import ds1_derived_stats
6from .ds2
import DS2_FAMILY, DS2_GAMES, DS2_GREAT_SOULS, ds2_derived_stats
7from .ds3
import ds3_derived_stats
17 "Adaptability":
"ADP",
18 "Intelligence":
"INT",
36 (
"Vitality",
"Max HP"),
37 (
"Attunement",
"Attunement (spell) slots"),
38 (
"Endurance",
"Stamina, equip load, physical defense"),
39 (
"Strength",
"Physical attack, strength-weapon scaling"),
40 (
"Dexterity",
"Physical attack, dex-weapon scaling, faster casting"),
41 (
"Resistance",
"Poison/bleed resistance, fire defense"),
42 (
"Intelligence",
"Magic attack, sorcery scaling"),
43 (
"Faith",
"Miracle scaling, lightning & magic defense"),
46 "ds2sotfs": OrderedDict(
49 (
"Endurance",
"Stamina"),
50 (
"Vitality",
"Equip load, physical defense, petrify resistance"),
51 (
"Attunement",
"Attunement (spell) slots, casting speed"),
52 (
"Strength",
"Physical attack, strength-weapon scaling"),
53 (
"Dexterity",
"Physical attack, dex-weapon scaling, casting speed"),
54 (
"Adaptability",
"Agility (i-frames), poison/bleed/petrify resistance"),
55 (
"Intelligence",
"Magic & dark attack, sorcery/hex scaling"),
56 (
"Faith",
"Lightning & dark attack, miracle/hex scaling"),
62 (
"Attunement",
"FP, attunement (spell) slots"),
63 (
"Endurance",
"Stamina"),
64 (
"Vitality",
"Equip load, physical defense"),
65 (
"Strength",
"Physical attack, strength-weapon scaling"),
66 (
"Dexterity",
"Physical attack, dex-weapon scaling, faster casting"),
67 (
"Intelligence",
"Magic attack, sorcery & pyromancy scaling"),
68 (
"Faith",
"Lightning & dark attack, miracle & pyromancy scaling"),
69 (
"Luck",
"Item discovery, bleed/poison buildup, hollow-weapon scaling"),
74 (
"Vigor",
"Max HP, fire defense & immunity"),
75 (
"Mind",
"FP (skill/spell points), focus resistance"),
76 (
"Endurance",
"Stamina, equip load, robustness"),
77 (
"Strength",
"Physical attack, strength-weapon scaling"),
78 (
"Dexterity",
"Dex-weapon scaling, faster casting, less fall damage"),
79 (
"Intelligence",
"Sorcery scaling, magic defense"),
80 (
"Faith",
"Incantation scaling"),
81 (
"Arcane",
"Item discovery, arcane-weapon scaling, death/holy resistance"),
89 return STAT_GOVERNS.get(DS2_FAMILY.get(game, game), {})
104 "soft caps 30 (~1,100 HP) & 50 (~1,500 HP), rising to ~1,900 at 99",
108 "1 slot at 10, then 12/14/16/19/23/28/34/41/50 — 10 slots max at 50",
112 "stamina maxes at 40 (160); equip load keeps rising (~+1/lvl) to 99",
114 (
"Strength",
"scaling soft cap 40"),
115 (
"Dexterity",
"scaling soft cap 40; cast speed improves to 45"),
116 (
"Resistance",
"minor per-level gains — commonly a dump stat"),
117 (
"Intelligence",
"scaling soft cap 40"),
118 (
"Faith",
"scaling soft cap 40"),
121 "ds2sotfs": OrderedDict(
123 (
"Vigor",
"soft caps 20 & 50; +30 HP/lvl to 20, +20 to 50, +5 after"),
124 (
"Endurance",
"soft cap 20; +2 stamina/lvl to 20, +1 after"),
127 "soft caps 29/49/70; +1.5 load/lvl to 29, +1 to 49, +0.5 to 69, +0.25 after",
131 "slots at 10/13/16/20/25/30/40/50/60/75/94; cast-speed breakpoints 30/45/60/80",
133 (
"Strength",
"scaling soft caps 40 & 50"),
134 (
"Dexterity",
"scaling soft caps 40 & 50"),
135 (
"Adaptability",
"raises Agility (with Attunement); gains taper past ~40"),
136 (
"Intelligence",
"scaling soft caps 40 & 50"),
137 (
"Faith",
"scaling soft caps 40 & 50"),
142 (
"Vigor",
"soft caps ~27 & 50; ~1,300 HP at 50, only ~100 more to 99"),
145 "FP soft cap 35 (450 max at 99); slots at 10/14/18/24/30/40/50/60/80/99",
147 (
"Endurance",
"stamina soft cap 40"),
148 (
"Vitality",
"roughly linear to 99"),
149 (
"Strength",
"scaling soft caps 40 & 60"),
150 (
"Dexterity",
"scaling soft caps 40 & 60"),
151 (
"Intelligence",
"scaling soft caps 40 & 60"),
152 (
"Faith",
"scaling soft caps 40 & 60"),
153 (
"Luck",
"+1 item discovery/pt (base 100); bleed/poison speed soft cap 50"),
158 (
"Vigor",
"soft caps 40 & 60"),
159 (
"Mind",
"soft caps 50 & 60"),
160 (
"Endurance",
"stamina soft caps 15/30/50; equip load 25/60"),
161 (
"Strength",
"scaling soft caps 20/50/80"),
162 (
"Dexterity",
"scaling soft caps 20/50/80"),
163 (
"Intelligence",
"scaling soft caps 20/50/80"),
164 (
"Faith",
"scaling soft caps 20/50/80"),
165 (
"Arcane",
"scaling soft caps 20/50/80; also raises item discovery"),
173 return STAT_CAPS.get(DS2_FAMILY.get(game, game), {})
178 "weapons":
"Weapons",
181 "talismans":
"Talismans",
183 "bolts":
"Ammunition",
184 "upgrade":
"Upgrade Materials",
185 "consumables":
"Consumables",
186 "online":
"Summon & Covenant Items",
187 "goods":
"Consumables & Goods",
188 "ashes":
"Ashes of War",
189 "emotes":
"Gestures",
190 "bosssouls":
"Boss Souls",
191 "items":
"Items Owned",
195 "arts":
"Combat Arts",
196 "prosthetics":
"Prosthetic Tools",
197 "skills":
"Skills & Techniques",
198 "beads":
"Prayer Beads & Gourd Seeds",
199 "memories":
"Memories & Remnants",
200 "storage":
"Storage (item box)",
209CAT_TITLE_GAME = {
"sdt": {
"armors":
"Attire"}}
212def cat_title(game, cat):
213 return CAT_TITLE_GAME.get(game, {}).get(cat, CAT_TITLE[cat])
246def guess_build(stats):
251 return stats.get(k)
or 0
254 g(
"Strength") + g(
"Dexterity"),
255 g(
"Intelligence") + g(
"Faith") + g(
"Attunement"),
258 return "caster / hybrid (high INT/FTH/ATN)"
259 if g(
"Strength") >= g(
"Dexterity") + 6:
260 return "strength-focused melee"
261 if g(
"Dexterity") >= g(
"Strength") + 6:
262 return "dexterity-focused melee"
263 return "quality / balanced melee"
267CURRENCY = {
"er":
"Runes",
"sdt":
"Sen"}
281 total = m[
"spent"] + m[
"held"]
283 "across every journey so far — Attack Power carries into New Game+ while "
284 "the Memories do not"
286 else "this journey; a boss that drops no Memory is not counted either way"
289 f
"{total} Memory-dropping boss{'' if total == 1 else 'es'} defeated"
290 f
" _({m['spent']} Memor{'y' if m['spent'] == 1 else 'ies'} already spent, "
291 f
"read back from Attack Power, plus {m['held']} still held — {lap})_"
304def vitality_necklaces(vitality):
307 "no Prayer Necklace used yet"
309 else f
"{used} Prayer Necklace{'' if used == 1 else 's'} used — four Prayer Beads "
310 f
"each, read back from Vitality across every journey so far"
323def gourd_seeds(gourd):
326 "no Gourd Seed used yet"
328 else f
"{used} Gourd Seed{'' if used == 1 else 's'} used — handed to Emma, read "
329 f
"back from the gourd's own capacity across every journey so far"
336 "—" if value
is None else f
"{value:,}" if isinstance(value, int)
else str(value)
341def fmt_playtime(seconds):
342 h, rem = divmod(seconds, 3600)
343 mn, s = divmod(rem, 60)
344 return f
"{h}:{mn:02d}:{s:02d}"
352DS1_BONFIRE_NOTE =
"each bonfire's own record, with how far it is kindled — a floor"
355DS3_BONFIRE_NOTE =
"bonfires lit, inferred from each area's flag bits — a floor"
360DS2_BONFIRE_NOTE =
"each bonfire the save records as discovered, by area — a floor"
367 "each idol's own flag bit, by area — a floor, and one that starts "
368 "again on a new journey"
381 "one-off world events, each read from its own flag — exact, but only "
382 "the flags that have been named are counted"
387 "each miniboss's own defeat flag — exact, not inferred, but it "
388 "resets on a new journey; names are the game's own, so a repeat in an "
389 "area is a second placement of that character"
396 "one-off world items picked up, from each area's pickup flags — "
397 "covers only the areas whose flag group is mapped"
409 "one-off world items picked up, from each area's item-lot flags — covers the "
410 "nine areas whose flag bank is mapped, 581 of the 826 lots the table knows"
424 "world events from the game's own event scripts, counted by kind — sourced, "
425 "not verified here; some are transient (set while the event runs), so a "
426 "finished run can honestly read 0 in a family"
433 "NPC deaths, hostility and questline milestones, each its own flag — the "
434 "descriptions are the source's and one is known to be on the wrong flag, so "
435 "an entity id is printed beside them where there is one"
440 "each enemy's own defeat flag — exact, not inferred, and it carries "
441 "across a new journey; names are enemy types, so repeats in an area "
442 "are different enemies"
450def lords_line(lords):
452 named = f
" — {' · '.join(lords['named'])}" if lords[
"named"]
else ""
453 if lords[
"placed"]
is None:
455 f
"{len(lords['named'])} of {lords['total']}{named}"
456 " _(NG+ — only the mapped throne flags are read, so this is a floor)_"
459 f
"{lords['placed']} of {lords['total']}{named}"
460 f
" _({lords['dead']} of the four lords defeated, {lords['held']} set"
461 f
"{'' if lords['held'] == 1 else 's'} of cinders still held)_"
469def missing_note(label, missing):
470 return f
"_{label}: {' · '.join(missing)}._" if missing
else None
478def count_dupes(names):
481 seen[n] = seen.get(n, 0) + 1
482 return [n
if c == 1
else f
"{n} ×{c}" for n, c
in seen.items()]
485def md_for_character(ch, slot_no):
486 L = [f
"## Slot {slot_no}: {ch['name']}",
""]
487 if ch[
"level"]
is not None:
489 f
"- **{'Level' if ch['game'] == 'er' else 'Soul Level'}:** {ch['level']}"
492 L.append(f
"- **Class:** {ch['klass']}")
493 if ch.get(
"covenant"):
494 L.append(f
"- **Covenant:** {ch['covenant']}")
496 L.append(f
"- **Sex:** {ch['gender']}")
497 if ch[
"ng_plus"]
is not None:
498 ng =
"New Game" if ch[
"ng_plus"] == 0
else f
"New Game +{ch['ng_plus']}"
499 L.append(f
"- **Playthrough:** {ng}")
500 if ch[
"soul_memory"]
is not None:
502 f
"- **Soul Memory:** {fmt(ch['soul_memory'])} _(total souls earned — main progress metric)_"
504 if ch.get(
"play_time"):
505 L.append(f
"- **Play Time:** {fmt_playtime(ch['play_time'])}")
506 if ch[
"souls"]
is not None:
507 L.append(f
"- **{CURRENCY.get(ch['game'], 'Souls')} held:** {fmt(ch['souls'])}")
508 if ch.get(
"attack")
is not None:
509 L.append(f
"- **Attack Power:** {ch['attack']}")
510 if ch.get(
"vitality")
is not None:
512 f
"- **Vitality:** {ch['vitality']} _({vitality_necklaces(ch['vitality'])})_"
514 if ch.get(
"gourd")
is not None:
516 f
"- **Healing Gourd:** {ch['gourd']} charge"
517 f
"{'' if ch['gourd'] == 1 else 's'} _({gourd_seeds(ch['gourd'])})_"
519 if ch.get(
"skill_points")
is not None:
521 f
"- **Skill Points Held:** {ch['skill_points']}"
522 " _(spendable at a Sculptor's Idol — the points already spent are "
523 "not stored, so this is what is banked, not what has been earned)_"
525 if ch.get(
"memories"):
526 L.append(f
"- **Memories:** {memories_line(ch['memories'])}")
527 if ch[
"humanity"]
is not None:
528 L.append(f
"- **Humanity:** {ch['humanity']}")
529 if ch[
"hp"]
is not None:
530 L.append(f
"- **Max HP:** {fmt(ch['hp'])}")
531 if ch.get(
"posture")
is not None:
532 L.append(f
"- **Max Posture:** {fmt(ch['posture'])}")
533 if ch.get(
"embered")
is not None:
535 "- **Embered:** Yes _(Max HP above includes the +30% ember bonus)_"
537 else "- **Embered:** No _(hollow — Max HP above is the base value)_"
539 if ch.get(
"fp")
is not None:
540 L.append(f
"- **Max FP:** {fmt(ch['fp'])}")
541 if ch.get(
"hollow_lvl"):
543 f
"- **Hollowing:** {ch['hollow_lvl']} _(higher = more deaths without an effigy)_"
545 if ch.get(
"deaths")
is not None:
546 L.append(f
"- **Deaths:** {fmt(ch['deaths'])}")
547 if ch[
"stamina"]
is not None:
548 L.append(f
"- **Stamina:** {fmt(ch['stamina'])}")
550 L.append(f
"- **Cinders of a Lord Placed:** {lords_line(ch['lords'])}")
551 if ch.get(
"endings"):
554 f
"- **Ending{'' if len(ends) == 1 else 's'} Reached:** " +
" · ".join(ends)
556 build = guess_build(ch[
"stats"])
558 L.append(f
"- **Build:** {build}")
562 keys = list(ch[
"stats"].keys())
566 "| " +
" | ".join(STAT_ABBR.get(k, k[:3].upper())
for k
in keys) +
" |",
567 "|" +
"----|" * len(keys),
568 "| " +
" | ".join(str(ch[
"stats"][k])
for k
in keys) +
" |",
573 rows = [k
for k
in keys
if k
in gov]
580 "<summary><b>Attribute Scaling</b> — what each stat "
581 "scales and its soft caps (game-mechanics reference, not read from "
582 "this save)</summary>",
586 caps = f
" {cap[k][:1].upper() + cap[k][1:]}." if cap.get(k)
else ""
587 L.append(f
"- **{k}** ({ch['stats'][k]}) — {gov[k]}.{caps}")
588 L += [
"",
"</details>",
""]
589 if ch[
"game"]
in DS2_GAMES:
590 d = ds2_derived_stats(ch[
"stats"])
591 agl = f
"{d['agility']}" + (
592 f
" _({d['iframes']} roll i-frames)_" if d[
"iframes"]
else ""
595 "### Derived Stats _(computed from attributes — base values before "
596 "rings & equipment; the in-game screen adds ring/gear bonuses on top)_",
598 f
"- **Stamina:** {d['stamina']}",
599 f
"- **Equip Load (max capacity):** {d['equip_load']:.1f}",
600 f
"- **Attunement Slots:** {d['slots']}",
601 f
"- **Agility (AGL):** {agl}",
602 f
"- **Poise (base):** {d['poise']:.1f}",
603 f
"- **ATK: Str:** {d['atk_str']}",
604 f
"- **ATK: Dex:** {d['atk_dex']}",
605 f
"- **Magic DEF:** {d['magic_def']}",
606 f
"- **Fire DEF:** {d['fire_def']}",
607 f
"- **Lightning DEF:** {d['lightning_def']}",
608 f
"- **Dark DEF:** {d['dark_def']}",
611 if ch[
"game"] ==
"ds3":
612 d = ds3_derived_stats(ch[
"stats"], ch.get(
"ring_mods"))
617 def credit(base, kind, unit):
620 parts =
", ".join(f
"+{v:g}{unit} {n}" for n, v
in w[kind])
621 return f
" _({base} base, {parts})_"
624 "### Derived Stats _(computed from attributes, plus the documented "
625 "bonus of any worn ring named beside the value — the game's other "
626 "gear is not read)_",
628 f
"- **Attunement Slots:** {d['slots']}"
629 + credit(d[
"slots_base"],
"slots",
""),
630 f
"- **Equip Load (max capacity):** {d['equip_load']:.1f}"
631 + credit(f
"{d['equip_load_base']:.1f}",
"load_pct",
"%"),
632 f
"- **Item Discovery:** {d['item_discovery']}"
633 + credit(d[
"item_discovery_base"],
"discovery",
""),
638 f
"+{v:g}% {'Max HP' if k == 'hp_pct' else 'Stamina'} ({n})"
639 for k
in (
"hp_pct",
"stam_pct")
644 f
"- **Also from rings:** {', '.join(already)} _(Max HP and "
645 f
"Stamina are read from the save, so they already include "
649 if ch[
"game"]
in (
"dsr",
"ptde"):
650 d = ds1_derived_stats(ch[
"stats"])
652 "### Derived Stats _(computed from attributes — base values before "
653 "rings & equipment)_",
655 f
"- **Attunement Slots:** {d['slots']}",
656 f
"- **Equip Load (max capacity):** {d['equip_load']:.1f}",
659 elif ch[
"tier"] ==
"roster":
661 "_Nightreign is read at roster tier: the save decrypts and every entry "
662 "verifies its own checksum, so the name above is certain, and nothing "
663 "else is claimed. The game keeps no persistent level, attributes or "
664 "souls to print — its progression is relics, unlocked Nightfarers and "
665 "Nightlord kills, and none of those has been pinned against a second "
669 elif ch[
"tier"] ==
"inventory":
671 "_Attributes are not printed for this slot: its stat block did not "
672 "validate (an unrecognised patch or an edited save), and a wrong "
673 "number is worse than none. Inventory and progress below are read "
679 return [f
"- {n}" + (f
" ×{q}" if q
and q > 1
else "")
for n, q
in items]
689 and ch[
"game"] !=
"sdt"
690 and not (ch[
"inv"].get(
"bosssouls")
or ch[
"inv"].get(
"memories"))
693 "### Remembrances Held _(major bosses defeated, not yet traded)_"
694 if ch[
"game"] ==
"er"
695 else "### Boss Souls Held _(bosses defeated, soul not yet consumed)_"
697 L += [header,
""] + bullets(ch[
"boss_souls"]) + [
""]
699 L += [
"### Key Items _(progress / areas & shortcuts unlocked)_",
""]
700 L += bullets(ch[
"key_items"]) + [
""]
703 if ch.get(
"bonfires")
and not ch.get(
"bonfire_areas"):
705 f
"### Bonfires Discovered ({len(ch['bonfires'])}) _(areas reached — a "
706 "floor on progress)_",
709 L += [f
"- {b}" for b
in ch[
"bonfires"]] + [
""]
710 if ch.get(
"bonfire_areas"):
711 lit = sum(c
for _a, c, _n, _t, _m
in ch[
"bonfire_areas"])
712 total = sum(t
for _a, _c, _n, t, _m
in ch[
"bonfire_areas"])
713 n = sum(1
for _a, c, _n, _t, _m
in ch[
"bonfire_areas"]
if c)
714 areas = len(ch[
"bonfire_areas"])
719 if ch.get(
"game")
in (
"dsr",
"ptde")
720 else DS2_BONFIRE_NOTE
721 if ch.get(
"game")
in DS2_GAMES
722 else SDT_BONFIRE_NOTE
723 if ch.get(
"game") ==
"sdt"
724 else DS3_BONFIRE_NOTE
729 "Sculptor's Idols Lit" if ch.get(
"game") ==
"sdt" else "Bonfires Discovered"
731 L += [f
"### {head} ({lit} of {total}, in {n} of {areas} areas) _({note})_",
""]
732 for name, c, named, tot, missing
in ch[
"bonfire_areas"]:
733 row = f
"- {name}: {c}/{tot}"
735 row += f
" — {', '.join(named)}"
739 row += f
" _(missing: {' · '.join(missing)})_"
742 if ch.get(
"covenants"):
743 found, total = len(ch[
"covenants"]), ch.get(
"covenant_total")
744 count = f
"{found} of {total}" if total
else f
"{found}"
746 f
"### Covenants Found ({count}) _(discovered — a floor; "
747 "the one currently worn is the Covenant field above)_",
750 L += [f
"- **{cov}:** {', '.join(w)}" for cov, w
in ch[
"covenants"].items()] + [
753 note = missing_note(
"Not found yet", ch.get(
"covenants_missing"))
756 if ch.get(
"world_flags"):
757 got = sum(c
for _a, c, _n, _t, _m
in ch[
"world_flags"])
758 total = sum(t
for _a, _c, _n, t, _m
in ch[
"world_flags"])
759 L += [f
"### World State ({got} of {total} tracked) _({DS1_WORLD_NOTE})_",
""]
760 for cat, c, names, tot, missing
in ch[
"world_flags"]:
761 row = f
"- {cat}: {c}/{tot}"
765 row += f
" — {' · '.join(names)}"
767 row += f
" _(not yet: {' · '.join(missing)})_"
770 if ch.get(
"minibosses"):
771 dead = sum(c
for _a, c, _n, _t, _m
in ch[
"minibosses"])
772 total = sum(t
for _a, _c, _n, t, _m
in ch[
"minibosses"])
774 f
"### Minibosses Defeated ({dead} of {total} tracked) _({SDT_MINIBOSS_NOTE})_",
777 for area, c, names, tot, alive
in ch[
"minibosses"]:
778 row = f
"- {area}: {c}/{tot}"
782 row += f
" — {' · '.join(count_dupes(names))}"
787 row += f
" _(still alive: {' · '.join(count_dupes(alive))})_"
790 if ch.get(
"npc_states"):
791 got = sum(c
for _f, c, _g, _t
in ch[
"npc_states"])
792 total = sum(t
for _f, _c, _g, t
in ch[
"npc_states"])
794 f
"### NPC States ({got} of {total} tracked) _({DS3_NPC_NOTE})_",
797 for family, c, names, tot
in ch[
"npc_states"]:
798 row = f
"- {family}: {c}/{tot}"
804 said = [n
for n
in names
if not n.startswith(
"character ")]
805 rest = len(names) - len(said)
807 row += f
" — {' · '.join(count_dupes(said))}"
809 row += f
" _(+{rest} more, known only by entity id)_"
812 if ch.get(
"world_events"):
813 got = sum(c
for _f, c, _t
in ch[
"world_events"])
814 total = sum(t
for _f, _c, t
in ch[
"world_events"])
816 f
"### World Events ({got} of {total} tracked) _({DS1_EVENT_NOTE})_",
819 for family, c, tot
in ch[
"world_events"]:
820 L.append(f
"- {family}: {c}/{tot}")
822 if ch.get(
"enemies"):
823 dead = sum(c
for _a, c, _n, _t, _m
in ch[
"enemies"])
824 total = sum(t
for _a, _c, _n, t, _m
in ch[
"enemies"])
826 f
"### One-Time Enemies Defeated ({dead} of {total} tracked)"
827 f
" _({DS3_ENEMY_NOTE})_",
830 for area, c, names, tot, alive
in ch[
"enemies"]:
831 row = f
"- {area}: {c}/{tot}"
833 row += f
" — {' · '.join(count_dupes(names))}"
835 row += f
" _(still alive: {' · '.join(count_dupes(alive))})_"
838 if ch.get(
"pickups"):
839 got = sum(c
for _a, c, _t, _m
in ch[
"pickups"])
840 total = sum(t
for _a, _c, t, _m
in ch[
"pickups"])
841 note = SDT_PICKUP_NOTE
if ch[
"game"] ==
"sdt" else DS3_PICKUP_NOTE
843 f
"### Items Collected ({got} of {total} tracked) _({note})_",
846 for area, c, tot, missing
in ch[
"pickups"]:
847 row = f
"- {area}: {c}/{tot}"
851 row += f
" _({len(missing)} still out there)_"
858 (area, missing)
for area, c, _t, missing
in ch[
"pickups"]
if c
and missing
863 "<summary>Where the missing items are — the ones with a known "
864 "location</summary>",
867 for area, missing
in todo:
868 L.append(f
"**{area}** — {len(missing)} missing")
870 L += [f
"- {item}" for item
in count_dupes(missing)]
872 L += [
"</details>",
""]
873 if ch.get(
"questlines"):
877 "### Rewards Obtained _(one-off rewards from NPCs, invaders and "
878 "landmark pickups — a progress floor)_",
882 f
"- **{src}:** {', '.join(rw)}" for src, rw
in ch[
"questlines"].items()
888 "gate":
"progression",
889 "clear":
"cleared (NG+)",
891 found, total = len(ch[
"bosses"]), ch.get(
"boss_total")
892 count = f
"{found} of {total} tracked" if total
else f
"{found}"
894 f
"### Bosses Defeated ({count}) _(a floor — from defeat "
895 "flags, held boss souls, progression, and NG+ clears; a boss whose soul "
896 "was consumed and isn't gated may still be missing)_",
899 for boss, srcs
in ch[
"bosses"].items():
900 L.append(f
"- {boss} _({', '.join(SRC[s] for s in srcs)})_")
905 avail = ch.get(
"bosses_available")
or []
906 rest = [b
for b
in (ch.get(
"bosses_missing")
or [])
if b
not in avail]
909 f
"_Available now — every prerequisite dead and the area already "
910 f
"reached (from the game's fixed route, not this save): "
911 f
"{' · '.join(avail)}._",
915 "No evidence yet" + (
", and behind something else" if avail
else ""), rest
921 ch.get(
"equipped_weapons")
922 or ch.get(
"equipped_armor")
923 or ch.get(
"equipped_rings")
924 or ch.get(
"equipped_ammo")
926 L += [
"### Equipped _(worn gear read from the equip slots)_",
""]
928 f
"- **{slot}:** {name}"
929 for slot, name
in ch.get(
"equipped_weapons", {}).items()
932 f
"- **{slot}:** {name}"
933 for slot, name
in ch.get(
"equipped_armor", {}).items()
935 if ch.get(
"equipped_rings"):
939 eff = dict(ch.get(
"ring_effects")
or [])
941 L.append(
"- **Rings:**")
943 f
" - {n}" + (f
" — {eff[n]}" if n
in eff
else "")
944 for n
in ch[
"equipped_rings"]
947 L.append(f
"- **Rings:** {', '.join(ch['equipped_rings'])}")
948 if ch.get(
"equipped_ammo"):
949 L.append(f
"- **Ammo:** {', '.join(ch['equipped_ammo'])}")
954 if any(ch[
"inv"].get(c)
for c
in CAT_ORDER):
955 L += [
"### Inventory",
""]
958 listed = {n
for n, _q
in ch[
"boss_souls"]} | {n
for n, _q
in ch[
"key_items"]}
959 for cat
in CAT_ORDER:
960 items = ch[
"inv"].get(cat)
963 if cat ==
"goods" and listed:
964 items = [it
for it
in items
if it[0]
not in listed]
969 if cat ==
"bosssouls":
970 great = [it
for it
in items
if it[0]
in DS2_GREAT_SOULS]
971 normal = [it
for it
in items
if it[0]
not in DS2_GREAT_SOULS]
972 for title, group
in ((
"Great Boss Souls", great), (
"Boss Souls", normal)):
974 L += [f
"#### {title}",
""] + bullets(group) + [
""]
976 title = cat_title(ch[
"game"], cat)
977 if cat ==
"goods" and listed:
978 title +=
" _(boss souls and key items are listed above)_"
979 L += [f
"#### {title}",
""] + bullets(items) + [
""]
980 if ch[
"unknown_count"]:
982 f
"_{ch['unknown_count']} inventory item(s) had IDs not in the name "
983 "database (upgraded / infused variants) and were omitted._",
986 if ch.get(
"internal_count"):
988 f
"_{ch['internal_count']} further entr"
989 f
"{'y' if ch['internal_count'] == 1 else 'ies'} carried only an engine "
990 "development name — placeholder rows and debug items rather than "
991 "anything the game hands you — and were left out._",
994 if ch.get(
"suppressed_count"):
996 f
"_{ch['suppressed_count']} further entr"
997 f
"{'y' if ch['suppressed_count'] == 1 else 'ies'} were engine state "
998 "rather than inventory — Sekiro has no armour system, so the character's "
999 "own body models sit in the protector table, the `Virtual Weapon:` "
1000 "rows restate a Combat Art already listed under its own name, and the "
1001 "two plain `Memory` rows are the game's counters of Memories spent and "
1002 "held. Counted here, not printed._",
stat_caps_for(game)
Soft-cap reference for a per-slot game id (DSR and PtDE share DS1).
stat_governs_for(game)
Map a per-slot game id to its STAT_GOVERNS family (DSR and PtDE share DS1).