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"
407 "one-off world items picked up, from each area's item-lot flags — covers the "
408 "nine areas whose flag bank is mapped, 583 of the 826 lots the table knows"
422 "world events from the game's own event scripts, counted by kind — sourced, "
423 "not verified here; some are transient (set while the event runs), so a "
424 "finished run can honestly read 0 in a family"
431 "NPC deaths, hostility and questline milestones, each its own flag — the "
432 "descriptions are the source's and one is known to be on the wrong flag, so "
433 "an entity id is printed beside them where there is one"
438 "each enemy's own defeat flag — exact, not inferred, and it carries "
439 "across a new journey; names are enemy types, so repeats in an area "
440 "are different enemies"
448def lords_line(lords):
450 named = f
" — {' · '.join(lords['named'])}" if lords[
"named"]
else ""
451 if lords[
"placed"]
is None:
453 f
"{len(lords['named'])} of {lords['total']}{named}"
454 " _(NG+ — only the mapped throne flags are read, so this is a floor)_"
457 f
"{lords['placed']} of {lords['total']}{named}"
458 f
" _({lords['dead']} of the four lords defeated, {lords['held']} set"
459 f
"{'' if lords['held'] == 1 else 's'} of cinders still held)_"
467def missing_note(label, missing):
468 return f
"_{label}: {' · '.join(missing)}._" if missing
else None
476def count_dupes(names):
479 seen[n] = seen.get(n, 0) + 1
480 return [n
if c == 1
else f
"{n} ×{c}" for n, c
in seen.items()]
483def md_for_character(ch, slot_no):
484 L = [f
"## Slot {slot_no}: {ch['name']}",
""]
485 if ch[
"level"]
is not None:
487 f
"- **{'Level' if ch['game'] == 'er' else 'Soul Level'}:** {ch['level']}"
490 L.append(f
"- **Class:** {ch['klass']}")
491 if ch.get(
"covenant"):
492 L.append(f
"- **Covenant:** {ch['covenant']}")
494 L.append(f
"- **Sex:** {ch['gender']}")
495 if ch[
"ng_plus"]
is not None:
496 ng =
"New Game" if ch[
"ng_plus"] == 0
else f
"New Game +{ch['ng_plus']}"
497 L.append(f
"- **Playthrough:** {ng}")
498 if ch[
"soul_memory"]
is not None:
500 f
"- **Soul Memory:** {fmt(ch['soul_memory'])} _(total souls earned — main progress metric)_"
502 if ch.get(
"play_time"):
503 L.append(f
"- **Play Time:** {fmt_playtime(ch['play_time'])}")
504 if ch[
"souls"]
is not None:
505 L.append(f
"- **{CURRENCY.get(ch['game'], 'Souls')} held:** {fmt(ch['souls'])}")
506 if ch.get(
"attack")
is not None:
507 L.append(f
"- **Attack Power:** {ch['attack']}")
508 if ch.get(
"vitality")
is not None:
510 f
"- **Vitality:** {ch['vitality']} _({vitality_necklaces(ch['vitality'])})_"
512 if ch.get(
"gourd")
is not None:
514 f
"- **Healing Gourd:** {ch['gourd']} charge"
515 f
"{'' if ch['gourd'] == 1 else 's'} _({gourd_seeds(ch['gourd'])})_"
517 if ch.get(
"skill_points")
is not None:
519 f
"- **Skill Points Held:** {ch['skill_points']}"
520 " _(spendable at a Sculptor's Idol — the points already spent are "
521 "not stored, so this is what is banked, not what has been earned)_"
523 if ch.get(
"memories"):
524 L.append(f
"- **Memories:** {memories_line(ch['memories'])}")
525 if ch[
"humanity"]
is not None:
526 L.append(f
"- **Humanity:** {ch['humanity']}")
527 if ch[
"hp"]
is not None:
528 L.append(f
"- **Max HP:** {fmt(ch['hp'])}")
529 if ch.get(
"posture")
is not None:
530 L.append(f
"- **Max Posture:** {fmt(ch['posture'])}")
531 if ch.get(
"embered")
is not None:
533 "- **Embered:** Yes _(Max HP above includes the +30% ember bonus)_"
535 else "- **Embered:** No _(hollow — Max HP above is the base value)_"
537 if ch.get(
"fp")
is not None:
538 L.append(f
"- **Max FP:** {fmt(ch['fp'])}")
539 if ch.get(
"hollow_lvl"):
541 f
"- **Hollowing:** {ch['hollow_lvl']} _(higher = more deaths without an effigy)_"
543 if ch.get(
"deaths")
is not None:
544 L.append(f
"- **Deaths:** {fmt(ch['deaths'])}")
545 if ch[
"stamina"]
is not None:
546 L.append(f
"- **Stamina:** {fmt(ch['stamina'])}")
548 L.append(f
"- **Cinders of a Lord Placed:** {lords_line(ch['lords'])}")
549 if ch.get(
"endings"):
552 f
"- **Ending{'' if len(ends) == 1 else 's'} Reached:** " +
" · ".join(ends)
554 build = guess_build(ch[
"stats"])
556 L.append(f
"- **Build:** {build}")
560 keys = list(ch[
"stats"].keys())
564 "| " +
" | ".join(STAT_ABBR.get(k, k[:3].upper())
for k
in keys) +
" |",
565 "|" +
"----|" * len(keys),
566 "| " +
" | ".join(str(ch[
"stats"][k])
for k
in keys) +
" |",
571 rows = [k
for k
in keys
if k
in gov]
578 "<summary><b>Attribute Scaling</b> — what each stat "
579 "scales and its soft caps (game-mechanics reference, not read from "
580 "this save)</summary>",
584 caps = f
" {cap[k][:1].upper() + cap[k][1:]}." if cap.get(k)
else ""
585 L.append(f
"- **{k}** ({ch['stats'][k]}) — {gov[k]}.{caps}")
586 L += [
"",
"</details>",
""]
587 if ch[
"game"]
in DS2_GAMES:
588 d = ds2_derived_stats(ch[
"stats"])
589 agl = f
"{d['agility']}" + (
590 f
" _({d['iframes']} roll i-frames)_" if d[
"iframes"]
else ""
593 "### Derived Stats _(computed from attributes — base values before "
594 "rings & equipment; the in-game screen adds ring/gear bonuses on top)_",
596 f
"- **Stamina:** {d['stamina']}",
597 f
"- **Equip Load (max capacity):** {d['equip_load']:.1f}",
598 f
"- **Attunement Slots:** {d['slots']}",
599 f
"- **Agility (AGL):** {agl}",
600 f
"- **Poise (base):** {d['poise']:.1f}",
601 f
"- **ATK: Str:** {d['atk_str']}",
602 f
"- **ATK: Dex:** {d['atk_dex']}",
603 f
"- **Magic DEF:** {d['magic_def']}",
604 f
"- **Fire DEF:** {d['fire_def']}",
605 f
"- **Lightning DEF:** {d['lightning_def']}",
606 f
"- **Dark DEF:** {d['dark_def']}",
609 if ch[
"game"] ==
"ds3":
610 d = ds3_derived_stats(ch[
"stats"], ch.get(
"ring_mods"))
615 def credit(base, kind, unit):
618 parts =
", ".join(f
"+{v:g}{unit} {n}" for n, v
in w[kind])
619 return f
" _({base} base, {parts})_"
622 "### Derived Stats _(computed from attributes, plus the documented "
623 "bonus of any worn ring named beside the value — the game's other "
624 "gear is not read)_",
626 f
"- **Attunement Slots:** {d['slots']}"
627 + credit(d[
"slots_base"],
"slots",
""),
628 f
"- **Equip Load (max capacity):** {d['equip_load']:.1f}"
629 + credit(f
"{d['equip_load_base']:.1f}",
"load_pct",
"%"),
630 f
"- **Item Discovery:** {d['item_discovery']}"
631 + credit(d[
"item_discovery_base"],
"discovery",
""),
636 f
"+{v:g}% {'Max HP' if k == 'hp_pct' else 'Stamina'} ({n})"
637 for k
in (
"hp_pct",
"stam_pct")
642 f
"- **Also from rings:** {', '.join(already)} _(Max HP and "
643 f
"Stamina are read from the save, so they already include "
647 if ch[
"game"]
in (
"dsr",
"ptde"):
648 d = ds1_derived_stats(ch[
"stats"])
650 "### Derived Stats _(computed from attributes — base values before "
651 "rings & equipment)_",
653 f
"- **Attunement Slots:** {d['slots']}",
654 f
"- **Equip Load (max capacity):** {d['equip_load']:.1f}",
657 elif ch[
"tier"] ==
"roster":
659 "_Nightreign is read at roster tier: the save decrypts and every entry "
660 "verifies its own checksum, so the name above is certain, and nothing "
661 "else is claimed. The game keeps no persistent level, attributes or "
662 "souls to print — its progression is relics, unlocked Nightfarers and "
663 "Nightlord kills, and none of those has been pinned against a second "
667 elif ch[
"tier"] ==
"inventory":
669 "_Attributes are not printed for this slot: its stat block did not "
670 "validate (an unrecognised patch or an edited save), and a wrong "
671 "number is worse than none. Inventory and progress below are read "
677 return [f
"- {n}" + (f
" ×{q}" if q
and q > 1
else "")
for n, q
in items]
684 if ch[
"boss_souls"]
and not (
685 ch[
"inv"].get(
"bosssouls")
or ch[
"inv"].get(
"memories")
688 "### Remembrances Held _(major bosses defeated, not yet traded)_"
689 if ch[
"game"] ==
"er"
690 else "### Boss Souls Held _(bosses defeated, soul not yet consumed)_"
692 L += [header,
""] + bullets(ch[
"boss_souls"]) + [
""]
694 L += [
"### Key Items _(progress / areas & shortcuts unlocked)_",
""]
695 L += bullets(ch[
"key_items"]) + [
""]
698 if ch.get(
"bonfires")
and not ch.get(
"bonfire_areas"):
700 f
"### Bonfires Discovered ({len(ch['bonfires'])}) _(areas reached — a "
701 "floor on progress)_",
704 L += [f
"- {b}" for b
in ch[
"bonfires"]] + [
""]
705 if ch.get(
"bonfire_areas"):
706 lit = sum(c
for _a, c, _n, _t, _m
in ch[
"bonfire_areas"])
707 total = sum(t
for _a, _c, _n, t, _m
in ch[
"bonfire_areas"])
708 n = sum(1
for _a, c, _n, _t, _m
in ch[
"bonfire_areas"]
if c)
709 areas = len(ch[
"bonfire_areas"])
714 if ch.get(
"game")
in (
"dsr",
"ptde")
715 else DS2_BONFIRE_NOTE
716 if ch.get(
"game")
in DS2_GAMES
717 else SDT_BONFIRE_NOTE
718 if ch.get(
"game") ==
"sdt"
719 else DS3_BONFIRE_NOTE
724 "Sculptor's Idols Lit" if ch.get(
"game") ==
"sdt" else "Bonfires Discovered"
726 L += [f
"### {head} ({lit} of {total}, in {n} of {areas} areas) _({note})_",
""]
727 for name, c, named, tot, missing
in ch[
"bonfire_areas"]:
728 row = f
"- {name}: {c}/{tot}"
730 row += f
" — {', '.join(named)}"
734 row += f
" _(missing: {' · '.join(missing)})_"
737 if ch.get(
"covenants"):
738 found, total = len(ch[
"covenants"]), ch.get(
"covenant_total")
739 count = f
"{found} of {total}" if total
else f
"{found}"
741 f
"### Covenants Found ({count}) _(discovered — a floor; "
742 "the one currently worn is the Covenant field above)_",
745 L += [f
"- **{cov}:** {', '.join(w)}" for cov, w
in ch[
"covenants"].items()] + [
748 note = missing_note(
"Not found yet", ch.get(
"covenants_missing"))
751 if ch.get(
"world_flags"):
752 got = sum(c
for _a, c, _n, _t, _m
in ch[
"world_flags"])
753 total = sum(t
for _a, _c, _n, t, _m
in ch[
"world_flags"])
754 L += [f
"### World State ({got} of {total} tracked) _({DS1_WORLD_NOTE})_",
""]
755 for cat, c, names, tot, missing
in ch[
"world_flags"]:
756 row = f
"- {cat}: {c}/{tot}"
760 row += f
" — {' · '.join(names)}"
762 row += f
" _(not yet: {' · '.join(missing)})_"
765 if ch.get(
"minibosses"):
766 dead = sum(c
for _a, c, _n, _t, _m
in ch[
"minibosses"])
767 total = sum(t
for _a, _c, _n, t, _m
in ch[
"minibosses"])
769 f
"### Minibosses Defeated ({dead} of {total} tracked) _({SDT_MINIBOSS_NOTE})_",
772 for area, c, names, tot, alive
in ch[
"minibosses"]:
773 row = f
"- {area}: {c}/{tot}"
777 row += f
" — {' · '.join(count_dupes(names))}"
782 row += f
" _(still alive: {' · '.join(count_dupes(alive))})_"
785 if ch.get(
"npc_states"):
786 got = sum(c
for _f, c, _g, _t
in ch[
"npc_states"])
787 total = sum(t
for _f, _c, _g, t
in ch[
"npc_states"])
789 f
"### NPC States ({got} of {total} tracked) _({DS3_NPC_NOTE})_",
792 for family, c, names, tot
in ch[
"npc_states"]:
793 row = f
"- {family}: {c}/{tot}"
799 said = [n
for n
in names
if not n.startswith(
"character ")]
800 rest = len(names) - len(said)
802 row += f
" — {' · '.join(count_dupes(said))}"
804 row += f
" _(+{rest} more, known only by entity id)_"
807 if ch.get(
"world_events"):
808 got = sum(c
for _f, c, _t
in ch[
"world_events"])
809 total = sum(t
for _f, _c, t
in ch[
"world_events"])
811 f
"### World Events ({got} of {total} tracked) _({DS1_EVENT_NOTE})_",
814 for family, c, tot
in ch[
"world_events"]:
815 L.append(f
"- {family}: {c}/{tot}")
817 if ch.get(
"enemies"):
818 dead = sum(c
for _a, c, _n, _t, _m
in ch[
"enemies"])
819 total = sum(t
for _a, _c, _n, t, _m
in ch[
"enemies"])
821 f
"### One-Time Enemies Defeated ({dead} of {total} tracked)"
822 f
" _({DS3_ENEMY_NOTE})_",
825 for area, c, names, tot, alive
in ch[
"enemies"]:
826 row = f
"- {area}: {c}/{tot}"
828 row += f
" — {' · '.join(count_dupes(names))}"
830 row += f
" _(still alive: {' · '.join(count_dupes(alive))})_"
833 if ch.get(
"pickups"):
834 got = sum(c
for _a, c, _t, _m
in ch[
"pickups"])
835 total = sum(t
for _a, _c, t, _m
in ch[
"pickups"])
836 note = SDT_PICKUP_NOTE
if ch[
"game"] ==
"sdt" else DS3_PICKUP_NOTE
838 f
"### Items Collected ({got} of {total} tracked) _({note})_",
841 for area, c, tot, missing
in ch[
"pickups"]:
842 row = f
"- {area}: {c}/{tot}"
846 row += f
" _({len(missing)} still out there)_"
853 (area, missing)
for area, c, _t, missing
in ch[
"pickups"]
if c
and missing
858 "<summary>Where the missing items are — the ones with a known "
859 "location</summary>",
862 for area, missing
in todo:
863 L.append(f
"**{area}** — {len(missing)} missing")
865 L += [f
"- {item}" for item
in count_dupes(missing)]
867 L += [
"</details>",
""]
868 if ch.get(
"questlines"):
872 "### Rewards Obtained _(one-off rewards from NPCs, invaders and "
873 "landmark pickups — a progress floor)_",
877 f
"- **{src}:** {', '.join(rw)}" for src, rw
in ch[
"questlines"].items()
883 "gate":
"progression",
884 "clear":
"cleared (NG+)",
886 found, total = len(ch[
"bosses"]), ch.get(
"boss_total")
887 count = f
"{found} of {total} tracked" if total
else f
"{found}"
889 f
"### Bosses Defeated ({count}) _(a floor — from defeat "
890 "flags, held boss souls, progression, and NG+ clears; a boss whose soul "
891 "was consumed and isn't gated may still be missing)_",
894 for boss, srcs
in ch[
"bosses"].items():
895 L.append(f
"- {boss} _({', '.join(SRC[s] for s in srcs)})_")
900 avail = ch.get(
"bosses_available")
or []
901 rest = [b
for b
in (ch.get(
"bosses_missing")
or [])
if b
not in avail]
904 f
"_Available now — every prerequisite dead and the area already "
905 f
"reached (from the game's fixed route, not this save): "
906 f
"{' · '.join(avail)}._",
910 "No evidence yet" + (
", and behind something else" if avail
else ""), rest
916 ch.get(
"equipped_weapons")
917 or ch.get(
"equipped_armor")
918 or ch.get(
"equipped_rings")
919 or ch.get(
"equipped_ammo")
921 L += [
"### Equipped _(worn gear read from the equip slots)_",
""]
923 f
"- **{slot}:** {name}"
924 for slot, name
in ch.get(
"equipped_weapons", {}).items()
927 f
"- **{slot}:** {name}"
928 for slot, name
in ch.get(
"equipped_armor", {}).items()
930 if ch.get(
"equipped_rings"):
934 eff = dict(ch.get(
"ring_effects")
or [])
936 L.append(
"- **Rings:**")
938 f
" - {n}" + (f
" — {eff[n]}" if n
in eff
else "")
939 for n
in ch[
"equipped_rings"]
942 L.append(f
"- **Rings:** {', '.join(ch['equipped_rings'])}")
943 if ch.get(
"equipped_ammo"):
944 L.append(f
"- **Ammo:** {', '.join(ch['equipped_ammo'])}")
949 if any(ch[
"inv"].get(c)
for c
in CAT_ORDER):
950 L += [
"### Inventory",
""]
953 listed = {n
for n, _q
in ch[
"boss_souls"]} | {n
for n, _q
in ch[
"key_items"]}
954 for cat
in CAT_ORDER:
955 items = ch[
"inv"].get(cat)
958 if cat ==
"goods" and listed:
959 items = [it
for it
in items
if it[0]
not in listed]
964 if cat ==
"bosssouls":
965 great = [it
for it
in items
if it[0]
in DS2_GREAT_SOULS]
966 normal = [it
for it
in items
if it[0]
not in DS2_GREAT_SOULS]
967 for title, group
in ((
"Great Boss Souls", great), (
"Boss Souls", normal)):
969 L += [f
"#### {title}",
""] + bullets(group) + [
""]
971 title = cat_title(ch[
"game"], cat)
972 if cat ==
"goods" and listed:
973 title +=
" _(boss souls and key items are listed above)_"
974 L += [f
"#### {title}",
""] + bullets(items) + [
""]
975 if ch[
"unknown_count"]:
977 f
"_{ch['unknown_count']} inventory item(s) had IDs not in the name "
978 "database (upgraded / infused variants) and were omitted._",
981 if ch.get(
"internal_count"):
983 f
"_{ch['internal_count']} further entr"
984 f
"{'y' if ch['internal_count'] == 1 else 'ies'} carried only an engine "
985 "development name — placeholder rows and debug items rather than "
986 "anything the game hands you — and were left out._",
989 if ch.get(
"suppressed_count"):
991 f
"_{ch['suppressed_count']} further entr"
992 f
"{'y' if ch['suppressed_count'] == 1 else 'ies'} were engine state "
993 "rather than inventory — Sekiro has no armour system, so the character's "
994 "own body models sit in the protector table, and the `Virtual Weapon:` "
995 "rows restate a Combat Art already listed under its own name. Counted "
996 "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).