This document tracks what has been built, what is partially complete, and what remains to bring the TypeScript web port to feature parity with the original Lex Talionis Python/Pygame engine.
84 source files, ~44,400 lines of TypeScript.
Builds cleanly with zero type errors. All four development phases (Foundation,
Playable, Visual Polish, Mobile/Distribution) are complete. The engine loads
.ltproj game data over HTTP and runs at 60 fps on Canvas 2D with dynamic
viewport scaling for mobile and desktop.
The engine supports loading different .ltproj projects via the ?project=
query parameter. Both chunked (directory-per-type with .orderkeys) and
non-chunked (single JSON array files) data formats are supported.
Completed:
- Configurable project path via
?project=query param - Non-chunked game_data fallback (items.json, skills.json, etc.)
- Non-chunked tilemap fallback (single tilemaps.json)
- Engine-level shared assets separated from project assets (sprites/menus, platforms, cursor)
- Combat palette loading: added
palette_data/subdirectory fallback path - URL encoding:
ResourceManager.resolveUrl()now encodes path segments for spaces/special chars - Title screen: animated panorama fallback (tries
title_background0.pngwhen single file missing) - Icons, fonts, base-surf, sprite-loader all encode NIDs in URLs
Known Limitations (per-project content):
- Missing
combat_*.pngpanoramas in non-default projects (combat backgrounds show nothing) - Projects may reference combat effects/palettes not present — renders without them gracefully
- Settings
Text Speedhad no effect on dialogue typing. (Fixed)EventStatenow passes_setting_text_speedintoDialog, and dialog typing now uses LT-style time-based cadence (ms-per-character, including0= instant). - Some Ch.5 destructible village events failed to fire from
DestroyVillageXregions. (Fixed) Event conditions in default data can target siblingVillageXNIDs while the interaction region isDestroyVillageX. Added compatibility fallback for Destructible triggers to retry with sibling region context when needed. - Chest/Door region checks could crash in menu state (
comps.some is not a function). (Fixed)evaluateCondition(unit.can_unlock(region))assumed item components were array-shaped, but runtimeItemObject.componentsis aMap. Added robustMap/array/object handling, support forcan_unlockexpressions, and region-prefix checks (Chest/Door). - Talk command menu missed level-scoped conversations (e.g. Natasha→Joshua in Ch.5). (Fixed)
Talk option detection in
MenuStatecalledgetEventsForTrigger()withoutlevelNid, so level-specificon_talkevents were filtered out. AddedlevelNidin both talk option discovery and talk target re-check. - Harness chapter intros (Ch.2/Ch.3) intermittently soft-locked with empty top state. (Fixed)
harness.loadLevel(clean=false)was manually pushingeventafterfree, whileFreeStatealready auto-pusheseventwhen level_start events exist. This could stack duplicateEventStateinstances and leave transient/empty state behavior in long intros. Fix: removed manual event push from harness and let normal state flow handle it. - Animation combat sometimes shows cyan/red placeholder blocks. (Fixed)
AnimationCombatnow waits ininituntil both sides resolve a realmainFrame(or timeout fail-safe), preventing first-load async sprite races from flashing stub rectangles at combat start. - Harness mode blocked by project picker overlay. (Fixed) When
multiple
.ltprojfolders existed and?project=was omitted, the picker overlay preventedwindow.__harness.readyfrom ever becoming true. Harness mode now auto-selectsdefault.ltproj(or first discovered project fallback) without redirect, restoring deterministic Playwright startup. - First dialogue still renders over the portrait. (Fixed) Dialog now
auto-sizes to text content width and uses
get_desired_center()mapping for portrait-aware horizontal positioning (matching Python). - Combat animations at half speed sometimes. (Fixed) Removed
Math.max(1, ticks)override that tied animation speed to browser refresh rate. Animation ticking is now unconditional at the top ofupdate(), matching Python'supdate_anims()pattern. - Enemies leave blue rectangle at start position when attacking. (Fixed)
Added
highlight.clear()inFreeState.begin(),FreeState.end(), andTurnChangeState.begin()to match Python's highlight cleanup lifecycle. - Lose cursor control after combat. (Fixed) Added finished-unit
check to
WeaponChoiceState.begin()with'repeat'return, plus added'repeat'to all dead-unit early-exit paths in MoveState, MenuState, and TargetingState for instant state cascade. - Red rectangle randomly appears during magic attack. (Fixed) Cleared
this.targetsinTargetingState.end()to prevent stale red rectangle draw when CombatState (transparent) draws on top. - Terrain platforms swap/move and sprites float in ranged/magic combat.
(Fixed) Three related bugs in combat animation platform/sprite positioning:
at_rangeoff-by-one — now computesatRange = distance - 1matching Python- Sprites now receive
range_offsetandpan_offsetso they track with platforms - Shake direction negated for sprites (
-totalShakeX) matching Python behavior
- Combat UI layout is wrong. (Fixed) Corrected name tag dimensions (66x16, matching Python sprites), centered name text, fixed HP bar height (56→40px), adjusted Y positioning, and removed always-shown CRT row.
- Reinforcements arrive too early in Ch.1. (Fixed) Changed event
condition fallback from
truetofalse— events with un-evaluable conditions are now skipped instead of fired. Added error logging to JS fallback evaluator. - Portrait mouths keep moving after dialog text finishes scrolling. (Fixed)
Event dialog now toggles portrait talking based on dialog typing state
(
typingvswaiting) instead of only stopping on full dialog close. - Cutscene background can be missing for first lines after
change_background. (Fixed)change_backgroundnow blocks event command progression until panorama load resolves, matching Python's synchronous behavior and preventing async race frames.
-
Process docs update (agent commit/push policy clarified):
- Updated
AGENTS.mdcommit policy section to explicitly state the rule applies to all session types and all edit scopes (code/docs/config).
- Updated
-
Dialogue text-speed parity fix (settings now actually affect typing):
- Updated
src/ui/dialog.tsto use LT-style time-based typing speed (milliseconds per character) instead of a fixed chars-per-frame step. - Updated
src/engine/states/game-states.tsto read_setting_text_speedand pass it into each newDialoginstance, with default fallback32. - Added LT dialog speed overrides:
- Per-command
text_speed(keyword and semicolon positional forms) - Inline text commands
{speed:X},{starting_speed},{max_speed}now update typing cadence during the same dialog line.
- Per-command
Text Speed = 0now behaves like LT max-speed mode (instant reveal).
- Updated
-
Event-state dialog/background parity fixes + regression coverage:
- Fixed portrait mouth animation lifecycle in
src/engine/states/game-states.ts: speaking portraits now start/stop talking on dialog typing-state transitions, and reliably stop on skip/dismiss paths. - Fixed async cutscene background race in
src/engine/states/game-states.ts:change_backgroundnow blocks until panorama load completes (or fails), with token-guarded completion to avoid stale async overwrites. - Added regression in
tests/harness.spec.ts:Dialog portraits stop talking while waiting for input.
- Fixed portrait mouth animation lifecycle in
-
AI region interaction + recruit persistence + Sacred Stones soak automation:
- Added two new harness regressions in
tests/harness.spec.ts:- Ch.2 AI-driven
PursueVillageDestructibleinteraction (forced enemy AI phase) verifiesDestroyVillage3+Village3region consumption andRuin3layer reveal. - Recruit persistence regression: simulated recruited Joshua survives chapter cleanup/reload
with player allegiance intact and appears in
prep_pickparty roster.
- Ch.2 AI-driven
- Fixed persistent-unit chapter load behavior in
src/engine/game-state.tsto match Python: persisted units now preserve runtime team/AI allegiance instead of being overwritten by next-level prefab team/AI fields. - Added Sacred Stones reliability soak automation:
- New script
scripts/sacred-stones-soak.mjsloops Playwright Sacred Stones suites (SOAK_ITERATIONS,SOAK_GREP,SOAK_WORKERSconfigurable, fail-fast on first failure). - Added npm scripts:
test:harness,test:ss:soak. - Documented soak usage in
TESTING.md.
- New script
- Added screenshots:
60-ch2-ai-destructible-interact-ruin3.png,61-recruit-persistence-prep-flow-joshua.png.
- Added two new harness regressions in
-
Chapter 4/5 regression matrix sweep (outro branches, villages, arena, ordering, turn idempotency):
- Added five harness regressions in
tests/harness.spec.tsfor:- Ch.4 outro branch matrix across Artur/Lute permutations (Artur-only, Lute-only, both alive, both dead)
- Ch.5
Village1/3/4visit reward matrix with one-time reward + region-consumption checks - Ch.5 arena interaction flow (menu option, event progression, return-to-map control)
- Ch.5 visit-vs-destroy ordering semantics (one-time region consumption in both directions)
- Ch.5 turn-event idempotency for
Turn2/4/8over repeated long-window retriggers
- Updated region cleanup semantics in
src/engine/states/game-states.tsso triggering one side of village Visit/Destructible siblings consumes both matching one-time regions on the same tile. - Added screenshots:
55-ch4-outro-branch-matrix.png,56-ch5-village134-visit-matrix.png,57-ch5-arena-flow-return.png,58-ch5-village-ordering-visit-vs-destroy.png,59-ch5-turn-event-idempotency.png. - Focused Playwright pass for these five new regressions: 5/5. Build also passes (
npm run build).
- Added five harness regressions in
-
Chapter 4 outro branch matrix regression coverage. Add tests for Artur-only, Lute-only, both alive, and both dead paths; verify dialogue/event progression and clean transition behavior.
-
Chapter 5 village visit matrix regression coverage. Add deterministic tests for
Village1/3/4rewards and region consumption; verify no duplicate rewards on re-interact attempts. -
Chapter 5 arena interaction flow coverage. Validate arena menu availability, interaction state flow, and safe return to map control without soft-lock.
-
Chapter 5 village destroy-vs-visit ordering checks. Add race-condition tests for enemy destructible events vs player visits to ensure one-time semantics and layer toggles are correct.
-
Chapter 5 turn-event idempotency sweep. Re-trigger
Turn2/4/8conditions across long frame windows and confirm no duplicate group spawn or stale event-state stacking. -
Enemy AI region interaction regression. Add harness coverage for AI-driven
Destructibleinteractions and validate region removal + event side effects match manual interactions. -
Recruit persistence across chapter transitions. Add regression tests ensuring recruited units remain correctly assigned/serialized through subsequent chapter loads and prep flow.
-
Sacred Stones reliability soak run automation. Add a long-run harness pass that executes multi-chapter mechanics batches repeatedly and fails on non-deterministic state regressions.
-
Chapter 4/5 additional event sweep (Village1, Turn3 cameo, Turn4 brigands):
- Added mechanics regressions in
tests/harness.spec.tsfor:- Ch.4 Village1 visit grants
Iron_Axeand consumes region - Ch.4 Turn3 cameo event (
L'arachel/Dozla/Rennac) exits cleanly with temporary units removed from map - Ch.5 Turn4 event spawns
Brigand2group (118,119)
- Ch.4 Village1 visit grants
- Added screenshots:
52-ch4-village1-iron-axe.png,53-ch4-turn3-cameo-cleared.png,54-ch5-turn4-brigand2-spawn.png. - Full Playwright harness suite now passes: 45/45.
- Added mechanics regressions in
-
Chapter 4 event edge-case sweep (villages, trigger region, snag bridge):
- Added mechanics regressions in
tests/harness.spec.tsfor:- Ch.4 Village2 visit recruits Lute and consumes region
- Ch.4 Trigger region spawns
RevenantReingroup on turn-change - Ch.4 Snag death triggers bridge layer reveal (
show_layer;Snag)
- Added screenshots:
49-ch4-village2-recruits-lute.png,50-ch4-trigger-revenant-reinforcements.png,51-ch4-snag-bridge-layer-revealed.png. - Full Playwright harness suite now passes: 42/42.
- Added mechanics regressions in
-
Chapter 3 outro branch coverage (recruit-dependent transition):
- Added mechanics regressions in
tests/harness.spec.tsfor:- Ch.3 outro branch with Neimi+Colm alive confirms Colm becomes
playerduring outro before transition to Ch.4 - Ch.3 outro branch with Colm dead still transitions cleanly to Ch.4 without title-state fallback
- Ch.3 outro branch with Neimi+Colm alive confirms Colm becomes
- Added screenshots:
47-ch3-outro-colm-player-before-ch4.png,48-ch3-outro-colm-dead-transition-ok.png. - Full Playwright harness suite now passes: 39/39.
- Added mechanics regressions in
-
Chapter 3 Colm flow coverage (spawn + recruitment):
- Added mechanics regressions in
tests/harness.spec.tsfor:- Ch.3
other_turn_changeevent spawns Colm and moves him to chest room - Ch.3 Neimi->Colm talk recruits Colm to player team
- Ch.3
- Added screenshots:
45-ch3-colm-turn-event-spawn.png,46-ch3-neimi-recruits-colm.png. - Full Playwright harness suite now passes: 37/37.
- Added mechanics regressions in
-
Destructible village sweep (Ch.2 + Ch.5) + trigger compatibility fix:
- Added mechanics regressions in
tests/harness.spec.tsfor:- Ch.2
DestroyVillage1/2/3->Ruin1/2/3layer visibility + region removal - Ch.5 destructible village interactions (
DestroyVillage2/4) ->Ruin2/4
- Ch.2
- Fixed region-trigger compatibility in
src/engine/states/game-states.ts(menu and AI interaction paths): whenDestructibletrigger fromDestroyXhas no matching event, retry using siblingXregion context. - Added screenshots:
43-ch2-destructible-villages-ruins.png,44-ch5-destructible-villages-ruins.png. - Full Playwright harness suite now passes: 35/35.
- Added mechanics regressions in
-
Chapter 3 full lock interaction sweep (all chest/door variants):
- Added mechanics regressions in
tests/harness.spec.tsfor:- All remaining Ch.3 chests (
Chest2/3/4) unlock + loot checks - Remaining Ch.3 doors (
Door2/3) unlock + region removal checks
- All remaining Ch.3 chests (
- Hardened harness flow by reloading clean Ch.3 per lock interaction case to avoid cross-case turn-state contamination from finished action flags.
- Added screenshots:
41-ch3-all-chests-unlocked.png,42-ch3-door2-door3-unlocked.png. - Full Playwright harness suite now passes: 33/33.
- Added mechanics regressions in
-
Chapter 3 unlock interaction coverage + can_unlock fix:
- Added mechanics regressions in
tests/harness.spec.tsfor:- Ch.3 chest interaction gating + unlock + loot (
Javelin) - Ch.3 door interaction gating + unlock region removal
- Ch.3 chest interaction gating + unlock + loot (
- Fixed
unit.can_unlock(region)condition handling insrc/events/event-manager.tsto support runtimeMapcomponents andcan_unlockcomponent expressions (includingregion.nid.startswith(...)). - Updated unlock consumption in
src/engine/states/game-states.tsto treatcan_unlockitems as key items for use decrement/removal. - Added screenshots:
39-ch3-chest1-unlock-javelin.png,40-ch3-door1-unlock-opened.png. - Full Playwright harness suite now passes: 31/31.
- Added mechanics regressions in
-
Chapter interaction coverage expansion (villages + shops):
- Added chapter mechanics regressions in
tests/harness.spec.tsfor:- Ch.2 Village1 Visit grants
Red_Gemand consumes region - Ch.5 Village2 Visit grants
Armorslayerand consumes region - Ch.5 Vendor and Armory region menu options appear on correct tiles
- Ch.2 Village1 Visit grants
- Added screenshots:
36-ch2-village1-visited-red-gem.png,37-ch5-village2-visited-armorslayer.png,38-ch5-vendor-armory-menu-options.png. - Sacred Stones chapter suites (
Later Chapters+Chapter Mechanics) now pass 15/15.
- Added chapter mechanics regressions in
-
Deeper Sacred Stones chapter sweep (Ch.2–Ch.5 mechanics):
- Added chapter mechanics tests in
tests/harness.spec.ts:- Ch.3 seize objective transitions to Ch.4
- Ch.4 turn-2 reinforcements (
Turn2Rein) spawn - Ch.5 turn-2 and turn-8 brigand reinforcements spawn
- Ch.5 Natasha→Joshua talk recruitment converts Joshua to player team
- Fixed talk menu regression in
src/engine/states/game-states.tsby passinglevelNidintogetEventsForTrigger()foron_talkchecks. - Added screenshots:
32-ch3-seize-transition-ch4.png,33-ch4-turn2-reinforcements.png,34-ch5-turn2-turn8-reinforcements.png,35-ch5-natasha-recruits-joshua.png. - Full Playwright harness suite now passes with expanded coverage: 26/26.
- Added chapter mechanics tests in
-
Sacred Stones multi-chapter smoke coverage + harness state fix:
- Added chapter smoke tests for Ch.2–Ch.5 in
tests/harness.spec.ts: clean-mode map load checks + non-clean intro progress checks. - Added screenshots for each chapter intro/map checkpoint:
30-ch{2..5}-clean-map.png,31-ch{2..5}-intro-progress.png. - Fixed duplicate
EventStatestacking insrc/harness.tsby removing redundant manualchange('event')inloadLevel(). - Full harness suite now passes with expanded coverage: 22/22.
- Added chapter smoke tests for Ch.2–Ch.5 in
-
Animation combat sprite-load race fix + regression test:
- Fixed startup race in
src/combat/animation-combat.ts:updateInit()now gates transition to visible phases until both combatants have resolvedmainFramedraw data, with a 1500ms fail-safe timeout. - Added Playwright regression in
tests/harness.spec.ts:Animation Combat Rendering › combat sprites resolve before visible animation phases (no stub boxes). - Captures
test-screenshots/26-animation-combat-no-stubs.png. - Full harness suite now passes: 14/14.
- Fixed startup race in
-
Harness + visual regression stabilization (Sacred Stones test run):
- Fixed harness boot regression in
main.ts: project picker is now bypassed in?harness=trueruns, defaulting todefault.ltprojfor deterministic automated tests. - Fixed flaky magic-sword regression assertion in
tests/harness.spec.ts: test now verifies deterministic weapon-use consumption (Light Branduses decremented) instead of requiring guaranteed HP damage on RNG-dependent hit. - Re-ran full harness suite after fixes: 13/13 passing.
- Fixed harness boot regression in
-
Seven bug fixes across combat, UI, events, and state management:
- Dialog over portrait: Auto-sized dialog width to text content, ported
Python's
get_desired_center()mapping for portrait-relative positioning. - Combat animation speed: Made
tickAnimsunconditional in top-levelupdate()(matching Python), removedMath.max(1, ticks)from 5 call sites. - Blue highlight rectangle: Added
highlight.clear()to FreeState begin/end and TurnChangeState (matching Python's cleanup lifecycle). - Cursor loss after combat: Added finished-unit guard to WeaponChoiceState,
added
'repeat'returns to all dead-unit early-exit paths for instant cascade. - Red rectangle during magic combat: Cleared targets in TargetingState.end() to prevent stale draw under transparent CombatState.
- Combat UI layout: Fixed name tag size (80→66x16), centered name text, fixed HP bar height (56→40px), adjusted stat layout to fit.
- Early reinforcements: Changed event condition fallback from
truetofalsein event-manager.ts, added error logging to JS fallback evaluator.
- Dialog over portrait: Auto-sized dialog width to text content, ported
Python's
-
Combat animation platform/sprite positioning fix (two passes). Fixed six bugs causing terrain pillars to move around and sprites to float during ranged/magic combat animations:
- Computed
atRange = distance - 1(matching Python) instead of passing raw Manhattan distance. Fixes melee getting ranged pan/poses/platforms. - Added
leftRangeOffset,rightRangeOffset,panOffset,totalShakeX,totalShakeYtoAnimationCombatRenderState.drawBattleSpritenow passes per-side range offsets todrawAnimFrame, which applies them Python-faithfully:spriteLeft = -totalShakeX + rangeOffset + panOffset. - Negated shake X for sprites (
-totalShakeX) matching Python'sshake = (-total_shake_x, total_shake_y). Combined screen + platform shake intototalShakeX/totalShakeYfor both platforms and sprites. - Pan logic overhaul: Added phase-change pan in
updateBeginPhase()so the camera pans to focus on each new attacker (matching Python'sset_up_combat_animation -> move_camera). Splitpan()intopanAway()(simple toggle) andpanBack()(looks at next strike to determine focus). AddedpanAwayboolean toBattleAnimationwith safety cleanup when a pose ends without issuing the return pan command. - Pan advancement now uses a separate frame accumulator for frame-rate independence (ticks at 60fps like Python regardless of browser refresh rate).
- Computed
-
Level progression / chapter chaining. Implemented full level-to-level transitions matching the Python engine's behavior:
win_gamecommand now sets_win_gameflag (deferred, not immediate)finishAndDequeue()checks_win_gameflag after each event, firesLevelEndtrigger for outro cutscenes, then callslevelEnd()levelEnd()resolves next level via_goto_levelgame var override or sequential order (skipping debug levels), then async loads the next levelcleanUpLevel()on GameState persists player units across levels (heals HP, clears rescue state, resets turn flags, removes non-persistent units)loadLevel()restores persistent units from previous level, placing them at positions defined in the new level's unit listset_next_chapterevent command overrides sequential progressionlose_gamecommand sets_lose_gameflag (deferred, returns to title)- Generic units set
persistent = false(only unique units carry over) - Added
go_to_overworldfield toLevelPrefabtype - Added
killUnitandtriggerEventto test harness - Fixed timing bug where
.then()callback ran before deferred state machine ops flushed, causing1 Introevent to be dequeued prematurely. Fix: null outcurrentEventinlevelEnd()before async load, deferlevelTransitionInProgressreset tobegin()instead of.then() - Ch.1 intro cutscene now verified: chapter_title + transition + speak all play
- Three Playwright tests: cutscene verification + combat_end trigger + direct flag
- All 12 tests pass (existing + new)
-
Magic sword / wind sword freeze fix. Fixed
castSpellinanimation-combat.tsto check the item'sbattle_cast_animcomponent (e.g. "Gustblade", "Lightning", "Nosferatu") before falling back to the item NID. Without this, spell effects never spawned, causing the animation to loop forever waiting forend_parent_looporspell_hit. Also implementedmagic_at_rangedynamic damage initem-system.ts(swaps STR→MAG and DEF→RES at distance > 1). -
Multi-project support. Fixed 3 hardcoded asset paths (base-surf, sprite-loader, cursor) to use configurable base URLs. Added
ResourceManager.getBaseUrl()accessor. Separated engine-level shared assets (/game-data/) from project-level assets (/game-data/{project}.ltproj/). -
Non-chunked data format support.
loadChunked()now falls back to loading singlegame_data/{type}.jsonarray files when.orderkeysdirectories don't exist.loadTilemaps()now triestilemaps.jsonbulk file before individual tilemap files. -
EXP bar and level-up display overhaul. Replaced placeholder canvas-primitive EXP bar and stat box with a faithful port of the original Python engine:
- New
ExpBarclass using the originalexpbar.pngsprite sheet (144x24 background, 3x7 begin cap, 1x7 middle fill, 2x7 end cap). Iris fade in/out animation. - New
LevelUpScreenclass with scroll-in/out animation, sequential stat spark reveals, color-cycling underlines (sine wave blend), BMP font rendering, portrait. - CombatState now uses a 7-phase EXP state machine matching the original:
exp_init → exp_wait (466ms) → exp0 (1 frame/EXP) → exp100 (wrap) → exp_leave → level_up → level_screen. - Added
playSfxLoop/stopSfxto AudioManager for looping "Experience Gain" SFX. - Uses the original
level_screen.pngandstat_underline.pngsprites. - Sound effects: "Experience Gain" (loop), "Level Up", "Level_Up_Level", "Stat Up".
- New
- Combat palette path fix — Non-chunked palettes at
palette_data/combat_palettes.jsonnot found because engine looks one directory level up. - URL encoding for resource NIDs — Tilesets, portraits, icons, panoramas, and music
with spaces/special characters in NIDs fail to load. Need
encodeURIComponent()orencodeURI()on URL path segments. - Animated title panoramas — Projects with numbered frames (
title_background0.pngthroughtitle_background32.png) instead of singletitle_background.png.
- Initiative bar rendering UI (visual bar showing unit order)
- Non-silent promotion choice UI (visual class selection)
- Supply menu state UI
- Aura propagation, charge/cooldown, conditional activation, proc skills
- RNG mode integration into combat solver
- Difficulty selection UI
- Roam AI for NPCs, shop/talk menu in roam mode
- Rescue icon, status effect icons, movement arrows on map
- Growth rates display, support list, weapon rank letters in info menu
- Base screen sub-menus (supports, codex, BEXP, sound room, achievements)