The palette constraint is not uniform across assets, so a single generic
line was not enough:
- Portraits list only the 11 non-sky colours, since a face never shows sky;
omitting the sky slots is safer than asking a generator to avoid them.
Adds the flat #492424 backdrop requirement (HMMM blits have no
transparency, so whatever surrounds the head gets copied literally).
- The background block is split by region: four sky colours inside the
window opening, eleven interior colours everywhere else, with the frame
and mullions counted as interior wood. Also reserves the top 16 and
bottom 64 rows the game draws over.
- Sprites take no palette at all — they are hardware sprites coloured at
runtime, so the art only defines shape.
Notes that the available clothing colours are few enough that characters
need distinct ones or they blur together in the dialogue box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves decision 8: art is generated in PixelLab from written prompts.
docs/ART.md carries the palette spec, the sky/wood slot rule, per-asset
sizes and return format, and prompts for Aki (needed for M3), the rest of
the cast, and the shop background.
art/palette.png (16x1, one pixel per entry) and art/palette_ref.png
(512x64 swatch strip) are generated from g_Palette so the generator and
the build cannot drift apart.
Flags a constraint the palette had hidden: only one literal skin entry, so
portraits use 6 -> 14 -> 4 as a hue-compatible skin ramp.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records slot count, naming, presentation, delete guard and the day-length
dial as decision-queue items 16-20 marked "defer": captured so the
reasoning survives, but explicitly not decided now. They only bite at M10
and are all easier to judge against a real slot screen than in the
abstract. Adds a "defer" convention to the queue preamble.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tool/disk_save already indexes every call by entry, so multiple save files
cost almost nothing on the storage side — KISSAT00.SAV, KISSAT01.SAV, and
DiskSave_Check/Delete/GetFreeEntries already exist per entry. The work is
the boot-time slot screen, which is why it sits late: it is easier to
design once there is real save content to display.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the hand-rolled PHYDIO/DSKIO sector writer, which never worked,
with engine/src/tool/disk_save.h — added in MSXgl v1.3.0 for exactly this
("save to disk from a ROM application"). Saves are now a real FAT file,
KISSAT00.SAV, on the disk in drive A.
Four non-obvious requirements, all documented in CLAUDE.md:
- LibModules needs both "tool/disk_save" and "dos"
- ROMDelayBoot = true, or the Disk ROM's INIT never runs
- boot then takes 40-90s of emulated time (use `set throttle off`)
- DiskSave_Check() returns SAVEDATA_UNSIGNED for good files: with APPSIGN
it wants the first 4 bytes to be g_AppSignature, but DiskSave_Save()
writes the payload raw and never adds it
Also fixes a save-timing bug: the write now happens in AdvanceDay() after
the day increments, so the file describes the morning the player wakes to.
Saving during the evening made a reload replay that day and double-count
its cups.
Verified: play a full day, cold boot, resume at day 2 with served intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 1 (shop scene, customer, siphon brew minigame, serve loop) and the
M2 day spine: day counter, three-phase day, event-driven clock, journal
line, and a SaveState with magic/version/checksum.
Saving to disk is NOT working yet. A cartridge ROM can reach the Disk ROM
only from RAM (both PHYDIO and CALSLT switch page 1 out from under the
caller), and ROMDelayBoot is required so the Disk ROM's INIT runs at all.
With both in place the disk is detected, but sector I/O returns carry-clear
without transferring — proven with a sentinel. Probe now verifies the
buffer actually changed, so a non-working disk degrades to "no save"
rather than corrupting anything. Full findings in the project CLAUDE.md.
Also documented: keyboard reads need interrupt protection (the BIOS ISR
scans the matrix too), and BankedCall must stay off until banked data
exists.
Docs: PLAN.md (ten milestones, per-stage requirements, decision queue),
SCRIPT.md (all four character arcs, 60 beats, checked against the
26-character box), and amendment notes in DESIGN.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Game flow now lists all movement-loop keys (M/joy-A minimap, S cat
sense, C cheat landing one move short of the exit), and the MSXgl
configuration section covers the full audio stack including ayFX.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sound effects (ayFX on the PSG, mixed over the FM music):
- 8-effect bank from sfx/sfx_bank.afb embedded as sfx_bank.h in fixed
main ROM (interrupt-readable without bank switching). Triggers: random
cat-paw footstep (0-2) per move, fanfare (4) on reaching the exit, map
unfold (5) on M, laugh (6) on C, ta-daa (7) when advancing the title/
hint/win screens. Priorities keep footsteps below UI/finish sounds.
- The H_TIMI hook now also runs ayFX_Update + PSG_Apply each frame;
PSG switched to indirect (buffered) access with AYFX_BUFFER_DEFAULT.
- Interrupt-safety fixes this surfaced: all joystick reads go through
ReadJoy() (DI/EI around Joystick_Read, which drives the PSG and raced
the ISR's PSG_Apply — caused noise, phantom input, minimap-on-move),
and the PSG mixer is muted at startup (zeroed buffer = all channels
enabled active-low, which PSG_Apply drove as noise during the logo).
Torus scrolling fix (bug from 7c24daa, exposed by mixed-direction play):
- ScrollDraw's bulk strip fills only honoured the scroll-axis base, so
with a non-zero orthogonal base the newly revealed edge was drawn
rotated by 2/4/6 tiles — phantom walls/passages while collision stayed
correct. Vertical strips are now pre-rotated by g_BaseX in staging;
horizontal column fills split at the 256-row wrap via FillStripColumns.
CLAUDE.md updated for both (SFX table + ISR rules, both-axis fill rule).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instead of teleporting straight onto the exit cell (instant win), the C
cheat now places the player on an open neighbour of the exit, leaving one
valid move onto it. The exit is a normal spanning-tree cell, so at least
one of its up/left/right neighbours has an open passage; pick the first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three follow-up fixes to the per-screen music:
- Decode music in the H_TIMI VBlank interrupt (MusicVBlank) instead of
once per Halt() in the game loops. The heavy per-step VDP work in
ScrollDraw could overrun a VBlank, dropping the decode rate so the
music slowed only while moving; the ISR keeps tempo steady regardless.
The playing track is mapped into the unused bank-3 window (0xA000) and
left there for the screen, so the ISR reads it with no bank switching
and never races the main code's bank-2 tile/sprite swaps. PlayTrack()
switches tracks under DI/EI. The game loops revert to plain Halt()s.
- PlayTrack() mutes the OPLL (key-off all 9 channels) before starting the
next track, silencing the note that lingered across screen changes.
- TitleScreen()/ShowHints() zero the hardware scroll offset before drawing
their absolute-coord images. Gameplay rests at g_BaseX/g_BaseY in
{0,64,128,192}; without the reset the returning title/hints screen
appeared horizontally shifted (only after finishing on a non-zero base).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each screen now plays its own looping MSX-Music (YM2413/OPLL) track:
title, hints, gameplay, and victory. Tracks are lVGM files embedded in
banked segments 26-29 (one per segment, each < 8KB so it never crosses a
boundary); segments 30-31 remain free.
- project_config.js: add psg/msx-music/vgm/lvgm_player modules; set
EmulMSXMusic so ./build.sh run gives openMSX the FM-PAC (-ext fmpac).
- msxgl_config.h: enable LVGM_USE_PSG/MSXMUSIC (rest off), PSG_DIRECT.
- mazegame.c: MSXMusic_Initialize() once at boot; PlayTrack()/PlayFrame()
bracket every player call with a bank-2 save/restore so music never
disturbs the tile/sprite banking; a PlayFrame() follows every Halt() in
the wait/animation loops (ScrollDraw, input loops now frame-synced,
WaitKeySequence) to advance each track at 60 Hz. Logo stays silent.
- CLAUDE.md: document the music pipeline, banking protocol, and segments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Movement previously ended each step by blanking the display, resetting the
scroll offset to 0, bulk-HMMM-shifting the whole viewport back to a canonical
origin, and pasting the new strip. That blank was visible as a flash on every
move.
Treat the 256x256 VRAM page as a torus instead. New g_BaseX/g_BaseY hold the
resting hardware scroll offset (a multiple of 64); a move animates the scroll
register from base to base +/- 64 and fills the newly exposed edge into the
VRAM cells that scroll off the opposite side, then keeps the new offset as the
resting base. No offset reset, no bulk shift, no strip paste, no blank -- so
no flash. All fill targets are base-relative; at base 0 they reduce exactly to
the previous code, and the fill timing is base-invariant.
Route all gameplay VRAM drawing (tiles, sprite, compositors, cat-sense arrow)
through the base via a new WriteVV helper that splits writes at the 256-column
wrap. The minimap overlay draws at absolute coords, so it zeroes the offset
while shown and restores the base on exit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The leg sat flush against the middle bar's right end, looking tacked on.
Move it left so both bars overhang it on the right and it crosses the
middle bar like a real glyph stroke. The leg/mid-bar crossing gets its
own palette entry (pal8), lit during both the mid bar's and the leg's
fade so neither shows a gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The uniform-thickness pass had dropped the bars' left overhangs, turning
the logo into a pinwheel (swastika-like). Restore the original
firehawk.sc5 layout — a vertical spine crossed by two bars that overhang
it on both sides, plus a hanging lower-right stroke — but even out the
weights: both bars and the leg are a uniform 6px, with the spine slightly
thicker (8px) to keep the original glyph's character.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give every stroke a 6px thickness (stem and right leg width 6, both
crossbars height 6) instead of the previous mix (stem 10, leg 4,
bars 4-5), and recenter the logo. The F+H now reads evenly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Enable single-page horizontal scrolling (R#25 SP2=1) so the scroll
offset wraps within the 256px page. Without it the V9958 default
(two-page) scroll revealed the wrong VRAM page, making new tiles
appear on the opposite edge during horizontal moves.
- Replace the rectangle-approximated FH logo with strokes matching the
original firehawk.sc5 artwork. The F+H is drawn as partitioned fills
(crossbar arms, stem, right leg) with dedicated palette entries for
the two stem/crossbar crossings, so each horizontal then each
vertical shimmers in and out gap-free before the whole logo holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>