Skip to content

fix: playtest pass 2 — bosses, brawl polish, sprite alpha, economy rebalance - #47

Merged
prabu-openclaw merged 5 commits into
mainfrom
fix/playtest-pass-2
Aug 1, 2026
Merged

fix: playtest pass 2 — bosses, brawl polish, sprite alpha, economy rebalance#47
prabu-openclaw merged 5 commits into
mainfrom
fix/playtest-pass-2

Conversation

@prabu-openclaw

Copy link
Copy Markdown
Collaborator

Second device-playtest pass (2026-07-25). Two commits: boss/flow fixes, then art + balance.

Run blockers

M3.5 was unstartable. The briefing card renders ROTATED (the scene fakes landscape while the device is portrait), so its height is bounded by the screen's width — ~402pt. The rules list is variable: up to four gear-gated loadout lines, plus BEST RUN, plus CORP HEAT. With no scroll, a geared-up player pushed RIDE clean off the edge. Reproduced in the simulator: on a fresh save RIDE sat at x≈51, barely on screen. Body scrolls now; RIDE is pinned outside the scroll so a longer list can never take the only way in.

Bosses

Not a missing-AI problem — all four archetypes have aggressive, previously-tuned behaviour. A boss is one unit against a squad of four, so the party takes four actions to its one: it spent the fight walking and died on the approach. Boss archetypes now get a second activation per enemy phase, which is what lets them close and fire in the same round.

The M5 mech had two more problems on top: move budget 2 while its autocannon only fired inside range 6 — and it spawns at (3,8) with the party entering near the bottom, so on the turn it appeared it could not shoot at all. Move 2→3, reach 6→8. Its stats were never the issue: deployBoss correctly builds bossMech (80 HP, armour 7), not the 34 HP rank-and-file combatMech.

The boss splash was being raised correctly the whole time. BossIntroOverlay dismissed on ANY tap with no grace period, and the reveal fires the instant the killing blow lands — so an inherited or eager follow-up tap killed the card inside a frame while it was still fading up over 0.9s. Taps now ignored for 1.2s. Both reveal paths also present the card BEFORE appending the enemy, so it is already rising as the sprite materialises behind it.

Art

81 of 444 frames had an opaque white background baked in instead of alpha — the M4.5 opponents (exactly half of each fighter's frames), every chase_* prop, every shopicon_*. Removed by edge-connected flood fill so interior detail survives, then a second pass cleared white pockets enclosed by the figure (the gap between razorgirl's arm and hip) in fighter frames only — shop icons keep theirs, where white is design. Raze's own frames were already correct.

The Cyber-Zombie is the juggernaut, and its art fills only 86% of its canvas, so at the default target height it rendered a ~60pt figure — the smallest enemy on the board (guard 67, bruiser 70, riot 68, rigger 76). Given its own entry at 99 → ~85pt.

Feel

  • Brawl attacks were a pure pose swap with no displacement, and the active beat is only 0.10–0.12s, so they read as a flicker. Fighters now coil back, drive through, and settle, with tilt scaled to weight. Hit windows untouched.
  • The VS card showed for 0.3s (first round) / 0.7s after. Now parks on a pulsing TAP TO FIGHT with a full-screen catcher and a 12s backstop.
  • Door lock state was computed once per room and painted on every door, but attemptTransition waives the cleared-room rule for backtracking — so the door you'd just walked through rendered locked while being freely walkable. Now per-door.
  • M4's mini-game already ran three escalating levels; they were visually identical, so clearing one read as the same board again. Each tier now has a name (OUTER SHELL / CIPHER LAYER / BLACK ICE CORE), colour, and callout.

Economy

Rank (to 1.6×) and heat (to 1.5×) stacked multiplicatively at 2.40× — that is how a ¥34,000 contract paid ¥94,800. Measured against the catalogue: one of every item is ¥111,000 and kitting all four runners is ~¥444,000, while a strong campaign banked ~¥570,000 — enough to buy everything for everyone.

Rank and risk now stack additively (best case 1.60×, both incentives intact) and base payouts are trimmed ~25%. A perfect run lands near ¥245,000 — about half the full-kit cost, so gear stays a real choice.

Tests: 126 → 135

  • BossBehaviourTests — drives runEnemyAI directly; asserts every boss acts, the mech threatens from its real spawn tile, only boss archetypes double-activate, and Sato's reveal raises the splash.
  • Payout-ceiling pair that fails if a campaign can ever fund the whole catalogue for the team.
  • Backtrack-spawn (M1 + M5) and backtrack-door-lock regressions.
  • Three existing economy tests rewritten to derive from basePayout/dataBonus instead of hardcoded figures.

Not device-verified yet. Two items still open and not in this PR: the M3 grimoire tile flash (already implemented — owner re-checking on a current build) and M5 SFX levels (measured; no per-mission volume exists in code and M5 is rifle-heavy so by asset levels it should be louder — needs to know which sounds felt quiet).

🤖 Generated with Claude Code

prabu-openclaw and others added 5 commits July 31, 2026 09:36
Playtest 2026-07-25.

M3.5 was unstartable. The briefing card renders ROTATED (the scene fakes
landscape while the device is portrait), so its height is bounded by the
screen's WIDTH — ~402pt. The rules list is variable: up to four gear-gated
loadout lines, plus BEST RUN, plus CORP HEAT. With no scroll, a geared-up
player pushed the RIDE button clean off the edge and could not enter the
mission. Reproduced in the simulator: on a FRESH save RIDE sat at x≈51,
barely on screen. The body now scrolls and RIDE is pinned outside the
scroll view, so a longer list can never take the only way in.

Bosses were pushovers. Not for want of AI — all four archetypes have
aggressive, previously-tuned behaviour. A boss is one unit against a squad
of four, so the party takes four actions to its one: it spent the fight
walking and died on the approach. Boss archetypes now get a SECOND
activation per enemy phase, which is what lets them close AND fire in the
same round. Still one unit; the party keeps the edge.

The M5 mech had two more problems on top. Its move budget was 2 while its
autocannon would only fire inside range 6 — and it spawns at (3,8) with the
party entering near the bottom, so on the turn it appeared it could not
shoot at all, and never caught up. Move 2 -> 3 and reach 6 -> 8. Its stats
were never the issue: deployBoss correctly builds bossMech (80 HP, armour
7), not the 34 HP rank-and-file combatMech.

The boss splash was being raised correctly the whole time — BossIntroOverlay
just dismissed on ANY tap with no grace period, and the reveal fires the
instant the killing blow lands, so an inherited or eager follow-up tap
killed the card inside a frame while it was still fading up over 0.9s. Taps
are now ignored for 1.2s. Both reveal paths also present the card BEFORE
appending the enemy, so it is already rising as the sprite materialises
behind it — splash first, then the boss, as intended. Covers M3, M5 and M6.

Tests: 126 -> 132. New tests/BossBehaviourTests.swift drives runEnemyAI
directly and asserts a boss actually acts, that the mech threatens from its
real spawn tile, that only boss archetypes get the extra activation, and
that Sato's reveal raises the splash. Also adds a backtrack-spawn regression
test covering M1 and M5 door-mirrored re-entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Playtest 2026-07-25, second batch.

81 of 444 sprite frames had an OPAQUE WHITE BACKGROUND baked in instead of
alpha — the M4.5 opponents (bodyguard/brawler/razorgirl/slugger, exactly
half of each one's frames), every chase_* prop, and every shopicon_*.
Removed by edge-connected flood fill so interior detail survives, then a
second pass cleared white pockets ENCLOSED by the figure (the gap between
razorgirl's arm and hip, bodyguard's arm and torso) in the fighter frames
only — shop icons keep theirs, where white is design. Verified on a dark
composite. Raze's own frames were already correct; the white was his
opponents.

The Cyber-Zombie is the juggernaut, and its art fills only 86% of its
canvas, so at the default target height it rendered a ~60pt figure — the
SMALLEST enemy on the board (guard 67, bruiser 70, riot 68, rigger 76) for
"a walking wall of bolted chrome". Given its own entry at 99, landing ~85pt.

Brawl attacks were a pure pose swap with no displacement, and the active
beat is only 0.10-0.12s, so they read as a flicker rather than a strike.
Fighters now coil back on wind-up, drive through on the hit and settle on
recovery, with body tilt scaled to weight. Hit windows are untouched.

The VS card was on screen for 0.3s on the first round and 0.7s after — not
long enough to read the challenger or the patch-up. It now parks on a
pulsing TAP TO FIGHT with a full-screen catcher, and a 12s backstop so a
missed tap can't strand the brawl.

Door lock state was computed ONCE per room and painted onto every door, but
attemptTransition waives the cleared-room requirement for backtracking — so
the door the player had just walked through rendered locked while being
freely walkable. Lock state is now per-door.

M4's mini-game already ran three escalating levels; they were just visually
identical, so clearing one and starting the next read as the same board
again. Each tier now has a name (OUTER SHELL / CIPHER LAYER / BLACK ICE
CORE), an accent colour, and a callout on advance.

Economy. Rank (to 1.6x) and heat (to 1.5x) stacked MULTIPLICATIVELY at
2.40x, which is how a 34,000 contract paid 94,800. Measured against the
catalogue: one of every item is 111,000 and kitting all four runners is
~444,000, while a strong campaign banked ~570,000 — enough to buy
everything for everyone. Rank and risk now stack additively (best case
1.60x) and base payouts are trimmed ~25%, landing a perfect run near
245,000: about half the full-kit cost, so gear stays a choice.

Tests: 133 -> 135. Adds a payout-ceiling pair that fails if a campaign can
ever fund the whole catalogue for the team, a backtrack-door lock test, and
rewrites three economy tests to derive from basePayout/dataBonus rather than
hardcoded figures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…te the mission unlock

## Drift gate (CI-blocking)

Every PR was failing `xcodegen drift gate` with the ENTIRE pbxproj rewritten
— 3,704 insertions, 3,704 deletions — while `xcodegen generate` locally
produced no diff at all.

XcodeGen derives its object IDs from the containing directory name. The
GitHub repo was renamed ShadowrunGame -> Hexwire, so CI now checks out to
`/Users/runner/work/Hexwire/Hexwire` while the local clone still sat at
`~/ShadowrunGame`. Same content, different path, every ID different.
Reproduced by copying the tree into a directory named `Hexwire` and
regenerating: 7,879 diff lines, matching CI exactly.

Local clone renamed to `~/Hexwire` and the project regenerated under that
path, so generated IDs now match what CI produces. This was blocking every
future PR, not just this one.

## Warnings: 30 -> 0

- `fireOverwatchShot` is called for its side effects at 18 sites and its
  count is never used; marked @discardableResult. Those 18 identical
  warnings were burying the real ones.
- MusicManager's fade timer touches @mainactor state from a @sendable
  closure (4 warnings). `scheduledTimer` installs on the run loop it was
  created from and `fade` is only reachable from the @mainactor class, so
  it IS on main — asserted with `MainActor.assumeIsolated` rather than
  hopping, since a hop would land the fade tick a frame late and stutter
  the ramp.
- SFXManager read the singleton's `targetVolume` from a @sendable
  notification closure to scale the SMG clip. Moved that resolution inside
  the existing main-actor hop, where the value is meaningful anyway.
- Two deprecated `onChange(of:perform:)` call sites left as-is; they still
  behave correctly and the migration is not mechanical.

## Ship blocker: devUnlockAllMissions

Was a plain `let devUnlockAllMissions = true` that a human had to remember
to flip before cutting a release — a standing WP10 blocker, and the source
of a "will never be executed" warning. Now `#if DEBUG` around both the
constant and its use, so Release compiles the bypass out entirely and a
release candidate CANNOT ship with progression disabled. Debug builds stay
unlocked for playtesting, so this changes nothing for the current pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`updateTurnIndicator()` was never called, and the `turnOrder` array it read
was never populated — its own `!turnOrder.isEmpty` guard would have returned
early even if something had called it. `currentTurnIndex` existed solely to
index that empty array. The live `.turnChanged` notification is posted by
CombatFlowController, so nothing observable changes.

Found by scanning for functions with zero production call sites. Worth
removing rather than leaving: vestigial scaffolding is exactly what let the
extraction blocker hide — `syntheticExtraction` was equally dead but had
passing tests, so side contracts looked covered while being uncompletable.

Related, NOT changed here: the `TurnManager` class is never instantiated
either (only its static `requestTurnAdvance` forwarder is used), so its
initiative machinery — turnOrder, rollAllInitiative, advanceTurn,
currentActor, upcomingActors — is dead too. That file also holds the very
live `Enemy` type and every enemy factory, so untangling it is a separate
change worth doing deliberately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The extraction fix earlier in this branch added a private
`firstExtractionTile` to MissionSetupService without noticing that
`GameState.firstExtractionTile` already existed — whose own comment reads
"Moved from BattleScene so room-entry authority owns the lookup", i.e. this
had already been consolidated once and I re-forked it.

Both scanned top-left to bottom-right and returned the first match, so they
agreed and nothing was broken. Removing it anyway: two implementations of
"where is the extraction tile" is precisely how the backtrack-spawn bug
happened — RoomManager and BattleScene each had their own answer, they
disagreed, and the squad landed at the wrong end of the room. Mission setup
and room entry now resolve the pad through the same function.

StepOnSemantics / ReplayModeCertification / ContractBoard all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@prabu-openclaw
prabu-openclaw merged commit b835818 into main Aug 1, 2026
7 checks passed
@prabu-openclaw
prabu-openclaw deleted the fix/playtest-pass-2 branch August 1, 2026 02:27
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