Skip to content

Merge dev → main (automated)#599

Merged
SorraTheOrc merged 107 commits into
mainfrom
release/dev-to-main-20260705205307
Jul 5, 2026
Merged

Merge dev → main (automated)#599
SorraTheOrc merged 107 commits into
mainfrom
release/dev-to-main-20260705205307

Conversation

@SorraTheOrc

Copy link
Copy Markdown
Member

Automated release created by ship skill.

Sorra the Orc added 30 commits June 20, 2026 17:20
Bug: restoreFromCheckpoint() created new FeudalismRenderer and
FeudalismAnimator instances (with new containers / Phaser game objects)
without destroying the originals, resulting in two complete sets of UI.

Fix: Follow the Beleaguered Castle pattern — mutate the existing session
in-place (Object.assign) and reuse the existing renderer and animator.
This avoids creating duplicate containers. refreshAll() already calls
removeAll(true) on every container before re-rendering, so existing
children are properly destroyed before new ones are created.

Changes:
- Replace  with
- Remove creation of new FeudalismRenderer and FeudalismAnimator instances
- Remove calls to createContainers / createInstructions / createInfluenceDisplay
- Add clearMarketSelection() before refreshAll() to reset selection state
- Remove redundant second refreshAll() call (refreshAll is already called
  by setPhase('player-turn') downstream)
- Add comprehensive JSDoc explaining the in-place mutation pattern
…rce leaks

Improvements:
1. Added port conflict detection (isPortInUse) to check if port 3000 is occupied before starting dev server
2. Added checkAndCleanupStaleDevServer() to detect and remove stale lock files from orphaned PIDs
3. Added installDevServerCleanupHandlers() - SIGTERM/SIGINT handlers and process.on('exit') to clean up tracked child processes and lock files on forced exit
4. Added trackChildProcess() to manage child process lifecycle for signal-based cleanup
5. Integrated isPortInUse check into ensureDevServer() with informative warnings about port conflicts
6. Updated killDevServer() to untrack children from the cleanup list after exit
7. Added dev-server-cleanup.test.ts with comprehensive tests for stale lock detection, port conflict handling, and tmp directory recovery
8. Updated docs/DEVELOPER.md troubleshooting section with process leak info
…st processes

In killDevServer(), when refCount reaches 0 and child is null
(server was already running, e.g. user started it with npm run dev),
the code was killing the dev server by PID. This terminated the
user's dev server along with test processes.

Changed to only kill the dev server when child is non-null
(i.e., we started it). When child is null, clean up the lock
file but leave the existing server running on port 3000.
…localization system

Replace hardcoded title/body string literals in UNIFIED_TUTORIAL_STEPS
with i18n key references (titleKey/bodyKey). Create English locale bundle
(tutorial-en.ts) containing all 13 steps' text. Add resolveTutorialStepText()
helper for runtime resolution via t().

Changes:
- TutorialFlow.ts: title/body -> titleKey/bodyKey in UnifiedTutorialStepDef
- TutorialFlow.ts: add resolveTutorialStepText() and re-export tutorialKey
- MainStreetTutorialHints.ts: register English locale at module load time
- MainStreetTutorialHints.ts: use t(step.titleKey/bodyKey) for display
- New: example-games/main-street/i18n/tutorial-en.ts (English locale bundle)
- New: tests/main-street/tutorial-i18n.test.ts (16 i18n-specific tests)
- Updated: tutorial-flow.test.ts (use titleKey/bodyKey + i18n text resolution)
- Updated: tutorial-text-updates.test.ts (resolve via i18n t() function)
- New: docs/main-street/tutorial-localization.md (how to edit/add translations)

Acceptance criteria:
1. Tutorial step copy is no longer hardcoded strings in TutorialFlow.ts
2. Default English locale bundle registered at runtime
3. Tutorial behavior unchanged (same 13 steps, same controls)
4. Tests verify i18n resolution and key coverage
5. Documentation covers editing and adding translations
…th card artwork

Root cause: DrawPileView called super() without passing countOffsetY, so it
defaulted to 60px in PileView. The card is CARD_H=130px tall, centered at
y + CARD_H/2, so the count text appeared at y + 125, only 5px from the card
bottom edge at y + 130, causing overlap.

Fix: Pass explicit countOffsetY: cardH/2 + 16 (=81px for CARD_H=130) to
position the count text 16px below the card bottom edge. Also export
DrawPileView for unit testing.

Tests: Added lost-cities-draw-pile-view.test.ts with 3 tests verifying:
- countOffsetY > CARD_H/2
- DrawPileView extends PileView
- Count text positioned correctly in empty state
Moved countOffsetY from CARD_H/2 + 16 (81px) to CARD_H/2 + 5.5 (70.5px),
reducing the downward offset by 50% (10.5px) as requested.
…verlay tests

The help panel and settings panel close() methods use a 300ms tween animation,
and removeInputBlocker() is called in the onComplete callback. The test was
waiting only waitFrames(10) (~166ms at 60fps), which is too short for the
animation to finish, causing a false assertion failure.

When the assertion fails, vitest pretty-format crashes with
"RangeError: Invalid string length" trying to format the massive Phaser Scene
object in the error context, causing entire suites to fail as crashed suites.

Fix: increased waitFrames after close() from 10 to 20 (~333ms) to ensure
the 300ms tween completes before checking inputBlocker is null.
…tion buttons in Lost Cities

Converts [ Next Round ] and [ New Match ] buttons from createOverlayButton
(text-only, green-on-transparent) to createActionButton (filled rectangle
background with stroke border and scaling hover effect), matching the
existing Menu button style.

Changes:
- Re-export createActionButton from LostCitiesAdapter
- showRoundSummary(): [ Next Round ] uses createActionButton with depth: 11
- showMatchSummary(): [ New Match ] uses createActionButton with depth: 11
- Updated browser test to find container wrapping action button for click
- Removed unused collectFromSceneAndHud helper

All 243 Lost Cities tests pass (6 browser + 237 unit/other).
…overlays

Replace single padded-string text objects with individually positioned
Phaser.GameObjects.Text objects per column in both showRoundSummary()
and showMatchSummary().

- Add COL_LABEL_X_OFFSET, COL_P0_X_OFFSET, COL_P1_X_OFFSET constants
- Header rows: separate 'Color'/'Round', 'You', 'AI' texts
- Data rows: separate label (left-aligned, originX=0) + scores
  (right-aligned, originX=1)
- Total/summary rows use same column layout
- Match-end breakdown section similarly refactored

Fixes misalignment caused by proportional font (Arial, sans-serif)
with string padding (padEnd/padStart).

Tests added: LostCitiesOverlayAlignment.browser.test.ts (3 new)
… helpers

- Create src/ui/ReducedMotion.ts with getEffectiveReducedMotion() utility
  that checks SettingsStore preference (primary) and CSS media query (fallback)
- Add tests/ui/ReducedMotion.test.ts with 23 tests covering:
  - getEffectiveReducedMotion() with settings panel preference, CSS media query,
    both set (precedence), and neither set
  - Placeholder tests for UI helpers accepting reducedMotion param
    (placeCard, dealCard, discardCard, flipCard, moveGameObject)
  - Placeholder tests for popTextOrIcon and sceneTransition SettingsStore checks
  - Type contract verification for all UI helper option types
- Export getEffectiveReducedMotion from src/ui/index.ts barrel file
…helpers

- Add reducedMotion?: boolean parameter to MoveGameObjectOptions, FlipCardOptions,
  PlaceCardOptions, DealCardOptions, DiscardCardOptions
- When reducedMotion=true: skip tweens, snap to final state, fire callbacks synchronously
- Update flipCard: instant texture swap + position when reducedMotion=true
- Update moveGameObject: snap target to destX/destY when reducedMotion=true
- Update placeCard, dealCard, discardCard: explicit param overrides CSS media query
- Update popTextOrIcon and sceneTransition: use getEffectiveReducedMotion() to check
  both SettingsStore preference and CSS media query (removed private prefersReducedMotion)
- All changes are backward-compatible (optional parameter)
- Create tests/golf/GolfAnimator.reducedMotion.test.ts with placeholder tests
  for reducedMotion-aware GolfAnimator (defines expected API contract)
- Type contract verification for GolfAnimator reduced motion property
- Add reducedMotion property to GolfAnimator (defaults to false)
- When reducedMotion=true: animateTurn skips all animations and calls onComplete
  immediately; showDrawnCard creates sprite at destination without tweens;
  animateDrawnCardToDiscard hides sprite and calls onComplete synchronously
- GolfScene reads this.settingsPanel.reducedMotion and passes to animator
- Add placeholder tests documenting the reduced motion contract
…LESCE4009A494: Implement reduced motion support in remaining games

- The Mind: MindAnimator.reducedMotion skips animateCardTowardsPile,
  showPenaltyCards, showLevelCompleteText; scene passes settingsPanel value
- Lost Cities: LostCitiesAnimator.reducedMotion skips phase1/phase2 animations;
  moveGameObject calls pass reducedMotion; scene passes settingsPanel value
- Beleaguered Castle: BeleagueredCastleRenderer.reducedMotion skips
  dealTableauAnimated and snapBack; scene passes settingsPanel value
- Feudalism: FeudalismAnimator.reducedMotion passed to all moveGameObject calls;
  scene passes settingsPanel value
- Add reducedMotion parameter to dealCard, placeCard, discardCard,
  flipCard, moveGameObject, popTextOrIcon, runSceneTransition option tables
- Update accessibility section to describe 3-tier priority (explicit param,
  SettingsStore, CSS media query)
- Document getEffectiveReducedMotion() utility
- Add missing flipCard and moveGameObject to animations overview
- Add new option tables for flipCard, moveGameObject, popTextOrIcon,
  runSceneTransition
Swapped the positions of 'Skip for Now' (now on the left) and
'Start Tutorial' (now on the right) in TutorialOfferModal.ts
to match the consistent convention used in all other tutorial
overlays (MainStreetTutorialHints.ts): left = dismiss/exit,
right = proceed/continue.

Also renamed variable names (startX/endX -> leftX/rightX) and
updated comments for clarity.

- Only TutorialOfferModal.ts modified
- Button text, colors, hover colors, and callbacks unchanged
…age compliance

- Updated all 13 tutorial step texts (T1-T13) in tutorial-en.ts for
  ~10-year-old reading level, under 50 words per body, 1-2 concepts per step,
  and plain-language principles (short sentences, common words, active voice)
- Externalized TutorialOfferModal.ts (title, body, button labels) to i18n
- Externalized MainStreetTutorialHints.ts overlay button labels to i18n
- Added modalKey() and overlayKey() helpers in tutorial-en.ts
- Updated docs/main-street/tutorial-localization.md with new keys and
  plain-language editorial guidelines
- All existing tests pass without modification
… skipping

- Update shouldShowTutorialOffer() to return false for 'skipped' status
- Update tests to expect false for skipped state
- Three tests updated: skipped-state offer, skip-action persistence,
  and legacy-bridge precedence
…urn, new cards

Implement health-category clinic rework:
- Add 'Health' synergy type to SynergyType union
- Add reputationPerTurn (optional) to BusinessCard/CommunitySpaceCard
- Add reputationBonus (optional, readonly) to UpgradeCard
- Add reputationBonus (mutable accumulator) to BusinessCard/CommunitySpaceCard
- Rework Clinic: Health synergy, baseIncome=0, reputationPerTurn=0.2
- Rework Medical Center upgrade: incomeBonus=0, reputationBonus=0.1
- New Private Clinic (cost 8, income 2, Health, upgradeable)
- New Private Medical Center upgrade (cost 4, incomeBonus 2)
- New Pharmacy (cost 6, income 1, Health, standalone)
- Update synergyColor function for Health (teal/cyan 0x1ABC9C)
- Add computeReputationPerTurn() to Adjacency module
- Apply reputation per turn during income phase in applyIncome()
- Accumulate reputationBonus in purchaseUpgrade
- Register all Health cards in Tier 4
- Update SVG generator for Health synergy color
- Regenerate card SVGs, manifests, and Monte Carlo baselines
- Update all test assertions for new template counts/decks
… is enabled

- Added syncTableauHandViews() call in dealTableauAnimated() reduced-motion
  branch, ensuring card sprites are created and interactivity is wired up
  before onDealComplete fires.
- Also fixed a test race condition in dev-server-cleanup.test.ts that was
  deleting the entire tmp/ directory in beforeEach/afterEach, which
  raced with parallel-running replay e2e tests.

The fix is a single-line addition to BeleagueredCastleRenderer.ts. The
reduced-motion branch previously skipped sprite setup entirely, leaving
the game board with no visible cards and no draggable interactivity.
…actory, decay logic, multiplier application, and barrel export

- Add ActiveEffect interface with effectType, multiplier, turnsRemaining, sourceEventId, description
- Implement createActiveEffect, decayActiveEffects, applyActiveEffectMultiplier, hasActiveEffectOfType
- Add DecayResult type for decay operation output
- Export ActiveEffect types and functions from core-engine barrel
- Write comprehensive unit tests (18 tests) covering creation, decay, multiplier composition, type checking, and full lifecycle
…guard, and unit tests

- Add DurationEventCard interface extending EventCard with duration, effectType, multiplier
- Add isDurationEventCard type guard function
- Update AnyCard union to include DurationEventCard
- Write 7 unit tests covering type discrimination, guard behavior with null/undefined, and field structure
- All 7 tests pass and TypeScript compilation succeeds
…serialization and migration

- Add activeEffects: ActiveEffect[] to MainStreetState interface (defaults to [])
- Add activeEffects to MainStreetSerializedState
- Add activeEffects to setupMainStreetGame initial state
- Add activeEffects to serializeMainStreetState and deserializeMainStreetState
- Add migration rule for missing activeEffects field (defaults to [])
- Import ActiveEffect type from core-engine
…r 4 unlock list

- Add evt-flu-outbreak DurationEventCard to EVENT_TEMPLATES in MainStreetCards.ts
- Add evt-flu-outbreak to TIER_4_NEW_CARD_IDS in MainStreetTiers.ts
- Generated SVG card art (65 SVGs generated)
- All tests pass (tier catalog coverage, card SVG coverage, card catalog baseline)
…tEngine

- Modify resolveEvent() to detect DurationEventCard and create ActiveEffect instead of one-shot deltas
- Add computeDurationWithClinicReduction() helper for Flu event duration reduction (Medical Center -3, Clinic -2, floor at 1)
- Record active-effect onset in activity log and transcript
- Add active-effect transcript event type to MainStreetTranscript
- Write 9 unit tests covering DurationEventCard resolution, clinic scanning, duration reduction, and regular EventCard unaffected path
- All tests pass
- Write 7 tests covering per-slot income multiplier, multiple effect composition, and turn-end decay
- Tests are expected to fail until F6 implements the income integration and decay logic
- Tests cover: single 0.8× effect, per-slot before-reputation application, no-active-effects baseline, two-effect composition (0.64×), decay at turn end, removal when expired, effect still applies for current turn before expiry
… and turn decay

- Modify applyIncome() in MainStreetAdjacency.ts to apply active effect income-multiplier per-slot before reputation multiplier
- Add decayActiveEffects call in processEndOfTurn() EndCheck phase
- Log effect expiration to activity log
- All 7 income-decay tests now pass
- All existing tests pass
…ent lifecycle

Integration tests:
- Full flu lifecycle: draw → income reduction → decay → expiration → normal
- Clinic reduces duration from 5 to 3 turns
- Medical Center reduces duration from 5 to 2 turns
- Activity log records flu onset and expiration

Save/load tests:
- Serialize/deserialize round-trip preserves activeEffects
- Missing activeEffects in old saves defaults to []

Monte Carlo:
- Basic sanity test verifying flu event doesn't break win rates
- All 7 tests pass
…iveEffect system

- docs/main-street/card-catalog.md: Added evt-flu-outbreak entry and duration event summary
- docs/main-street/core-rules-and-mechanics.md: Added ActiveEffect system section (Section 9) covering data structure, turn flow, and flu example
- docs/DEVELOPER.md: Added ActiveEffect system overview with API table and Main Street integration notes
…template

- Fix meta-progression test counts: 64→65 cards, 62→63 unique IDs
- Fix expanded-card-pool test counts: 17→18 templates, 51→54 deck cards
- Fix expanded-card-pool positive incident ratio: exclude neutral duration-based events from calculation
- Fix game-state test: EVENT_TEMPLATE_COUNT 17→18
- Update expanded-card-manifest.json with evt-flu-outbreak entry
Sorra the Orc added 28 commits June 30, 2026 16:36
Add MainStreetIntegration.test.ts with 12 integration test cases:
- Full game loop with purchase-to-hand, tableau placement, and income
- IncomePhase net income calculation (base + synergy - staff costs)
- Market cycling over multiple turns with mixed purchases
- Staff card lifecycle (purchase → ongoing cost → layoff)
- Layoff with fewer hand cards than slots to remove
- Save/load migration round-trip for all multi-use fields
- Old-format save compatibility (missing fields default to empty)
- Deterministic RNG preservation after migration round-trip
- Combined multi-system gameplay over 3 turns

All 1504 tests pass, 2 skipped (needing I3 selling feature).
- Create docs/main-street/prd-multi-use-card-economy.md with complete
  technical specification covering hand management, synergy, placement/
  sell, market cycling, staff cards, architecture, and test coverage
- Update the-build-gdd.md with Appendix D documenting all new mechanics
  and removed physical-game mechanics
- Update README.md to mention Multi-Use Card Economy features
Add MainStreetPlaceSell.test.ts with 26 test cases covering all 6
acceptance criteria:

AC1: Place from hand to tableau at 80% cost (coins deducted)
AC2: Sell from hand for 75% value (coins credited, card to discard)
AC3: Sell from tableau for 75% value (coins credited, slot emptied)
AC4: Insufficient coins blocks placement with error state
AC5: Sold cards accumulate in discard pile
AC6: Place/sell actions recorded in game transcript

Tests follow the TDD specification pattern from the existing hand
state tests (MainStreetHandState.test.ts):
- Use feature detection flags (HAND_FEATURE_AVAILABLE,
  PLACE_SELL_API_AVAILABLE, PLACE_SELL_LEGALITY_AVAILABLE,
  CONSTANTS_AVAILABLE) so they compile and skip gracefully
  before the implementation items (I1, I3) are complete.
- Access new state fields via (state as any) for type safety.
- Guard all implementation-dependent tests with it.runIf().
Add place/sell functionality to MainStreet:
- PLACE_COST_RATIO (0.8) and SELL_VALUE_RATIO (0.75) constants
- placeFromHand(state, handIndex, slotIndex) — places card from hand
  to tableau at 80% cost, validates slot and coins
- sellFromHand(state, handIndex) — sells card from hand for 75%
  value, credits coins, adds to discardPile
- sellFromTableau(state, slotIndex) — sells tableau card for 75%
  value, frees slot, adds to discardPile
- All operations log to activity log
- Fix place/sell test: add missing slot occupation before test

All 1528 tests pass, 4 skipped. Every child item of the
Multi-Use Card Economy epic is now completed!
…entered

Changes CASCADE_X from hardcoded 120 (far left) to GAME_W / 2 (640,
matching the horizontal layout's HAND_CENTER_X). This ensures the
vertical cascade stays anchored at the same central screen position
as the horizontal layout, preventing the hand from jumping to the
left when toggling layouts.

Added GymHandPileLayoutToggle.test.ts with 7 tests verifying:
- CASCADE_X is set to GAME_W / 2
- Vertical toggle uses setBaseX with CASCADE_X
- Horizontal toggle uses setCenterX with HAND_CENTER_X
- Layout direction is set correctly for both modes
- Spacing slider is synced to CASCADE_SPACING on toggle
…icon with event name, increase duration to 1800ms, font size to 20px

Changes:
- GymAudioFeedbackScene: showPopText now displays '"'♪ eventName'"' instead of '"'eventName: sfxKey'"'
  (hides internal sound key debug info, shows user-friendly music note + event)
- Duration increased from 400ms to 1800ms (normal) and from 100ms to 500ms (reduced motion)
- Font size increased from 14px to 20px for readability
- Added tests: popTextOrIcon longer duration test, GymAudioFeedback structural tests verifying
  music note, readable duration, and font size in showPopText
…FromCheckpoint

Bug: After resuming a checkpoint saved when it's the AI's turn,
restoreFromCheckpoint() unconditionally set the phase to 'player-turn',
allowing the human to interact (e.g., click reserved cards) even though
currentPlayerIndex pointed to the AI. This caused 'Card X not found in
market or reserved cards' errors.

Fix: Replace unconditional this.turnController.setPhase('player-turn')
with a check of session.players[session.currentPlayerIndex].isAI:
- If AI -> setPhase('ai-turn') + schedule executeAiTurn() with delay
- If human -> setPhase('player-turn') (unchanged behavior)

Also added ANIM_DURATION import from FeudalismConstants to use the
same timing (ANIM_DURATION + 200) as afterTurnComplete.

Tests: Added resume-turn-phase.test.ts with 13 tests verifying:
- Fresh game starts with currentPlayerIndex=0 (human turn)
- After human turn, checkpoint preserves AI as current player
- After AI turn, checkpoint preserves human as current player
- Multiple turn cycles correctly alternate player index
- Serialize/deserialize round-trips preserve currentPlayerIndex
- CheckpointManager round-trips preserve both turn states
…e, and zero income

Extends the existing neutral income sound (added in 5592cef) with dedicated
INCOME_POSITIVE and INCOME_NEGATIVE SFX keys, completing all three income states.

Changes:
- Add INCOME_POSITIVE ('sfx-income-positive') and INCOME_NEGATIVE ('sfx-income-negative')
  constants to SFX_KEYS in MainStreetConstants.ts
- Add ToneForge factory mappings for all three income sounds in sfx-tf-mapping.ts
- Update MainStreetAnimator to play INCOME_NEGATIVE for delta < 0 and keep
  INCOME_NEUTRAL for delta === 0 (positive delta already emits income-gained event)
- Change income-gained event mapping from COIN_POP to INCOME_POSITIVE in
  MainStreetLifecycleManager (dedicated positive income sound, not generic coin-pop)
- Add preload entries for INCOME_POSITIVE (coin-pop.wav) and INCOME_NEGATIVE
  (discard.wav) as WAV fallbacks, consistent with existing INCOME_NEUTRAL pattern
- Expand tests: verify all three SFX keys are defined, mapped to distinct tf keys,
  and route correctly by income sign (positive/negative/zero)
… Spike scene

The status line text was created with createHudText but the reference was
not stored, so the text never updated when tint/blend modes changed.

Changes:
- Store statusLineText reference as a class property
- Add updateStatusLine() method to refresh the displayed text
- Call updateStatusLine() from cycleTint(), cycleBlendMode(), and resetTint()
- Add browser integration tests for the shader spike scene

All 6 new tests pass, verifying:
- Initial status line shows correct defaults
- Status line updates on tint cycle, blend cycle, and reset tint
- Event log continues to work without regression
- Combined tint+blend state is reflected correctly
…tes in Gym HUD scene

Fixes two issues with panel status lines in GymHudComponentsScene:
1. SettingsPanel status line was not updating when panel toggled
2. HelpPanel status line was behind the open panel (x=60, panel covers 448px)

Changes:
- Store status line text object references as class properties
- Add updateStatusLines() method to refresh text on panel state changes
- Move HelpPanel status line from x=60 to x=460 (visible when panel is open)
- Call updateStatusLines() in all button handlers (Open, Close, Toggle)
- Add 8 new tests verifying status line initial display, updates on open/close/toggle for both panels, and HelpPanel visibility positioning
- Fixed action anchor Y position (0.175 → 0.27, pixelOverride 126 → 195)
  to prevent [ Toggle Fill ] overlapping the SLL Title Anchor.
- Set Shell overlay to be ON by default (shellVisible = true).
- Removed the Layout button and its associated logic.
- Made the Profile button cycle through layout options (changes visual
  layout of elements on screen, not just description text).
- Fixed overlay visualization: shows colour-coded dots at actual element
  positions (Title, Help, Action, Content) with correlated legend,
  instead of meaningless zone rect coordinates.
- Updated tests to match new behaviour.
…ects

Bugs fixed:
1. Replaced setPipeline('Light2D') (Phaser 3 API) with setLighting(true) (Phaser 4 API) so sprites are affected by lights
2. Stored light reference from addLight() for proper toggle/move control
3. Fixed toggleLight() to actually enable/disable the LightsManager instead of just calling enable() repeatedly
4. Fixed moveLight() to use .x/.y properties directly instead of non-existent setPosition() method
5. Updated log messages to accurately reflect actual lighting state
6. Added comprehensive browser tests covering Canvas fallback, WebGL operation, toggle, and move functionality
- Added missing 'buy-business-to-hand' case to scoreAction() in
  MainStreetAiStrategy.ts (return 0 as fallback score)
- Added missing 'staff' case to cardLabel() in MainStreetCards.ts
  (format: name + cost, matching upgrade pattern)
- Added missing 'buy-business-to-hand' case to buildRationale() in
  MainStreetHint.ts (return descriptive string)

These were the last 3 TypeScript errors blocking a clean 'npm run build'.
… updated Main Street layout

Updated all 7 non-null tutorial highlight zones in main-street-tutorial.layout.json
to match the current base layout and renderer positions:

- hud: narrowed from 100% to 50% screen width (centered)
- marketBusinessRow: extended width to match market bg box (to x=940)
- streetGrid: moved up to y=337px (from 433px) matching new streetTop
- endTurnButton: updated to match createActionButton position
- incidentQueue: moved from left side to right column (x=960)
- investmentsRow: extended width to match market bg box
- helpButton: updated to match hint button position (left of end turn)

Also updated test expectations and added layout sync warning comment.
…sue causing RangeError crash

Root cause: Tests 4-20 used advanceFrames(5) and advanceFrames(10) waits
(~83ms and ~167ms at 60fps) which are insufficient for the 300ms panel
open/close animations in HelpPanel and SettingsPanel. When assertions
on post-close input blocker state failed, Vitest's pretty-format tried
to serialize the deeply-nested Phaser object graph with maxDepth: Infinity,
causing a RangeError: Invalid string length.

Fix: Increased all advanceFrames calls from 5 or 10 to 20 frames (~333ms),
which exceeds the 300ms animation duration and ensures:
- Input blockers are removed before assertions
- Toggle animations complete before the next toggle
- Open animations complete before close operations

Changed file: tests/gym/GymHudComponentsScene.browser.test.ts
…id area instead of spanning full width

The streetGrid highlight zone for the T4 'Place a Business' tutorial step was
incorrectly spanning the full game width (w=1) and positioned too low on the
y-axis (y=0.468056).

Changes:
- Set y=0.444444 to match the base street anchor, including the grid label area
- Set w=0.73 so the highlight stops before the right sidebar column at x=0.75
- Updated expected zone bounds in test to compute values from proportional
  layout coordinates (320px y, 934px w at 1280x720)
- Updated DEVELOPER.md streetGrid zone description from 'full-width' to
  'stops before right column'

All 34 tutorial layout resolution tests pass.
…Migration browser test failures

Fixes:
1. GolfReplay.browser.test.ts: Updated stockSprite.visible assertion to match
   PileView behavior — when the pile is empty, the sprite remains visible but
   ghosted (reduced alpha), not hidden. Changed assertion to check visible=true
   and alpha <= 0.3 for both stock and discard sprites.

2. MindRenderer.disableGameInteraction: Added safety check for sprite.scene
   before calling disableInteractive(). When the game reaches game-over during
   AI-only play, some sprites may have been destroyed (e.g., during level
   transitions when renderHumanHand destroys old sprites and creates new ones).
   Without this check, disableInteractive() crashes with
   'TypeError: Cannot read properties of undefined (reading 'sys')' because
   Phaser sets scene=null on destroyed game objects.

These were pre-existing failures discovered-from CG-0MR2PHIEZ007TNDC.
…n tutorial T7

Root cause: processEndOfTurn() calls cycleMarketCards(), which discards all
scenario-placed market cards and refills from the random deck. By T7, the
explicit card placement from the TutorialScenario system is lost.

Changes:
- TutorialScenario.ts: Replace evt-grand-opening with evt-festival in
  STANDARD_TUTORIAL_SCENARIO investments row. Update coin budget comments.
- tutorial-en.ts: T7 body text now references Local Festival.
- TutorialFlow.ts: Update coin budget table and comments for  cost.
- MainStreetState.ts: Add skipMarketCycleOnEndTurn flag to prevent market
  cycling during the tutorial T6->T7 transition.
- MainStreetEngine.ts: Guard cycleMarketCards() behind the flag.
- MainStreetTurnController.ts: Set the flag when tutorial is active and
  current step is action 'end-turn' (T6). Reset after processing.
- Test files: Update E2E and unit tests to reference Local Festival ()
  instead of Grand Opening Sale ().
- Add CELEBRATE SFX key (sfx-challenge-complete) to MainStreetConstants
- Add sfx-challenge-complete mapping to sfx-tf-mapping.ts (success-fanfare)
- Update TurnResult to include newlyCompletedChallenges array
- processEndOfTurn now captures and returns newly completed challenge IDs
- Add animateCelebration() to MainStreetAnimator with particle burst,
  pop-text fallback, and reduced-motion support
- Wire celebration triggers in MainStreetTurnController.endTurn() with
  staggered timing for multiple challenges
- Preload celebration audio in MainStreetLifecycleManager
- Add tests (challenge-celebration.test.ts) for newlyCompletedChallenges
- Fix MainStreetOverlay.browser.test.ts mock to include new field

Files changed:
  example-games/main-street/MainStreetEngine.ts
  example-games/main-street/scenes/MainStreetAnimator.ts
  example-games/main-street/scenes/MainStreetConstants.ts
  example-games/main-street/scenes/MainStreetLifecycleManager.ts
  example-games/main-street/scenes/MainStreetTurnController.ts
  example-games/main-street/sfx-tf-mapping.ts
  tests/main-street/MainStreetOverlay.browser.test.ts
  tests/main-street/challenge-celebration.test.ts
…tent heights instead of fixed LOG_LINE_H

Root cause: logMaxScroll was computed as hiddenCount * LOG_LINE_H (18px),
but word-wrapped entries can be taller than 18px. This caused the scroll
max to be smaller than the actual content height, and entries at the
bottom of the scroll window extended past the mask boundary.

Fixes:
1. refreshLog(): Render all entries to compute logTotalContentH from
   actual text heights, then compute logMaxScroll = logTotalContentH -
   visibleH. Scroll is now applied by shifting the content container
   (setY) instead of re-rendering with a different startIdx.
2. applyLogScroll(): Updated to use the same container shift logic for
   consistency.
3. Added browser tests verifying scroll bounds use actual content
   heights, auto-scroll snaps to bottom, overflow entries are properly
   clamped on content height decrease, and word-wrap entries have
   larger scroll bounds than the fixed line-height estimate.
…eedyStrategy

Adds countVisibleRanks() and computeColumnBonus() functions that enable the
GreedyStrategy to consider visible instances of each card rank when evaluating
column-building moves. When all 4 copies of a target rank are visible, the
column bonus is reduced to zero (pursuit is futile). When fewer are visible,
the bonus scales proportionally.

- countVisibleRanks(): counts face-up grid cards + discard top, preserving
  the AI information boundary
- computeColumnBonus(): returns a negative score adjustment for swaps that
  build toward a column match (2 matching + 1 unknown), scaled by remaining
  unknown copies of the target rank
- chooseDrawSource(): enhanced to prefer discard when it helps build a column
  and unknown copies of the target rank remain
- chooseMoveForCard(): accepts optional visibleRanks parameter to apply
  column weighting in move selection
- 12 new tests covering: countVisibleRanks, computeColumnBonus
  (proportionality, all-visible edge case, discard-and-flip, non-matching),
  chooseMoveForCard integration, chooseDrawSource integration
- All 168 existing golf tests continue to pass
…ntent scrolling off the top

The initial logAutoScroll = true caused the first refreshLog() render to snap
to the bottom (logScrollOffset = logMaxScroll), pushing the first entries
above the mask. Changing to false shows the earliest entries first, and
auto-scroll kicks in naturally when the user scrolls to the bottom.
… test

Root cause: the 'all entries fit' branch in refreshLog() explicitly set
logAutoScroll=true. After entry count grew to overflow, this re-enabled
auto-scroll which snapped to bottom and pushed content above the mask.

Fix: remove s.logAutoScroll = true from the 'all entries fit' branch.
logAutoScroll is now only updated in the overflow branch via atBottom
computation, matching the original (pre-container-shift) behavior.

New test 'does not position content above the log display area when
entries first overflow' validates:
1. Initial render (entries fit) keeps logScrollOffset=0 and
   logAutoScroll=false
2. After entries overflow, logScrollOffset stays 0, container is NOT
   shifted up
3. All text objects have non-negative local Y (no entries above the
   container origin), proving content starts at the correct position
   below the title bar
…m mask graphics

Root cause analysis: In Phaser 4 RC7, GeometryMask.preRenderCanvas calls
graphics.renderCanvas() directly to draw the clip path onto the canvas
context. While GraphicsCanvasRenderer does not itself check the
property, there may be edge cases in the scene graph traversal where an
invisible graphics object is skipped during mask setup. The safest fix is
to keep the mask graphics visible but use a transparent fill (alpha=0),
so the clip path is reliably created by the mask pipeline while the
normal render pipeline draws nothing visible.

Changes:
- MainStreetRenderer.ts: Removed setVisible(false) on logMaskGraphics;
  use fillStyle(0xffffff, 0) (transparent) instead of opaque white
- MainStreetInputManager.ts: Updated updateLogMask() to use
  fillStyle(0xffffff, 0) for consistency
- activity-log-rendering.browser.test.ts: Added tests verifying mask
  is applied to content container, coordinates are correct at scroll
  position 0, and container shifts up when scrolled to bottom

All 10 activity-log-rendering browser tests pass.
… per-entry visibility

Root cause: Phaser 4 RC7 GeometryMask does not reliably clip content
outside the mask area, causing log entries shifted above the visible
window (via container.setY shift) to render over the title bar and
beyond the log panel.

Fix:
1. Per-entry visibility safety net in refreshLog() and applyLogScroll():
   After shifting the content container, iterate through all children
   and set visible=true only for those whose local Y falls within
   [scrollOffset, scrollOffset + visibleH). Entries outside this
   window have visible=false and are not rendered at all.

2. Fixed barBg (turn header background) rendering: use
   setPosition(0, yOff) instead of hardcoded fillRect(0, yOff, ...)
   so that Graphics.y correctly reflects the entry position for
   visibility checks.

3. Mask graphics: removed setVisible(false), use transparent fill
   (alpha=0) to avoid interfering with mask rendering.

4. Reverted logAutoScroll initialization to false (no auto-scroll
   on first render).

5. Removed spurious logAutoScroll=true from all-entries-fit branch.

12 browser tests pass.
@SorraTheOrc
SorraTheOrc merged commit cbf0e32 into main Jul 5, 2026
1 check failed
@SorraTheOrc
SorraTheOrc deleted the release/dev-to-main-20260705205307 branch July 5, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant