Commit Graph

5 Commits

Author SHA1 Message Date
claude e0c1e0d5c6 Stage 5: bosses that move, attacks that warn, and a second boss
ci / verify (push) Successful in 49s
Boss movement is a property of the PHASE, not of the boss -- a fight that
stands still and then starts hunting you is one boss with two phases. Four
modes (STATIC, ORBIT, CHASE, WAYPOINTS) handled generically in
SimWorld._move_boss, so a boss that moves is still data. BossDef.stationary
is gone rather than kept beside the phases: a flag claiming the boss stood
still while a phase walked around would be a second source of truth and the
wrong one, so moves() is derived.

CHASE holds a distance instead of closing, because a boss standing on top of
you is a boss whose bullets cannot be read. Waypoints are fractions of the
arena so one phase works in rooms of different sizes. Every mode is speed
clamped in one place -- ORBIT computes an absolute destination and would
otherwise snap onto its circle on the first tick -- and movement slides
against geometry so a boss cannot walk through the pillars its own arena was
designed around.

The room clamp moved to after movement, where it is finally load-bearing. It
was a no-op while every boss stood still, which is exactly when an invariant
is cheapest to establish: boss rooms deliberately do not lock, so walking out
is always an escape, and that only holds if the boss cannot follow.

TelegraphedStrikeEmitter marks spots and fills them a moment later. The moment
between is the feature: a burst at your feet is a coin flip, the same burst
with a second of notice is a question. It stays stateless like every other
emitter -- they are shared resources and two bosses of the same kind must not
stomp each other -- so strike positions are derived from the volley number and
a test asserts the burst lands where the marker promised. Markers are drawn
through fog and through walls, unlike everything else in the view, because a
warning you cannot see is an unavoidable hit with extra steps.

The Cantor of the Vault fights in the choir vault: static, then a four-corner
circuit, then a chase, then orbiting while marking. It exists to prove the
format stretched, and a test asserts it uses both new mechanisms.

Which boss a run has now comes from its SEED rather than its depth. Depth is a
dev flag nothing in play raises, so the arena was keyed to something no player
can change and the second boss was unreachable in an actual game.

Two things found while finishing:

  - tools/export_content.gd had a hand-maintained boss list and had already
    gone stale, silently not writing the Cantor. Content.ALL_ENEMIES and
    ALL_BOSSES now feed the export tool, the renderer and five tests that each
    kept their own copy.
  - diag_loot failed intermittently after another diagnostic. Taking over from
    the bot cleared its input queue but not its HELD input, so a starved server
    coasted on the bot's last movement vector for half a second and walked the
    player off the item it had been placed on. The press arrived correctly,
    which is why "the press reached the simulation" passed while everything it
    should have caused failed.

check.sh clean, 409 tests, SMOKE PASS (19 assertions), all four diagnostics
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:32:12 +02:00
claude ff5e527ad4 Fix black wall tops, pillar art, player anchor, and whole-sheet bullets
ci / verify (push) Successful in 46s
Bullets rendered as the entire sprite sheet, which was the clue that named the
cause: a MultiMeshInstance2D cannot show a sub-region of a texture. Its mesh UVs
run 0..1 across whatever texture it is handed, and an AtlasTexture does not
help -- the draw resolves to the underlying atlas and the region is discarded.
Frames are now pre-sliced into 32 standalone 16x16 textures at startup, so each
frame is one texture assignment per kind and no region is involved. My previous
two attempts fixed the atlas contents while the renderer was structurally
incapable of showing one cell of it.

Black voids above walls: wall_top_mid has 12 of its 16 rows transparent. It is a
cap meant to be laid over a wall, not a tile, and drawn alone it was three
quarters nothing. Floor is now drawn beneath every tile so transparency shows
ground rather than clear colour, the wall face is drawn from the opaque
wall_mid, and the cap is overlaid only where open floor lies above.

Pillars used a wall segment and read as stray blocks; they now use the column
tile, which looks like something you walk around.

The player hitbox appeared at the head because it was: the knight occupies rows
8..27 of a 28-row cell, so anchoring on the cell centre put the draw origin
about 4px above the body's centre. Added a per-sprite anchor offset that cancels
it, with a test that recomputes the offset from the art so a re-cut sheet fails
rather than silently misplacing the character.

Tests pin the two tileset facts that caused this: the cap is mostly transparent
(so it may never be drawn alone) and the wall face is fully opaque (so it may).

166 tests. check.sh, test.sh and smoke.sh pass.
2026-09-04 00:22:01 +02:00
claude afe76c22ce Fix bullet atlas, animation speed, sprite flip, fire rate, silent shots
ci / verify (push) Successful in 46s
Bullet sprites were wrong because I read the pack's layout backwards. Each of
its 8 PNGs is one ANIMATION FRAME, and a column within a file is a COLOUR
variant -- so walking columns cycled the palette while the shape sat still, and
the rows I picked were in a region of larger multi-cell sprites, which is the
scattered debris that showed on screen. tools/build_local_assets.py now composes
a proper atlas: 8 frames across, one row per SimConfig.KIND_*.

Animations ran at whatever the machine's framerate was, because they counted
_process calls. At 240fps that is four times too fast on its own, before the
requested halving. Both actor and bullet animation are now driven by elapsed
seconds, so they look the same on any machine.

The knight shifted sideways instead of mirroring: a Rect2 with negative width
does not flip, the draw call normalises it, so the sprite kept its orientation
and jumped right by its own width. Mirrored through the canvas transform about
the sprite's centre instead.

Shots were silent when a bullet spawned inside an enemy. The sound rode on
BULLET_SPAWN, which is deliberately suppressed for a bullet resolved and removed
within the same tick -- so no event, no sound, and this got more likely the
closer you stood. Firing is now its own event (PLAYER_FIRED): the shot happened
whether or not a bullet survived to be replicated. Protocol 3 -> 4, because
inserting mid-enum shifts the wire value of every event after it.

Fire cooldown 7 -> 14 ticks (4.3 shots/sec).

Licence question 2, confirmed verbatim: the paid tier DOES grant commercial use
("You may use these assets in personal, commercial or non-commercial
projects"), but its next sentence still forbids redistribution "no matter how
much you modify it". Paying moves these from local-only-and-non-commercial to
local-only-and-commercial; it never makes them committable. Only replacing them
with permissively licensed art does that.

Question 3: CREDITS.md now credits every pack, including the two whose licences
do not require it and the unused ones in considering_dont_use_yet/.

158 tests. check.sh, test.sh and smoke.sh pass.
2026-09-04 00:03:04 +02:00
claude 3586555aea Remove non-redistributable art from the repo; record licence terms
Researched the licence at each of the four itch.io sources. Two of the four
forbid redistribution, and publishing a repository containing them is exactly
that, so they are out of the tree.

  0x72 DungeonTileset II    CC0 1.0    commercial ok, no credit required
  Helton Yan Pixel Combat   CC BY 4.0  commercial ok, CREDIT REQUIRED
  Fire Pixel Bullet 16x16   custom     non-commercial, NO REDISTRIBUTION
  750+ Effect and FX Pixel  custom     non-commercial, NO REDISTRIBUTION

Both bdragon1727 packs state verbatim: "You cannot do: Resell / redistribute
this asset." The FX pack's paid tier is no better for this purpose -- "You can
NOT re-distribute the file, no matter how much you modify it" -- so paying does
not unlock it and neither does editing the art.

bullets.png and impact.png therefore move to assets/local/, gitignored.
Art.bullets_texture() / impact_texture() load them at runtime and return null
when absent; the bullet renderer falls back to the generated dot it used before,
tinted per kind so bullet types stay distinguishable. Nothing there may be
preload()ed -- preload resolves at compile time and would fail the build on
every machine without the files, which is every clone.

Verified both ways rather than assumed: with the files present the game uses
them and no fallback triggers; with them moved aside, 156/156 tests pass and the
game runs clean on the generated dots.

Added CREDITS.md, because Helton Yan's CC BY 4.0 makes attribution a licence
obligation rather than a courtesy. A file in the source tree does not discharge
it once the game ships -- an in-game credits screen is now a tracked todo, and
flagged as a licence requirement rather than polish.

NOT addressed here, and it needs a decision: these files remain in git history
at 9fa2e26. Removing them from the tip does not remove them from a clone. If
this repo is ever made public, history has to be rewritten (git filter-repo)
and force-pushed. Also, both bdragon packs are non-commercial in their free
tier, which conflicts with the stated Steam goal -- either contribute for the
commercial tier (still no redistribution) or replace with CC0 art, which would
solve both problems and let the assets back into the repo.
2026-09-03 23:52:41 +02:00
claude 9fa2e260f2 Replace placeholder rendering with real sprites and audio
Terrain, actors, boss and bullets are sprites now, and four sounds play off
server events. 548KB total across seven files, out of 3.9GB of source packs.

- src/view/art.gd holds every atlas rect and sound path in one table, copied
  from the 0x72 pack's own tile_list. Scattering coordinates through draw calls
  would make re-cutting the atlas a hunt, and a wrong rect invisible.
- Bullets get one MultiMesh per kind rather than one overall: a
  MultiMeshInstance2D carries a single texture and each kind needs a different
  region of the sheet. Four draw calls, no custom shader passing UVs through
  per-instance data.
- src/view/sfx.gd is a 16-voice round-robin pool. Bullet-hell fire rates mean
  sounds overlap constantly, and identical sounds landing within two frames are
  collapsed so a ring hitting eight bullets is a bang rather than clipping.
  Every sound is triggered by a server event, never a local guess, so what you
  hear matches what happened.

Audio was converted, not copied: the pack ships 24-bit/96kHz masters averaging
2MB. Downsampled to 16-bit/44.1kHz mono, silence-trimmed, and the shoot sound
hard-capped to 0.30s -- it came out at 2.03s, against a fire cooldown of 0.12s,
which would have smeared held fire into noise.

tests/unit/test_art.gd checks what cannot be eyeballed here: every atlas rect
lands inside its texture, every animation frame of a strip fits (the last frame
is what runs off the sheet, not the first), there is a sprite per bullet kind
and per enemy visual, and no sound is long enough to stack badly. A wrong atlas
coordinate does not error -- it silently draws the wrong pixels.

Licence sources recorded in docs/ASSETS.md now that they are known. All four
are free versions and four rows still say "confirm at source": free itch packs
vary on credit and commercial use, and there is no credits screen yet.

155 tests (was 146). check.sh, test.sh and smoke.sh pass.
2026-09-03 23:43:13 +02:00