Difficulty Unlock Loop

The key words MUST, MUST NOT, and MAY are used as in RFC 2119.

PSZ's replay structure: clearing the story raises the ceiling. Three tiers — Normal, Hard, Super-Hard — gate behind story completion, per character. The per-difficulty enemy scaling, rewards (#318 tiers), and technique levels already exist; this is the loop that gates access to them.

The rule

Only the finale gates tiers; clearing side or variant quests on a difficulty does nothing. The progression map (GameState.DIFFICULTY_PROGRESSION) is normal → hard → super-hard; the finale id is GameState.STORY_FINALE_QUEST.

Persistence

Unlock state is per character, mirroring completed_missions exactly: GameState.unlocked_difficulties is the runtime list, CharacterManager swaps it in/out per slot, SaveManager persists it in the character dict (save v7). Old saves migrate by seeding ["normal"]; a character whose completed_missions already contains the finale retroactively keeps Hard (the cleared difficulty is unrecoverable from pre-v7 saves, so the grant is conservative — first tier up only).

Guild counter gating (removed for now — see status note)

When the selector returns (at character select), tiers locked for the character MUST be presented as locked rather than silently hidden, with the unlock condition surfaced ("Clear the story on Normal"), and a locked tier MUST NOT be selectable. Current state: the guild counter has no difficulty submenu at all — accepting a quest goes straight to the confirm on Normal. The historical per-quest submenu (all three tiers, the [LOCKED] tag, input-gate + async-confirm re-checks) is removed; this gating contract moves to the character-select selector when it lands.

Implemented by

Testing

Unit (test_difficulty_unlock): the finale-clear rule per tier, the non-finale no-op, and the always-on Normal — all exercising GameState/report_quest directly, so they stay valid with the picker removed. Post-build: the regression matrix's dark_castle phase asserts Hard is unlocked in the post-finale save snapshot.