Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5789ad8b3 | ||
|
|
aa8fa3ddf9 | ||
|
|
db7de024de | ||
|
|
1c0d2d9a88 | ||
|
|
60c9e13fa8 | ||
|
|
25a098e6af | ||
|
|
97eebbaefc | ||
|
|
70b42f6da1 | ||
|
|
cbde457da5 | ||
|
|
45749775fa |
@@ -41,6 +41,7 @@ dos2
|
|||||||
!/projects/template
|
!/projects/template
|
||||||
!/projects/template_msx2
|
!/projects/template_msx2
|
||||||
!/projects/mazegame
|
!/projects/mazegame
|
||||||
|
!/projects/kissaten_yugure
|
||||||
|
|
||||||
/projects/samples/datasrc
|
/projects/samples/datasrc
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Kissaten Yūgure — MSX2 cozy game
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
A cozy coffee-shop (kissaten) simulation for MSX2, set in a provincial Japanese
|
||||||
|
town in 1987. Target audience: adult retro gamers who love Japanese art and
|
||||||
|
computers. Written in C using the MSXgl library. Working title: "Kissaten Yūgure"
|
||||||
|
(Twilight Coffee Shop).
|
||||||
|
|
||||||
|
Read `docs/DESIGN.md` before making any design or architecture decisions.
|
||||||
|
Read `docs/CONVERSATION.md` for the original design discussion and rationale.
|
||||||
|
|
||||||
|
## Design pillars (never violate)
|
||||||
|
- Cozy contract: no fail states, no bankruptcy, no time pressure outside the
|
||||||
|
brew minigame. Progression is always forward.
|
||||||
|
- Atmosphere over mechanics: palette work, music, and character writing carry
|
||||||
|
the game.
|
||||||
|
- Static-heavy rendering: no scrolling. Only sprites and small blits move.
|
||||||
|
- Small, finishable scope. When in doubt, cut.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
- Target: MSX2, SCREEN 5 (G4, 256×212, 16 colors from 512)
|
||||||
|
- ROM format: ASCII16 MegaROM (banked) — plan bank layout from day one
|
||||||
|
- Library: MSXgl (C) — https://github.com/aoineko-fr/MSXgl
|
||||||
|
- Compiler: SDC C via MSXgl build system
|
||||||
|
- Music: **unresolved — this line is stale.** It describes a PSG / MML → NDP
|
||||||
|
pipeline, but the working pipeline used for `projects/mazegame` is the
|
||||||
|
`msx-music-generator` repo emitting lVGM + ayFX, on MSX-Music (YM2413).
|
||||||
|
This project targets plain MSX2, where FM is not standard hardware.
|
||||||
|
Decision deferred to M9; see `docs/PLAN.md` §8 item 15.
|
||||||
|
- Dialogue: authored in YAML/JSON, compiled to C arrays / binary banks by
|
||||||
|
`tools/compile_dialogue.js` (Node.js)
|
||||||
|
- Emulator for testing: openMSX (also verify on real hardware profiles)
|
||||||
|
|
||||||
|
## Build & test
|
||||||
|
- Build: `./build.sh` (MSXgl build_tool; ROM → `out/` and `emul/rom/kissaten.rom`)
|
||||||
|
- Run: `./build.sh run` → builds and launches openMSX
|
||||||
|
- Requires `node` on PATH, else build.sh falls back to the bundled
|
||||||
|
Windows/Linux binary and dies: `export PATH="/opt/homebrew/opt/node@24/bin:$PATH"`
|
||||||
|
- Headless verification: drive openMSX with a Tcl script —
|
||||||
|
`openmsx -machine C-BIOS_MSX2 -carta emul/rom/kissaten.rom -script test.tcl`
|
||||||
|
using `after time <s> {screenshot f.png}` and `keymatrixdown 8 1` (SPACE).
|
||||||
|
Add `set throttle off` to the script: emulated time then runs many times
|
||||||
|
faster than wall clock, so a 200-second play-through takes a couple of
|
||||||
|
seconds. `after time` still counts emulated seconds, so timings hold.
|
||||||
|
- **Boot is slow — budget for it.** `ROMDelayBoot = true` (needed for disk
|
||||||
|
access, see below) means the game only appears at roughly 40-90s of
|
||||||
|
emulated time. Screenshots before that show a blue BASIC screen, which
|
||||||
|
looks exactly like a hang and is not one. Test with a disk machine:
|
||||||
|
`openmsx -machine Philips_NMS_8250 -carta emul/rom/kissaten.rom -diska save.dsk`
|
||||||
|
- Dialogue data (stage 3): `node tools/compile_dialogue.js data/dialogue/*.yaml`
|
||||||
|
- Music data (stage 3+): `node tools/generate_mml.js` → NDP compiler → `data/music/`
|
||||||
|
|
||||||
|
## Platform gotchas (learned the hard way)
|
||||||
|
- **Keyboard:** read the matrix row inside `DisableInterrupt()`/`EnableInterrupt()`.
|
||||||
|
`Keyboard_Read()` is an `out` (row select) then `in`; the BIOS ISR scans the
|
||||||
|
matrix too, and if it lands between the two you get phantom keypresses.
|
||||||
|
`INPUT_KB_UPDATE` (buffered mode) is *not* a fix — it reads the BIOS
|
||||||
|
NEWKEY/OLDKEY area, which C-BIOS does not maintain in the standard format.
|
||||||
|
- **`BankedCall = true` is off until banked code actually exists.** With it on
|
||||||
|
and nothing banked, RAM globals were corrupted at runtime (state machine
|
||||||
|
ran wild, counters garbage). Re-enable deliberately at stage 3.
|
||||||
|
- Erase blits must repaint the *actual* background at that spot — the counter
|
||||||
|
area has three bands (wainscot / counter top highlight / counter top).
|
||||||
|
|
||||||
|
## Disk saving from a cartridge ROM — WORKING
|
||||||
|
|
||||||
|
Saves go to a real file on a real disk: `KISSAT00.SAV` in the root of the
|
||||||
|
disk in drive A. The disk stays a normal FAT disk you can inspect and copy.
|
||||||
|
|
||||||
|
**Use `engine/src/tool/disk_save.h`.** MSXgl v1.3.0 added this module
|
||||||
|
specifically for "save to disk from a ROM application". Do not hand-roll
|
||||||
|
sector I/O — an earlier attempt using PHYDIO (Main ROM 0144h) and a direct
|
||||||
|
CALSLT to DSKIO (4010h) had both entry points return carry-clear *while
|
||||||
|
transferring nothing*, proven with a sentinel that survived the call intact.
|
||||||
|
|
||||||
|
Four things are required, none of them obvious:
|
||||||
|
|
||||||
|
1. **`LibModules` must include both `"tool/disk_save"` and `"dos"`.** The
|
||||||
|
module alone will not link. See `projects/samples/s_save.js` for the
|
||||||
|
reference configuration, and `s_save.c` for usage.
|
||||||
|
2. **`ROMDelayBoot = true`.** A cartridge's INIT normally runs during the BIOS
|
||||||
|
slot scan and MSXgl never returns from it, so the Disk ROM's own INIT never
|
||||||
|
happens and there is no disk system to talk to. This option installs an
|
||||||
|
H.STKE hook and returns to the scan instead.
|
||||||
|
3. **Boot becomes slow.** The game appears at roughly 40-90s of *emulated*
|
||||||
|
time. Screenshots before that show a blue BASIC screen and look exactly
|
||||||
|
like a hang. Use `set throttle off` in the test script so this costs
|
||||||
|
seconds of wall clock, not minutes.
|
||||||
|
4. **`DiskSave_Check()` returns `SAVEDATA_UNSIGNED` for perfectly good
|
||||||
|
files.** With `AppSignature = true` (`-DAPPSIGN`), `DiskSave_Check()`
|
||||||
|
requires the file's first four bytes to equal `g_AppSignature` — but
|
||||||
|
`DiskSave_Save()` writes the payload raw and never adds it. Treat
|
||||||
|
`SAVEDATA_UNSIGNED` as success; `SaveState` carries its own magic, version
|
||||||
|
and checksum, which is a stronger check regardless.
|
||||||
|
|
||||||
|
**Save timing:** written in `AdvanceDay()` *after* the day counter increments,
|
||||||
|
so the file always describes the morning the player wakes to. Saving during
|
||||||
|
the evening instead makes a reload replay the day just finished and
|
||||||
|
double-count its cups.
|
||||||
|
|
||||||
|
Absent or unusable disk is not an error the player has to handle — it just
|
||||||
|
means nothing persists, and the morning card says so.
|
||||||
|
|
||||||
|
Test:
|
||||||
|
```
|
||||||
|
openmsx -machine Philips_NMS_8250 -carta emul/rom/kissaten.rom -diska save.dsk
|
||||||
|
```
|
||||||
|
Create a blank 720K disk with `tools/build/msxtar/msxtar -cf save.dsk --dos1 --size=720K`.
|
||||||
|
|
||||||
|
**RTC CMOS is not an alternative** for a save this size, despite
|
||||||
|
`RTC_USE_SAVEDATA` being TRUE in `msxgl_config.h`. Block 3 of the RP-5C01 is
|
||||||
|
13 nibbles and MSXgl's `RTC_SaveData()` stores exactly **6 bytes**, against a
|
||||||
|
~40-byte `SaveState`. (MSXgl has no cartridge-SRAM mapper target either; the
|
||||||
|
`PAC` module's FM-PAC SRAM, 8 x 1024 bytes, is the other real option.)
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 537 B |
|
After Width: | Height: | Size: 610 B |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
GIMP Palette
|
||||||
|
Name: Kissaten Yugure (full)
|
||||||
|
Columns: 16
|
||||||
|
#
|
||||||
|
0 0 0 transparent
|
||||||
|
36 36 36 outline
|
||||||
|
73 36 36 dark wood
|
||||||
|
146 73 36 mid wood
|
||||||
|
182 109 73 light wood
|
||||||
|
255 182 73 lamp glow
|
||||||
|
255 219 146 cream
|
||||||
|
109 73 73 muted wall
|
||||||
|
73 36 146 dusk deep
|
||||||
|
146 73 182 dusk violet
|
||||||
|
219 109 109 dusk rose
|
||||||
|
255 146 73 horizon amber
|
||||||
|
109 36 0 coffee
|
||||||
|
73 109 182 coat blue
|
||||||
|
255 182 146 skin
|
||||||
|
255 255 219 warm white
|
||||||
|
After Width: | Height: | Size: 650 B |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
GIMP Palette
|
||||||
|
Name: Kissaten Yugure (portrait)
|
||||||
|
Columns: 11
|
||||||
|
#
|
||||||
|
36 36 36 outline
|
||||||
|
73 36 36 dark wood
|
||||||
|
146 73 36 mid wood
|
||||||
|
182 109 73 light wood
|
||||||
|
255 182 73 lamp glow
|
||||||
|
255 219 146 cream
|
||||||
|
109 73 73 muted wall
|
||||||
|
109 36 0 coffee
|
||||||
|
73 109 182 coat blue
|
||||||
|
255 182 146 skin
|
||||||
|
255 255 219 warm white
|
||||||
|
After Width: | Height: | Size: 474 B |
|
After Width: | Height: | Size: 103 B |
@@ -0,0 +1,12 @@
|
|||||||
|
:: ____________________________
|
||||||
|
:: ██▀▀█▀▀██▀▀▀▀▀▀▀█▀▀█ │ ▄▄▄ ▄▄
|
||||||
|
:: ██ ▀ █▄ ▀██▄ ▀ ▄█ ▄▀▀ █ │ ▀█▄ ▄▀██ ▄█▄█ ██▀▄ ██ ▄███
|
||||||
|
:: █ █ █ ▀▀ ▄█ █ █ ▀▄█ █▄ │ ▄▄█▀ ▀▄██ ██ █ ██▀ ▀█▄ ▀█▄▄
|
||||||
|
:: ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀────────┘ ▀▀
|
||||||
|
:: by Guillaume 'Aoineko' Blanchard under CC BY-SA license
|
||||||
|
::────────────────────────────────────────────────────────────────────
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
cls
|
||||||
|
|
||||||
|
..\..\tools\build\Node\node.exe ..\..\engine\script\js\build.js %1 %2 %3 %4 %5 %6 %7 %8 %9
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ____________________________
|
||||||
|
# ██▀▀█▀▀██▀▀▀▀▀▀▀█▀▀█ │ ▄▄▄ ▄▄
|
||||||
|
# ██ ▀ █▄ ▀██▄ ▀ ▄█ ▄▀▀ █ │ ▀█▄ ▄▀██ ▄█▄█ ██▀▄ ██ ▄███
|
||||||
|
# █ █ █ ▀▀ ▄█ █ █ ▀▄█ █▄ │ ▄▄█▀ ▀▄██ ██ █ ██▀ ▀█▄ ▀█▄▄
|
||||||
|
# ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀────────┘ ▀▀
|
||||||
|
# by Guillaume 'Aoineko' Blanchard under CC BY-SA license
|
||||||
|
#────────────────────────────────────────────────────────────────────
|
||||||
|
clear
|
||||||
|
|
||||||
|
if type -P node; then
|
||||||
|
node ../../engine/script/js/build.js $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||||
|
else
|
||||||
|
../../tools/build/Node/node ../../engine/script/js/build.js $1 $2 $3 $4 $5 $6 $7 $8 $9
|
||||||
|
fi
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# Kissaten Yūgure — Art brief
|
||||||
|
|
||||||
|
## Pipeline — export SVG, not PNG
|
||||||
|
|
||||||
|
**Generate at high resolution, export as SVG, drop it in `art/`, and run:**
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 tools/svg2portrait.py art/aki-neutral.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
That writes `art/aki-neutral-48.png` (the real asset) and
|
||||||
|
`art/aki-neutral-48-x6.png` (a 6× preview for judging).
|
||||||
|
|
||||||
|
**Export SVG, never a resized bitmap.** This was learned the hard way. A
|
||||||
|
48×48 PNG produced by resizing in Photoshop came back with **1330 unique
|
||||||
|
colours** in 2304 pixels — bicubic resampling turns every flat region into a
|
||||||
|
gradient, and reducing *that* to 11 colours speckles horribly, worst on large
|
||||||
|
flat areas like the blazer and background.
|
||||||
|
|
||||||
|
The SVG from Magnific is flat-shaded vector: ~550 paths, solid fills, a
|
||||||
|
handful of gradients. The converter rasterises it at 768×768 (16× the target)
|
||||||
|
via `qlmanage`, then takes the **most common colour in each 16×16 block** —
|
||||||
|
a true pixelate with no blending at all. Measured on Aki: 19 colours after
|
||||||
|
the pixelate, 10 after the palette reduction, and the two are nearly
|
||||||
|
indistinguishable. **The 11-colour portrait palette is sufficient for this
|
||||||
|
art** — that was the open question and it's settled.
|
||||||
|
|
||||||
|
If you must work from bitmaps, the same logic applies: resize only by exact
|
||||||
|
integer factors with nearest-neighbour, never bicubic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The palette — read this before generating anything
|
||||||
|
|
||||||
|
Everything in the game shares **one 16-colour palette**. It is already in
|
||||||
|
`kissaten.c` as `g_Palette` and is what the current build renders with.
|
||||||
|
|
||||||
|
- `art/palette.png` — 16×1 px, one pixel per entry. Machine-readable; use it
|
||||||
|
wherever PixelLab accepts a palette image.
|
||||||
|
- `art/palette_ref.png` — 512×64 swatch strip, same order, for eyeballing.
|
||||||
|
|
||||||
|
| # | Hex | Role | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | `#000000` | transparent | **Never paint with this.** It is the transparent index. |
|
||||||
|
| 1 | `#242424` | outline | near-black; outlines, dark hair |
|
||||||
|
| 2 | `#492424` | dark wood | deep shadow, dark brown hair |
|
||||||
|
| 3 | `#924924` | mid wood | wall panels, doors, deep skin shadow |
|
||||||
|
| 4 | `#B66D49` | light wood | counter top, **skin shadow** |
|
||||||
|
| 5 | `#FFB649` | lamp glow | warm light |
|
||||||
|
| 6 | `#FFDB92` | cream | glassware, **skin highlight** |
|
||||||
|
| 7 | `#6D4949` | muted wall | upper wall, muted cloth |
|
||||||
|
| 8 | `#492492` | dusk deep | **sky only** |
|
||||||
|
| 9 | `#9249B6` | dusk violet | **sky only** |
|
||||||
|
| 10 | `#DB6D6D` | dusk rose | **sky only** |
|
||||||
|
| 11 | `#FF9249` | horizon amber | **sky only** |
|
||||||
|
| 12 | `#6D2400` | coffee | coffee, dark red-brown cloth |
|
||||||
|
| 13 | `#496DB6` | coat blue | uniforms, coats |
|
||||||
|
| 14 | `#FFB692` | skin | **skin base** |
|
||||||
|
| 15 | `#FFFFDB` | warm white | highlights, cup rims, eye whites |
|
||||||
|
|
||||||
|
### The one non-negotiable rule
|
||||||
|
|
||||||
|
**Slots 8–11 are sky. Slots 4–7 are wood and warm interior.** Nothing else may
|
||||||
|
use 8–11.
|
||||||
|
|
||||||
|
This is what makes seasons and time-of-day free: the background bitmap is
|
||||||
|
authored once, and each season/hour is a 32-byte palette swap. If a character's
|
||||||
|
jacket is painted in slot 9, that jacket changes colour every time the sun
|
||||||
|
moves. Getting this wrong is not a cosmetic issue — it silently destroys the
|
||||||
|
whole seasonal system, and you don't find out until M7.
|
||||||
|
|
||||||
|
### Skin, specifically
|
||||||
|
|
||||||
|
There is only one literal "skin" entry, which is too thin for a face. Use
|
||||||
|
these three as a ramp — they're hue-compatible:
|
||||||
|
|
||||||
|
- highlight `#FFDB92` (6) → base `#FFB692` (14) → shadow `#B66D49` (4)
|
||||||
|
- deepest shadow / hair: `#924924` (3), `#492424` (2), `#242424` (1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. What I need back
|
||||||
|
|
||||||
|
| Asset | Size | Count |
|
||||||
|
|---|---|---|
|
||||||
|
| Character portrait | **48×48** | 3 per character (neutral / happy / troubled) |
|
||||||
|
| Shop background | **256×212** | 1 |
|
||||||
|
| Customer sprite | **16×16** | 2 layers per character (fill + outline) |
|
||||||
|
|
||||||
|
**PNG, generated natively at the target size — do not downscale a larger
|
||||||
|
image.** Downscaling pixel art produces half-pixels and anti-aliased edges
|
||||||
|
that cannot be quantised cleanly.
|
||||||
|
|
||||||
|
No anti-aliasing, no gradients, no dithering unless it's deliberate hard
|
||||||
|
checkerboard. If PixelLab can be constrained to the 16 colours, do that; if
|
||||||
|
not, generate close and I'll quantise — but the sky/wood slot rule above
|
||||||
|
still has to hold visually or the quantiser can't fix it.
|
||||||
|
|
||||||
|
**48×48 is very small** — roughly 6 pixels of eye. Faces need a strong
|
||||||
|
silhouette and simplified features. Detail that reads at 200×200 will turn to
|
||||||
|
mud. If a generation looks great zoomed out and illegible at 1:1, it's wrong.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Prompts — Aki (needed now, for M3)
|
||||||
|
|
||||||
|
Hoshino Aki, 17, studying for Tokyo entrance exams, cream soda drinker.
|
||||||
|
Character notes in `SCRIPT.md` §1. Written as a girl there; that's one word to
|
||||||
|
change if you'd rather — the dialogue beats themselves use no gendered
|
||||||
|
pronouns.
|
||||||
|
|
||||||
|
### Portrait palette block — paste into every portrait prompt
|
||||||
|
|
||||||
|
Portraits never show sky, so the four sky slots are simply absent from the
|
||||||
|
list. That is safer than asking the generator to avoid them.
|
||||||
|
|
||||||
|
```
|
||||||
|
Use only these 11 colours and no others:
|
||||||
|
#242424 #492424 #924924 #B66D49 #FFB649 #FFDB92 #6D4949 #6D2400
|
||||||
|
#496DB6 #FFB692 #FFFFDB
|
||||||
|
Skin: #FFDB92 highlight, #FFB692 base, #B66D49 shadow, #924924 deep shadow.
|
||||||
|
Hair and outlines: #242424 or #492424.
|
||||||
|
Eye whites and highlights: #FFFFDB.
|
||||||
|
Fill the entire background behind the head with flat #492424.
|
||||||
|
Do not use pure black #000000 anywhere.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common style stem** — prepend to all three, then append the palette block:
|
||||||
|
|
||||||
|
> 48x48 pixel art portrait, 1987 Japanese anime style as drawn for MSX2 or
|
||||||
|
> PC-88 games, head and shoulders, three-quarter view, hard-edged pixels, no
|
||||||
|
> anti-aliasing, flat cel shading, simple readable silhouette, flat single
|
||||||
|
> colour background
|
||||||
|
|
||||||
|
**Aki — neutral**
|
||||||
|
> ...17 year old Japanese high school student, straight dark bob-length hair,
|
||||||
|
> tired but polite expression, calm eyes, slight shadows under the eyes,
|
||||||
|
> looking slightly off to one side
|
||||||
|
|
||||||
|
**Aki — happy**
|
||||||
|
> ...17 year old Japanese high school student, straight dark bob-length hair,
|
||||||
|
> small genuine smile, eyes softened and slightly narrowed, faint blush,
|
||||||
|
> looking toward the viewer
|
||||||
|
|
||||||
|
**Aki — troubled**
|
||||||
|
> ...17 year old Japanese high school student, straight dark bob-length hair,
|
||||||
|
> eyes cast downward, faint frown, tense mouth, weary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Prompts — the rest of the cast (M8, not urgent)
|
||||||
|
|
||||||
|
Same style stem and the **same portrait palette block from §3**, swapping the
|
||||||
|
clothing line. Note the clothing colours available are only `#496DB6` (blue),
|
||||||
|
`#6D4949` (muted brown-grey), `#6D2400` (dark red-brown), `#492424` and
|
||||||
|
`#242424` — so give each character a distinct one or they'll blur together in
|
||||||
|
the dialogue box.
|
||||||
|
|
||||||
|
**Ōta-san** — retired stationmaster, ~70, black coffee, punctual.
|
||||||
|
> elderly Japanese man in his seventies, close-cropped grey hair, deeply
|
||||||
|
> lined face, upright bearing, plain dark jacket buttoned to the collar
|
||||||
|
- *neutral:* composed, formal · *happy:* faint dignified smile, eyes crinkled
|
||||||
|
- *troubled:* jaw set, looking past the viewer
|
||||||
|
|
||||||
|
**Fujimoto** — manga artist, ~30, exhausted, funny about it.
|
||||||
|
> Japanese manga artist in their thirties, messy shoulder-length hair, heavy
|
||||||
|
> shadows under the eyes, round glasses, rumpled shirt with sleeves pushed up
|
||||||
|
- *neutral:* wired, sleepless · *happy:* crooked grin, animated
|
||||||
|
- *troubled:* hollow stare, hand near face
|
||||||
|
|
||||||
|
**Nakajima-san** — widow, ~65, orders two coffees.
|
||||||
|
> Japanese woman in her mid-sixties, silver hair pinned back neatly, soft
|
||||||
|
> composed face, high-collared blouse under a plain cardigan, small pearl
|
||||||
|
> earrings
|
||||||
|
- *neutral:* serene, self-possessed · *happy:* warm gentle smile
|
||||||
|
- *troubled:* distant, looking at something not in the room
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Prompt — the shop background (M6, longest lead time)
|
||||||
|
|
||||||
|
256×212. This one carries the game's whole look and is the hardest to redo,
|
||||||
|
so it's worth several attempts.
|
||||||
|
|
||||||
|
Composition is fixed by the code — see `DESIGN.md` §4 and `DrawScene()`:
|
||||||
|
|
||||||
|
- Top **16px** is the status bar (drawn over) — keep it empty/dark.
|
||||||
|
- **y=16–148** is the shop scene.
|
||||||
|
- Bottom **64px** is the dialogue window (drawn over) — keep it empty/dark.
|
||||||
|
- Window on the **left** (~x16–88), showing dusk sky in slots 8–11 only.
|
||||||
|
- Door on the **right** (~x208–244).
|
||||||
|
- Counter running horizontally at **y≈98**, its top edge level.
|
||||||
|
- Hanging lamp above the counter, centre.
|
||||||
|
- Siphon coffee rig sits on the counter left of centre — I can keep drawing
|
||||||
|
this procedurally if it's easier to leave out.
|
||||||
|
|
||||||
|
> 256x212 pixel art interior of a small 1987 Japanese kissaten coffee shop at
|
||||||
|
> dusk, viewed straight on, static single-screen game background, wooden
|
||||||
|
> counter running across the lower third, tall window on the left showing a
|
||||||
|
> violet and amber dusk sky, wooden door on the right, hanging lamp with warm
|
||||||
|
> glow above the counter, wood panelling, shelves with glassware, cosy and
|
||||||
|
> quiet, 1987 MSX2 game art, hard-edged pixels, no anti-aliasing, flat cel
|
||||||
|
> shading
|
||||||
|
|
||||||
|
### Background palette block — this one is split by region
|
||||||
|
|
||||||
|
Unlike the portraits, the background needs all 15 colours — but **which
|
||||||
|
colours may appear is decided by where you are in the image**. This is the
|
||||||
|
constraint that makes seasons free, and it is the easiest one to get wrong.
|
||||||
|
|
||||||
|
```
|
||||||
|
Use only these 15 colours and no others.
|
||||||
|
|
||||||
|
For the sky visible through the window, use ONLY these four:
|
||||||
|
#492492 #9249B6 #DB6D6D #FF9249
|
||||||
|
|
||||||
|
For everything else — walls, floor, counter, door, window frame, lamp,
|
||||||
|
shelves, glassware, every object in the room — use ONLY these eleven:
|
||||||
|
#242424 #492424 #924924 #B66D49 #FFB649 #FFDB92 #6D4949 #6D2400
|
||||||
|
#496DB6 #FFB692 #FFFFDB
|
||||||
|
|
||||||
|
The four sky colours must appear nowhere except inside the window opening.
|
||||||
|
The window frame, mullions and sill are wood and use the interior colours.
|
||||||
|
Do not use pure black #000000 anywhere.
|
||||||
|
Leave the top 16 rows and the bottom 64 rows flat #242424 — the game draws
|
||||||
|
its own status bar and dialogue box over them.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this one rule matters more than the rest of this document:** each season
|
||||||
|
and hour of day is a 32-byte palette swap on the same bitmap. Repaint slots
|
||||||
|
8–11 and the sky goes from dusk to noon to rain for free. But if a chair leg
|
||||||
|
or a coffee cup is painted in `#9249B6`, that chair leg changes colour every
|
||||||
|
time the sun moves — and you won't find out until M7, long after the art is
|
||||||
|
finished.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Sprites (M6)
|
||||||
|
|
||||||
|
16×16, two layers per customer: a **fill** layer and an **outline** layer that
|
||||||
|
is the same silhouette dilated by one pixel, drawn behind. Sprite mode 2, so
|
||||||
|
each layer is a single colour — these are shapes, not shaded art. Current
|
||||||
|
placeholders are in `kissaten.c` as `g_SprCustFill` / `g_SprCustLine`.
|
||||||
|
|
||||||
|
**Palette does not apply here in the usual sense.** These are hardware
|
||||||
|
sprites, not bitmap art: each layer is one flat colour chosen at runtime by
|
||||||
|
the code, so the image only needs to define *shape*. Draw them as pure
|
||||||
|
black-on-white silhouettes and I'll assign colours — currently `#496DB6` for
|
||||||
|
the fill layer and `#242424` for the outline, per customer.
|
||||||
|
|
||||||
|
Worth doing by hand rather than generating; a 16×16 single-colour silhouette
|
||||||
|
is faster to draw than to prompt for.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Design conversation record — Kissaten Yūgure
|
||||||
|
|
||||||
|
A record of the original brainstorm and design discussion (Claude ↔ Jurjen,
|
||||||
|
July 2026) that led to this project. Kept for rationale and context; the
|
||||||
|
distilled result lives in `DESIGN.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The brief
|
||||||
|
|
||||||
|
Jurjen wanted a new MSX2 game idea: **a cozy game targeted mainly at male
|
||||||
|
adults who love Japanese art and retro games**, built with MSXgl.
|
||||||
|
|
||||||
|
Claude's read on the audience: nostalgia for late-80s Japan, quiet melancholy,
|
||||||
|
craftsmanship, and pixel art in the style of MSX magazine covers / Konami and
|
||||||
|
Compile titles. The MSX2 itself is a Japanese cultural artifact, so the
|
||||||
|
platform is part of the appeal. Reference vibe: *A Short Hike* energy,
|
||||||
|
rendered like an MSX2 classic.
|
||||||
|
|
||||||
|
## 2. Four concepts pitched
|
||||||
|
|
||||||
|
1. **Showa-era shopkeeper sim** — run a tiny kissaten, sento, or ramen stall
|
||||||
|
in a sleepy 1980s town. Regulars with small stories, seasons change the
|
||||||
|
palette (SCREEN 5 palette swaps per season = big atmosphere, low cost).
|
||||||
|
Simple state machines + dialogue, MSXgl-friendly; PSG/NDP pipeline fits
|
||||||
|
gentle city-pop-adjacent loops. Hits the demographic dead center.
|
||||||
|
|
||||||
|
2. **Bonsai / garden tending game** — shape a tree or karesansui garden over
|
||||||
|
in-game years via a simple L-system-ish growth model. Minimal input,
|
||||||
|
maximal zen, sumi-e visuals, persistent growth via save. Smallest and most
|
||||||
|
distinctive option; nobody has made it on MSX.
|
||||||
|
|
||||||
|
3. **Rural train journey game** — conductor/passenger on a one-car local
|
||||||
|
line; each station is a vignette. Landscapes are the star, but horizontal
|
||||||
|
scrolling is work on MSX2 (vertical scroll register is free), so it would
|
||||||
|
need screen-by-screen scenes or parallax-lite. Densha + inaka nostalgia.
|
||||||
|
|
||||||
|
4. **Yokai postman / errand game** — cozy-spooky letter delivery between
|
||||||
|
friendly yokai at dusk. Iconic character material, light fetch-quest
|
||||||
|
structure, Ghibli / Mizuki Shigeru vibe.
|
||||||
|
|
||||||
|
**Recommendation:** #1 or #2, because scope kills hobby projects. #1 had the
|
||||||
|
strongest emotional hook and marketing appeal ("run a coffee shop on real
|
||||||
|
MSX2 hardware"); #2 was the most achievable and distinctive.
|
||||||
|
|
||||||
|
**Cross-cutting principles:** lean into palette work (MSX2's superpower),
|
||||||
|
keep text minimal or bilingual-friendly for the Japanese retro community,
|
||||||
|
and let the MML → NDP music pipeline carry the mood.
|
||||||
|
|
||||||
|
## 3. Decision
|
||||||
|
|
||||||
|
Jurjen picked **#1, the kissaten sim**. Working title: **Kissaten Yūgure**
|
||||||
|
("Twilight Coffee Shop") — you inherit a small kissaten in a fictional
|
||||||
|
provincial town, 1987.
|
||||||
|
|
||||||
|
## 4. Design deep-dive (summary — full detail in DESIGN.md)
|
||||||
|
|
||||||
|
- **Core loop:** one day ≈ 5–10 real minutes. Morning prep (menu focus,
|
||||||
|
radio weather, stock) → open hours (serve + siphon-brew timing minigame +
|
||||||
|
dialogue with regulars) → evening close (till, journal, save).
|
||||||
|
- **Cozy contract:** no fail states, no bankruptcy; money gates cosmetic and
|
||||||
|
menu upgrades (grinder, jazz records, a cat). Progression = story + seasons.
|
||||||
|
- **Regulars:** 6–8 characters (retired stationmaster, exam student, manga
|
||||||
|
artist, widow...), each with 12–20 short scenes triggered by day count,
|
||||||
|
season, weather, or what you serve. Right order at the right moment
|
||||||
|
advances arcs.
|
||||||
|
- **Screen layout (SCREEN 5, 256×212):** 16 px status bar (day/season,
|
||||||
|
clock, ¥) · 132 px shop scene (door, counter with customer sprites,
|
||||||
|
siphon rig) · 64 px dialogue window (48×48 portrait, 3 lines × 26 chars,
|
||||||
|
typewriter reveal). Static-heavy on purpose: only sprites move — no
|
||||||
|
scrolling — the biggest scope-saver in the design.
|
||||||
|
- **Hardware mapping:** VRAM page 0 = scene bitmap, page 1 = asset warehouse
|
||||||
|
blitted via HMMM/HMMV. Seasonal palette trick: one background authored
|
||||||
|
around palette slots (sky in 8–11, wood tones in 4–7); each season/time of
|
||||||
|
day is a 32-byte palette table, optionally lerped across the day. Sprite
|
||||||
|
mode 2, two layered 16×16 sprites per customer. Portraits 48×48 × 3
|
||||||
|
expressions — where the pixel-art budget goes.
|
||||||
|
- **Data model:** tiny `SaveState` (~40 bytes) with day, season, weather,
|
||||||
|
money, unlock bitmask, and 8 `Regular` structs (arc_stage, affinity,
|
||||||
|
last_visit_day). Dialogue as data: YAML/JSON compiled by a Node.js tool to
|
||||||
|
C arrays / binary banks. ASCII16 MegaROM layout from day one.
|
||||||
|
- **Visitor logic:** weighted table per tick, filtered by season / weather /
|
||||||
|
time / days-since-last-visit — tunable in a spreadsheet, produces
|
||||||
|
noticeable patterns ("the stationmaster always comes when it rains").
|
||||||
|
- **Music:** 6–8 short cozy PSG loops via the existing MML → NDP →
|
||||||
|
`ndp_player` pipeline (`generate_mml.js` for AI-assisted MML). Diegetic
|
||||||
|
jukebox upgrade lets the player pick the shop's track.
|
||||||
|
- **Scope ladder:** (1) scene + one customer + brew + serve → (2) day cycle
|
||||||
|
+ save + money → (3) dialogue engine + two regulars ← real milestone →
|
||||||
|
(4) seasons/weather/palettes → (5) full cast, upgrades, endings. Every
|
||||||
|
stage playable.
|
||||||
|
|
||||||
|
## 5. Moving to Claude Desktop / Claude Code
|
||||||
|
|
||||||
|
Jurjen asked about continuing the project in Claude Desktop with context
|
||||||
|
files. Plan:
|
||||||
|
|
||||||
|
- Put a lean **CLAUDE.md** in the project root — Claude Code reads it at the
|
||||||
|
start of every session. It holds build commands, toolchain quirks (SDCC,
|
||||||
|
MSXgl paths, openMSX), conventions, and design pillars; kept under ~200
|
||||||
|
lines, referencing deeper docs instead of duplicating them.
|
||||||
|
- Full design lives in **docs/DESIGN.md**; this file (**CONVERSATION.md**)
|
||||||
|
preserves the original discussion and rationale.
|
||||||
|
- Claude Code's auto memory will additionally accumulate learned corrections
|
||||||
|
(e.g. bank layout details) over time.
|
||||||
|
- The dialogue compiler (`compile_dialogue.js`) will live in `tools/` next to
|
||||||
|
the existing `generate_mml.js`.
|
||||||
|
|
||||||
|
## 6. Open threads (not yet designed)
|
||||||
|
|
||||||
|
- Dialogue script format spec + compiler implementation
|
||||||
|
- Brew minigame detailed design
|
||||||
|
- Palette / day-cycle system implementation details
|
||||||
|
- Character roster finalization and story arcs
|
||||||
|
- Bank layout plan for the ASCII16 MegaROM
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Kissaten Yūgure — Design document
|
||||||
|
|
||||||
|
Working title: **Kissaten Yūgure** ("Twilight Coffee Shop")
|
||||||
|
Platform: MSX2 · Library: MSXgl (C) · ROM: ASCII16 MegaROM
|
||||||
|
|
||||||
|
> **Amendments.** This document still describes the game's intent and is the
|
||||||
|
> place to start. A few specifics have since been decided differently — where
|
||||||
|
> they conflict, the documents below win:
|
||||||
|
>
|
||||||
|
> - **Cast size: four, not six to eight** (§3). The four examples in §3 became
|
||||||
|
> the entire roster. Rationale in `PLAN.md` §6; full arcs in `SCRIPT.md`.
|
||||||
|
> - **Endings: one**, not several (§8 stage 5). `PLAN.md` §6.
|
||||||
|
> - **Saving: a real save disk** (`.dsk`), not yet working — see the disk
|
||||||
|
> section in `../CLAUDE.md`. Note the RP-5C01 CMOS holds only 6 bytes
|
||||||
|
> through MSXgl, so it is not an alternative for a ~40-byte `SaveState`.
|
||||||
|
> - **Music (§7) is out of date.** It describes a PSG / MML → NDP pipeline;
|
||||||
|
> the working pipeline is `msx-music-generator` → lVGM + ayFX. Which chip
|
||||||
|
> this game targets is still open — `PLAN.md` §8 item 15.
|
||||||
|
> - **The brew minigame is coffee-only.** Aki's cream soda is poured, not
|
||||||
|
> brewed, so her visits skip it — `SCRIPT.md` §8.
|
||||||
|
>
|
||||||
|
> Build order and per-stage requirements live in `PLAN.md`, which supersedes
|
||||||
|
> the scope ladder in §8 below with a finer eleven-milestone breakdown.
|
||||||
|
|
||||||
|
## 1. Concept
|
||||||
|
|
||||||
|
You inherit a small kissaten (coffee shop) in a fictional provincial Japanese
|
||||||
|
town in 1987. A cozy, no-fail-state game about serving regulars, learning
|
||||||
|
their stories, and watching the town and seasons change.
|
||||||
|
|
||||||
|
**Audience:** adult retro gamers with a love of Japanese art, computers, and
|
||||||
|
late-80s Japan nostalgia. The MSX2 itself is part of the fantasy — the game
|
||||||
|
should look like it came off an MSX magazine cover.
|
||||||
|
|
||||||
|
**Tone references:** A Short Hike (pace and warmth), Konami/Compile-era MSX2
|
||||||
|
pixel art, Showa-era kissaten culture, gentle city-pop-adjacent PSG music.
|
||||||
|
|
||||||
|
**The cozy contract:** no fail states. You cannot go bankrupt. Money gates
|
||||||
|
cosmetic and menu upgrades (better grinder, jazz records, a cat), not
|
||||||
|
survival. Progression = unlocking regulars' stories and seasonal change.
|
||||||
|
|
||||||
|
## 2. Core loop
|
||||||
|
|
||||||
|
One in-game day ≈ 5–10 real minutes, in three phases:
|
||||||
|
|
||||||
|
1. **Morning (prep):** choose today's menu focus (coffee blend, toast set,
|
||||||
|
curry), check the radio for weather (weather affects who visits), buy stock.
|
||||||
|
2. **Open hours (the heart):** customers arrive one or two at a time. Take
|
||||||
|
orders, brew/cook via a timing minigame, and — most importantly — talk.
|
||||||
|
Regulars have multi-day story arcs told in short dialogue beats.
|
||||||
|
3. **Evening (close):** count the till, journal entry summarizing the day, save.
|
||||||
|
|
||||||
|
**Brew minigame:** a siphon coffee brew with a "release at the right moment"
|
||||||
|
mechanic. One-button friendly, thematic, satisfying to master. Quality result
|
||||||
|
feeds into customer affinity slightly (never punishingly).
|
||||||
|
|
||||||
|
## 3. The regulars
|
||||||
|
|
||||||
|
Six to eight recurring characters; this is where the game lives. Examples:
|
||||||
|
|
||||||
|
- The retired stationmaster
|
||||||
|
- A high-schooler studying for entrance exams
|
||||||
|
- A manga artist missing deadlines
|
||||||
|
- A widow who orders the same thing daily
|
||||||
|
|
||||||
|
Each has a story arc of ~12–20 short scenes, triggered by day count, season,
|
||||||
|
weather, or what you serve them. Serving the *right* thing at the right moment
|
||||||
|
advances arcs — the player learns preferences by paying attention, which
|
||||||
|
mechanically rewards the "regulars at my shop" fantasy.
|
||||||
|
|
||||||
|
**Visitor logic per open-hours tick:** roll against a weighted table filtered
|
||||||
|
by (season, weather, time-of-day, days-since-last-visit). Simple, tunable in
|
||||||
|
a spreadsheet, and it naturally produces patterns players notice and love
|
||||||
|
("the stationmaster always comes when it rains").
|
||||||
|
|
||||||
|
## 4. Screen layout — SCREEN 5 (256×212)
|
||||||
|
|
||||||
|
| Region | Height | Contents |
|
||||||
|
|---|---|---|
|
||||||
|
| Status bar | 16 px | Day + season · clock · money (¥) |
|
||||||
|
| Shop scene | 132 px | Tile/bitmap background: window, shelves, radio. Door zone (customer entry), counter zone (16×16 layered customer sprites), siphon rig zone (brew minigame). |
|
||||||
|
| Dialogue window | 64 px | 48×48 portrait left; 3 lines × 26 chars text with typewriter reveal; choice cursor on line 3 when needed. |
|
||||||
|
|
||||||
|
Deliberately static-heavy: only sprites move, so the MSX2's weak scrolling is
|
||||||
|
sidestepped entirely. Status bar and dialogue window redraw rarely — only the
|
||||||
|
scene needs per-frame attention. This is the single biggest scope-saver.
|
||||||
|
|
||||||
|
## 5. Hardware / MSXgl mapping
|
||||||
|
|
||||||
|
**Video.** SCREEN 5 (G4), 16 colors from 512.
|
||||||
|
- VRAM page 0: visible shop background bitmap.
|
||||||
|
- VRAM page 1: asset warehouse — customer sprite frames, portraits, UI tiles,
|
||||||
|
blitted with VDP commands (HMMM); dialogue box open/close is HMMV rectangle
|
||||||
|
fill + font rendering, never touching the scene.
|
||||||
|
|
||||||
|
**Seasonal palette trick.** Author the background once, designed around
|
||||||
|
palette slots: sky-through-window colors in slots 8–11, wood/warm tones in
|
||||||
|
4–7. Each season *and* time of day is then just a 32-byte palette table —
|
||||||
|
morning spring light vs. rainy autumn dusk on the same bitmap, basically
|
||||||
|
free. Optionally lerp between palettes across the in-game day for a slow
|
||||||
|
ambient shift. This is the most atmospheric feature in the game.
|
||||||
|
|
||||||
|
**Sprites.** Sprite mode 2 (per-line colors on 16×16). Two layered sprites
|
||||||
|
per customer (outline + fill) for the classic MSX2 look. Max two customers
|
||||||
|
on screen plus player hands/steam effects — nowhere near the 8-per-line limit.
|
||||||
|
|
||||||
|
**Portraits.** 48×48 with three expressions per character (neutral, happy,
|
||||||
|
troubled), copied from page 1 into the dialogue box. Portraits are where the
|
||||||
|
pixel-art budget goes; they sell the characters.
|
||||||
|
|
||||||
|
**ROM.** ASCII16 MegaROM from day one — dialogue, portraits, and music banks
|
||||||
|
will not fit in 32KB. MSXgl supports banked ROMs well.
|
||||||
|
|
||||||
|
## 6. Data model
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
u8 id;
|
||||||
|
u8 arc_stage; // progress through their story
|
||||||
|
u8 affinity; // 0-255, raised by right orders
|
||||||
|
u8 last_visit_day;
|
||||||
|
} Regular;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
u16 day;
|
||||||
|
u8 season; // derives palette set
|
||||||
|
u8 weather; // affects visitor table
|
||||||
|
u16 money;
|
||||||
|
u8 unlocked_items; // bitmask: grinder, records, cat...
|
||||||
|
Regular regulars[8];
|
||||||
|
} SaveState; // ~40 bytes → trivial to save
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dialogue** is the biggest data mass. Scenes stored in a flat script format:
|
||||||
|
|
||||||
|
```
|
||||||
|
(trigger conditions) → (portrait id, expression, text lines,
|
||||||
|
optional choice, effects)
|
||||||
|
```
|
||||||
|
|
||||||
|
Authored in YAML/JSON; a small Node.js compiler (`tools/compile_dialogue.js`)
|
||||||
|
emits C arrays or a binary blob for a MegaROM bank. Budget: ~8 characters ×
|
||||||
|
~15 scenes plus ambient lines.
|
||||||
|
|
||||||
|
## 7. Music
|
||||||
|
|
||||||
|
PSG via the MML → NDP compiler → `ndp_player` pipeline, with AI-assisted MML
|
||||||
|
generation through `tools/generate_mml.js` (Anthropic API).
|
||||||
|
|
||||||
|
Needs: 6–8 short cozy loops rather than driving action tracks — morning,
|
||||||
|
afternoon, evening, rain, plus one theme per major story beat.
|
||||||
|
|
||||||
|
**Diegetic jukebox:** the record-player upgrade lets the player choose the
|
||||||
|
shop's background track — a feature and a home for all generated music in one.
|
||||||
|
|
||||||
|
## 8. Scope ladder
|
||||||
|
|
||||||
|
Build in this order so every stage is a playable game:
|
||||||
|
|
||||||
|
1. Shop scene + one customer + brew minigame + serve loop, no story
|
||||||
|
2. Day/night + save + money
|
||||||
|
3. Dialogue engine + two regulars with short arcs ← **the real milestone**
|
||||||
|
4. Seasons/weather/palettes
|
||||||
|
5. Remaining cast, upgrades, endings
|
||||||
|
|
||||||
|
If serving the exam student his usual and getting one new line of his story
|
||||||
|
feels good at stage 3, the game works.
|
||||||
|
|
||||||
|
## 9. Other principles
|
||||||
|
|
||||||
|
- Lean hard into palette work — the MSX2's superpower.
|
||||||
|
- Keep text minimal or bilingual-friendly; the Japanese retro community is a
|
||||||
|
big chunk of the real audience.
|
||||||
|
- Let the soundtrack carry the mood — cozy games live or die on music.
|
||||||
|
- Marketing hook: "run a coffee shop on real MSX2 hardware."
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
# Kissaten Yūgure — Production plan
|
||||||
|
|
||||||
|
Companion to `DESIGN.md` (what the game is) and `CONVERSATION.md` (why).
|
||||||
|
This document is **how it gets built**: milestone order, the cast, the story
|
||||||
|
architecture, and what has been deliberately cut.
|
||||||
|
|
||||||
|
Status at time of writing: **M1 complete** (scope ladder stage 1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Organizing principle
|
||||||
|
|
||||||
|
The code in this project is small. Stage 1 — shop scene, customer, brew
|
||||||
|
minigame, full serve loop — took one sitting and fits in 17KB of a 128KB ROM.
|
||||||
|
Every remaining *system* is comparably small.
|
||||||
|
|
||||||
|
The risk is content. Four characters with real arcs is roughly 60 scenes of
|
||||||
|
writing and 12 portraits of pixel art. The failure mode for a project like
|
||||||
|
this is producing all of that before discovering the core moment doesn't land.
|
||||||
|
|
||||||
|
So the plan is ordered by one rule:
|
||||||
|
|
||||||
|
> **Reach the emotional test on the least content possible, then build the
|
||||||
|
> machinery around what you learned.**
|
||||||
|
|
||||||
|
`DESIGN.md` §8 states the test directly: *if serving the exam student his
|
||||||
|
usual and getting one new line of his story feels good, the game works.*
|
||||||
|
Everything before M3 exists only to make that test possible. Everything after
|
||||||
|
M3 is scaling something already proven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Milestones
|
||||||
|
|
||||||
|
Each milestone has an exit criterion. Don't advance without meeting it.
|
||||||
|
|
||||||
|
Each also lists **what it requires** — split into what I need *from you*
|
||||||
|
(decisions, writing, art) and what must be *in place* technically. Anything
|
||||||
|
under "from you" is a hard block: I can't guess my way past it, and guessing
|
||||||
|
wrong is expensive to unwind later.
|
||||||
|
|
||||||
|
### M1 — Serve loop ✅ *(complete)*
|
||||||
|
|
||||||
|
Shop scene, one customer, siphon brew minigame, serve loop, no story.
|
||||||
|
|
||||||
|
**Exit:** ✅ A customer arrives, orders, is served a brew of variable quality,
|
||||||
|
reacts, and leaves; the counter increments and the next arrives.
|
||||||
|
|
||||||
|
### M2 — The day spine
|
||||||
|
|
||||||
|
The thin half of scope-ladder stage 2. Day counter, three phase transitions
|
||||||
|
(morning prep → open hours → evening close), save/restore, clock and day in
|
||||||
|
the status bar.
|
||||||
|
|
||||||
|
Money is deliberately **not** built here beyond a displayed number. It gates
|
||||||
|
nothing until M9, and a value that gates nothing isn't worth debugging yet.
|
||||||
|
|
||||||
|
This milestone exists because the day counter is the spine every dialogue
|
||||||
|
trigger hangs from — arc pacing, `last_visit_day`, "he always comes when it
|
||||||
|
rains" patterns.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **Save medium.** My recommendation: **RTC CMOS**. Every MSX2 has it, it
|
||||||
|
holds ~50 bytes, the `SaveState` in `DESIGN.md` §6 is ~40, and
|
||||||
|
`msxgl_config.h` already enables `RTC_USE_SAVEDATA` + `RTC_USE_SAVESIGNED`.
|
||||||
|
Alternatives are cartridge SRAM (needs mapper support, better capacity) or
|
||||||
|
the FM-PAC's SRAM (needs the cartridge present). Say the word if you'd
|
||||||
|
rather not depend on CMOS.
|
||||||
|
- **Day length.** `DESIGN.md` says 5–10 real minutes. I need one number to
|
||||||
|
tune against, plus a rough customers-per-day count (I'd start at 4–6).
|
||||||
|
- **Clock model:** does time advance in real seconds, or one tick per customer
|
||||||
|
served? Event-driven is more forgiving of a player who walks away — which
|
||||||
|
suits the cozy contract — and it's what I'd pick.
|
||||||
|
|
||||||
|
**Requires — in place:** nothing beyond M1.
|
||||||
|
|
||||||
|
**Produces:** `SaveState` struct, phase state machine, save/load module.
|
||||||
|
|
||||||
|
**Status: ✅ complete.** Day counter, three phases, journal line, `SaveState`
|
||||||
|
and disk persistence all working. Verified by playing a day, cold-booting, and
|
||||||
|
resuming at day 2 with the served count intact.
|
||||||
|
|
||||||
|
**Pacing dial, revisited at the end.** `CUSTOMERS_PER_DAY` in `kissaten.c` is
|
||||||
|
the only number that sets day length. Worth reconsidering at **M10** as a
|
||||||
|
player-facing choice — the slot screen is already a boot-time front-end, so
|
||||||
|
"how long should a day be?" has a natural home there, asked once when a new
|
||||||
|
shop is started. A game with no fail states can afford to let the player set
|
||||||
|
its own tempo, and it costs one saved byte.
|
||||||
|
Not worth building before the content exists: the right default only becomes
|
||||||
|
obvious once days have stories in them.
|
||||||
|
|
||||||
|
### M3 — Vertical slice ← **the milestone that matters**
|
||||||
|
|
||||||
|
One regular. Six beats. **Hardcoded in C.** No YAML, no compiler, no engine,
|
||||||
|
no data format.
|
||||||
|
|
||||||
|
Build the exam student (§3 below) because their arc is the most legible: the
|
||||||
|
"usual" is visually distinct (cream soda, not coffee), the want is stated
|
||||||
|
early, and the change is a single order switch the player can *see*.
|
||||||
|
|
||||||
|
Also needed: order recognition — the customer asks for something specific, and
|
||||||
|
serving it (vs. not) advances or holds the arc.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **Aki's six beats, as written text.** 6 × 3 lines × 26 chars. This is the
|
||||||
|
hard block on M3 — there is no version of this milestone without it. Either
|
||||||
|
you write them, or you tell me to draft them and review. If I draft: I need
|
||||||
|
a tone sample you like (a scene from a game or book that hits the register
|
||||||
|
you want) so the first pass isn't a guess.
|
||||||
|
- **The item list at this point.** Minimum coffee + cream soda, but tell me if
|
||||||
|
the toast set or curry from `DESIGN.md` §2 should exist yet.
|
||||||
|
- **A scratch portrait for Aki — 48×48, three expressions.** Crude is fine;
|
||||||
|
ugly is fine. But M3's exit criterion is an *emotional* judgement, and
|
||||||
|
judging it with a blank brown rectangle where a face belongs stacks the deck
|
||||||
|
against the test. This is the one place art work moves earlier than M6, and
|
||||||
|
it's worth it.
|
||||||
|
|
||||||
|
**Requires — in place:** M2's day counter (arc beats are paced by day).
|
||||||
|
|
||||||
|
**Produces:** hardcoded arc, order-recognition logic, "the usual" concept.
|
||||||
|
|
||||||
|
**Exit:** The `DESIGN.md` test. Serve the student their usual, get the next
|
||||||
|
beat of their story, and want to come back tomorrow to see the following one.
|
||||||
|
If this is flat, stop and fix the *writing*, not the code.
|
||||||
|
|
||||||
|
### M4 — Dialogue engine and compiler
|
||||||
|
|
||||||
|
Now — and only now — extract the engine from what M3 taught. Scene data model,
|
||||||
|
trigger evaluation, `tools/compile_dialogue.js` emitting a binary bank, and
|
||||||
|
`BankedCall = true` re-enabled (see §7 hazards).
|
||||||
|
|
||||||
|
Port the student's hardcoded arc to data and prove it plays identically.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **Sign-off on the scene schema before I write the compiler.** I'll propose
|
||||||
|
the YAML shape as a one-page sample; reviewing it costs ten minutes and
|
||||||
|
changing it after 60 scenes are authored costs a rewrite.
|
||||||
|
- **The string-ID decision** from §4 — whether to route text through IDs now
|
||||||
|
for a possible Japanese pass later. Cheap today, expensive to retrofit.
|
||||||
|
|
||||||
|
**Requires — in place:**
|
||||||
|
- M3 proven (the schema should be derived from real content, not imagined).
|
||||||
|
- `node` on PATH for the compiler — already working, documented in
|
||||||
|
`../CLAUDE.md`.
|
||||||
|
- `BankedCall = true` re-enabled, carefully. See §7.
|
||||||
|
|
||||||
|
**Produces:** `tools/compile_dialogue.js`, `data/dialogue/*.yaml`, the banked
|
||||||
|
dialogue blob, and the bank layout that everything after M4 slots into.
|
||||||
|
|
||||||
|
**Exit:** Adding a scene requires editing a YAML file and rebuilding. Zero C
|
||||||
|
changes.
|
||||||
|
|
||||||
|
### M5 — Second regular and visitor logic
|
||||||
|
|
||||||
|
Add the stationmaster. Build the weighted arrival table from `DESIGN.md` §3 —
|
||||||
|
roll per open-hours tick, filtered by season, weather, time-of-day, and
|
||||||
|
days-since-last-visit.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **Ōta's full arc** (~15 beats). Same authoring question as M3 — yours or
|
||||||
|
mine to draft.
|
||||||
|
- **Initial arrival weights**, or approval of a table I propose.
|
||||||
|
`DESIGN.md` §3 wants this tunable in a spreadsheet; I'd rather hand you a
|
||||||
|
starting table to react to than ask you to invent numbers cold.
|
||||||
|
- **How visible the pattern should be.** "The stationmaster always comes when
|
||||||
|
it rains" only lands if it's *nearly* deterministic. My instinct is to make
|
||||||
|
weather-driven arrivals much stronger than feels statistically tasteful,
|
||||||
|
because the player needs to notice.
|
||||||
|
|
||||||
|
**Requires — in place:** M4 engine and compiler.
|
||||||
|
|
||||||
|
**Produces:** visitor weight table, arrival roll, multi-arc scheduling.
|
||||||
|
|
||||||
|
**Exit:** Two arcs interleave over a week of play without colliding, and the
|
||||||
|
arrival pattern is legible enough that you can predict who's coming.
|
||||||
|
|
||||||
|
### M6 — Art pass
|
||||||
|
|
||||||
|
The real background bitmap on the palette-slot layout already reserved in
|
||||||
|
code (sky 8–11, wood/warm 4–7), plus 12 portraits (4 characters × 3
|
||||||
|
expressions). Retire the placeholder `HMMV` rectangles.
|
||||||
|
|
||||||
|
**Requires — from you:** generated in **PixelLab** from the prompts in
|
||||||
|
`ART.md` — one 256×212 background and 12 portraits (48×48, three expressions
|
||||||
|
× four regulars), returned as PNGs in `art/`.
|
||||||
|
|
||||||
|
The palette spec and the sky/wood slot rule are written up in `ART.md` §1,
|
||||||
|
with machine- and human-readable references at `art/palette.png` and
|
||||||
|
`art/palette_ref.png`. **That rule is the whole of M7 riding on one
|
||||||
|
constraint**, and a generator will not respect it unprompted.
|
||||||
|
|
||||||
|
**Requires — in place:**
|
||||||
|
- A PNG → SC5 conversion path. `projects/mazegame` already has working
|
||||||
|
`convert_screens.py` / `convert_tiles.py` precedent to adapt, plus MSXtk.
|
||||||
|
|
||||||
|
**Produces:** converted asset banks, page-1 warehouse layout, blit routines.
|
||||||
|
|
||||||
|
**Exit:** No procedural placeholder fills remain in the shop scene.
|
||||||
|
|
||||||
|
### M7 — Seasons, weather, palettes
|
||||||
|
|
||||||
|
Scope-ladder stage 4. Palette tables per season × time-of-day, optional lerp
|
||||||
|
across the in-game day, weather affecting the arrival table.
|
||||||
|
|
||||||
|
Deliberately placed *after* the art pass and *after* two working arcs. See §7.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **How many palette sets.** 4 seasons × 3 times of day = 12 tables at 32
|
||||||
|
bytes each (384 bytes — trivial). But that's 12 moods to *art-direct*, which
|
||||||
|
is the real cost. Fewer, done well, beats twelve done mechanically.
|
||||||
|
- **Mood direction** for each: I can propose the RGB tables, but "rainy
|
||||||
|
autumn dusk" needs your eye to confirm it feels right, not mine.
|
||||||
|
|
||||||
|
**Requires — in place:** **M6 must be done.** This milestone is a no-op
|
||||||
|
without a background authored on the palette-slot layout — that dependency is
|
||||||
|
the entire reason M7 sits after the art pass instead of before it.
|
||||||
|
|
||||||
|
**Produces:** palette tables, time-of-day lerp, weather → arrival hookup.
|
||||||
|
|
||||||
|
**Exit:** A spring morning and a rainy autumn dusk are visibly different moods
|
||||||
|
on identical bitmap data.
|
||||||
|
|
||||||
|
### M8 — Remaining cast
|
||||||
|
|
||||||
|
The manga artist and the widow, full arcs. This is the largest pure-writing
|
||||||
|
block in the project — roughly 30 scenes.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **Fujimoto's and Nakajima's arcs** — ~30 beats total, the single largest
|
||||||
|
writing block in the project.
|
||||||
|
- **A ruling on Nakajima's reveal.** Her second coffee is the best hook in the
|
||||||
|
cast precisely because the player solves it unaided. I need to know how far
|
||||||
|
you're willing to go without confirming it — my instinct is *very* far, and
|
||||||
|
that it should never be stated outright by anyone but her.
|
||||||
|
|
||||||
|
**Requires — in place:** M4 engine, M6 portraits.
|
||||||
|
|
||||||
|
**Exit:** All four arcs complete and reachable in one playthrough.
|
||||||
|
|
||||||
|
### M9 — Music and upgrades
|
||||||
|
|
||||||
|
6–8 cozy loops via the existing `msx-music-generator` pipeline. The
|
||||||
|
record-player upgrade turns the jukebox into both a feature and a home for
|
||||||
|
every generated track. Money finally gates something.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **Which sound chip.** This needs settling, because the docs and your actual
|
||||||
|
toolchain disagree: `DESIGN.md` §7 and `CLAUDE.md` describe a PSG / MML →
|
||||||
|
NDP pipeline, but the working pipeline you built for `mazegame` is
|
||||||
|
`msx-music-generator` → **lVGM + ayFX**, using MSX-Music (YM2413) on an
|
||||||
|
MSX2+. This project targets plain MSX2, where FM is *not* standard — it
|
||||||
|
needs an FM-PAC. Options: PSG-only (universal, thinner), FM with PSG
|
||||||
|
fallback (best sound, more work), or retarget to MSX2+. I'd take **PSG-only
|
||||||
|
through the lVGM pipeline you already have**, and treat FM as a bonus path.
|
||||||
|
Either way `DESIGN.md` §7 should be updated to match reality.
|
||||||
|
- **Briefs for 6–8 tracks:** mood, rough tempo, loop length. Morning,
|
||||||
|
afternoon, evening, rain, plus a theme or two for story beats.
|
||||||
|
- **Upgrade list and prices** — the point at which money finally means
|
||||||
|
something.
|
||||||
|
|
||||||
|
**Requires — in place:** money economy from M2 promoted to actually gating.
|
||||||
|
|
||||||
|
**Exit:** Music changes with time-of-day and weather; the player can choose a
|
||||||
|
record.
|
||||||
|
|
||||||
|
### M10 — Save slots (multi-file)
|
||||||
|
|
||||||
|
Several shops on one disk. `tool/disk_save` is already built for this: every
|
||||||
|
call takes an `entry` index, so slots become `KISSAT00.SAV`, `KISSAT01.SAV`
|
||||||
|
and so on with no change to the save format. `DiskSave_Check(entry)` reports
|
||||||
|
each slot's state, `DiskSave_GetFreeEntries()` counts the free ones, and
|
||||||
|
`DiskSave_Delete(entry)` removes one. **The storage side is nearly free.**
|
||||||
|
|
||||||
|
The actual work is front-end: a slot screen at boot showing what's in each
|
||||||
|
one, and a confirm step before overwriting or deleting. That is also why this
|
||||||
|
milestone sits late — it touches the boot flow, and it is much easier to
|
||||||
|
design once there is real save content to display.
|
||||||
|
|
||||||
|
Worth framing in-fiction rather than as a file manager. A slot is not "Save
|
||||||
|
1", it is a shop with days behind it — *"Day 34 · 210 cups"* tells the player
|
||||||
|
which one is theirs better than a filename does.
|
||||||
|
|
||||||
|
**Requires — from you — all deferred to M10 (§8 items 16–20):** how many
|
||||||
|
slots; whether they are named; whether a slot presents as a shop or a save
|
||||||
|
file; what guards deletion; and whether the day-length dial surfaces here as a
|
||||||
|
player-facing choice. Recommendations are recorded in the decision queue, but
|
||||||
|
none of these should be settled before the screen exists to judge them
|
||||||
|
against — they are cheap to decide late and awkward to unpick if fixed early.
|
||||||
|
|
||||||
|
**Requires — in place:** M2's save layer (done). Nothing else technically —
|
||||||
|
this could be built any time, and is late only because the design benefits
|
||||||
|
from real content.
|
||||||
|
|
||||||
|
**Produces:** slot selection screen, per-slot summary, delete with confirm,
|
||||||
|
`SAVE_ENTRY` becoming a runtime value rather than the `#define` it is now.
|
||||||
|
|
||||||
|
**Exit:** Three shops can be run in parallel from one disk, each resuming to
|
||||||
|
its own day and cast state, and no single keypress can destroy one.
|
||||||
|
|
||||||
|
### M11 — Ending, polish, hardware
|
||||||
|
|
||||||
|
One quiet ending (see §6). Real-hardware verification, 50/60Hz check, timing
|
||||||
|
pass on the brew minigame.
|
||||||
|
|
||||||
|
**Requires — from you:**
|
||||||
|
- **The ending text**, and what triggers it — my recommendation is a day count
|
||||||
|
reached *after* all four arcs resolve, so it can never cut a story short.
|
||||||
|
- **Real hardware, or the machine profiles you care about.** I can only verify
|
||||||
|
against openMSX here; C-BIOS has already shown behavioural differences from
|
||||||
|
real BIOS during M1, so emulator-passing is not hardware-passing.
|
||||||
|
- **50Hz or 60Hz as the primary target** (PAL vs NTSC timing for the brew
|
||||||
|
minigame's feel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The cast
|
||||||
|
|
||||||
|
`DESIGN.md` §3 suggests six to eight regulars and offers four examples. **Take
|
||||||
|
the four examples as the entire cast.** Reasoning in §6.
|
||||||
|
|
||||||
|
Each has a **want**, a **secret**, and a **change**. The want is stated early.
|
||||||
|
The secret is never announced — it's inferred from behaviour, then eventually
|
||||||
|
confirmed in one quiet line. The change is small and late.
|
||||||
|
|
||||||
|
### Hoshino Aki — the exam student
|
||||||
|
|
||||||
|
- **Usual:** cream soda float. A child's order they're faintly embarrassed by.
|
||||||
|
- **Rhythm:** weekday evenings, then scarcer as exams approach.
|
||||||
|
- **Want:** to pass the Tokyo entrance exams.
|
||||||
|
- **Secret:** the course is their parent's choice. They want to study
|
||||||
|
something else and haven't said so to anyone.
|
||||||
|
- **Change:** orders coffee instead — once, without comment. The player will
|
||||||
|
notice before they understand it.
|
||||||
|
- **Why first:** the arc is legible, the usual is visually distinct from every
|
||||||
|
other order, and the change is expressible entirely through the serve
|
||||||
|
mechanic.
|
||||||
|
|
||||||
|
### Ōta-san — the retired stationmaster
|
||||||
|
|
||||||
|
- **Usual:** black coffee, no sugar. Arrives at the same minute daily.
|
||||||
|
- **Rhythm:** early hours; rain makes him certain to appear.
|
||||||
|
- **Want:** to have somewhere to be at a fixed time.
|
||||||
|
- **Secret:** the line he worked was cut back before he retired. The precision
|
||||||
|
he still keeps is for a timetable that no longer exists.
|
||||||
|
- **Change:** stops apologising for lingering.
|
||||||
|
|
||||||
|
### Fujimoto — the manga artist
|
||||||
|
|
||||||
|
- **Usual:** strongest coffee available, endless refills, toast set when
|
||||||
|
reminded to eat.
|
||||||
|
- **Rhythm:** very late; a monthly deadline cycle drives their frequency and
|
||||||
|
their panic.
|
||||||
|
- **Want:** to finish the chapter.
|
||||||
|
- **Secret:** the series is ending, and they come here to be somewhere that
|
||||||
|
isn't the desk.
|
||||||
|
- **Change:** the shop appears in the manga. They show you the page.
|
||||||
|
|
||||||
|
### Nakajima-san — the widow
|
||||||
|
|
||||||
|
- **Usual:** two coffees. Always two.
|
||||||
|
- **Rhythm:** slow, seasonal, tied to specific weather and dates.
|
||||||
|
- **Want:** to keep a routine that belonged to two people.
|
||||||
|
- **Secret:** the second coffee is his order, and it goes cold every time.
|
||||||
|
- **Change:** one day she orders one. No speech about it.
|
||||||
|
- **Note:** her hook is the strongest in the cast because it's **visible in
|
||||||
|
the serve mechanic before it is ever explained**. The player solves it
|
||||||
|
themselves. Protect that — do not have anyone explain her early.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Story architecture
|
||||||
|
|
||||||
|
### Arc shape
|
||||||
|
|
||||||
|
Twelve to eighteen beats per character. A beat is three lines of 26
|
||||||
|
characters — a *moment*, not a scene.
|
||||||
|
|
||||||
|
| Beats | Function |
|
||||||
|
|---|---|
|
||||||
|
| 1–3 | Establish the routine. Player learns the usual. |
|
||||||
|
| 4–8 | A crack. Mentioned in passing, never explained. |
|
||||||
|
| 9–14 | It develops. Serving choices nudge; they never gate. |
|
||||||
|
| 15+ | Resolution — quiet, undramatic, small. |
|
||||||
|
|
||||||
|
Cozy resolves **gently**. No reveals, no tragedy delivered as a twist. Ōta
|
||||||
|
doesn't confess a catastrophe; he admits the house is too quiet now. Restraint
|
||||||
|
is the genre, and it's also cheaper to write well than melodrama is.
|
||||||
|
|
||||||
|
### Authoring order
|
||||||
|
|
||||||
|
Write **one arc completely** before starting the next. Writing everyone's
|
||||||
|
first three beats produces four strangers and no one you know.
|
||||||
|
|
||||||
|
### Trigger vocabulary
|
||||||
|
|
||||||
|
The scene format is `(conditions) → (portrait, expression, text, effects)`.
|
||||||
|
The condition language is the real design work — keep it minimal:
|
||||||
|
|
||||||
|
```
|
||||||
|
day >= N season == S
|
||||||
|
arc_stage == N weather == W
|
||||||
|
affinity >= N phase == P
|
||||||
|
served == ITEM absent >= N days
|
||||||
|
```
|
||||||
|
|
||||||
|
Eight condition types cover every behaviour described in `DESIGN.md`.
|
||||||
|
**Resist adding a ninth.** Every addition multiplies the authoring surface and
|
||||||
|
the testing burden, and the temptation will be strongest exactly when the
|
||||||
|
writing is hardest.
|
||||||
|
|
||||||
|
### Effects
|
||||||
|
|
||||||
|
Scenes may raise affinity, advance `arc_stage`, set a flag, or unlock an item.
|
||||||
|
Nothing else. No arbitrary state mutation — that path ends in a scripting
|
||||||
|
language nobody asked for.
|
||||||
|
|
||||||
|
### Text and localisation
|
||||||
|
|
||||||
|
Current font is ASCII at 26 characters per line. `DESIGN.md` §9 flags the
|
||||||
|
Japanese retro community as a real slice of the audience.
|
||||||
|
|
||||||
|
**Recommendation:** don't build kana support now, but route every player-facing
|
||||||
|
string through an ID from the start. A Japanese pass then becomes a font and a
|
||||||
|
table swap rather than re-authoring the entire script. This costs almost
|
||||||
|
nothing today and is expensive to retrofit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Budgets
|
||||||
|
|
||||||
|
### ROM — not a constraint
|
||||||
|
|
||||||
|
| Item | Size |
|
||||||
|
|---|---|
|
||||||
|
| Stage 1 code | ~17 KB |
|
||||||
|
| All dialogue text (~8,000 chars) | ~8 KB |
|
||||||
|
| Portraits (4 × 3 × 48×48 @ 4bpp) | ~14 KB |
|
||||||
|
| Background bitmap (256×212 @ 4bpp) | ~27 KB |
|
||||||
|
| Music (6–8 lVGM loops) | ~20 KB |
|
||||||
|
|
||||||
|
Comfortably inside 128 KB with room for a second background or seasonal
|
||||||
|
variants if they ever prove necessary. **ROM size will not be what limits this
|
||||||
|
project.**
|
||||||
|
|
||||||
|
### Writing — the actual constraint
|
||||||
|
|
||||||
|
Four characters × ~15 beats = **~60 scenes**. At three beats per sitting that
|
||||||
|
is twenty writing sessions. This is the single largest line item in the
|
||||||
|
project and it does not compress.
|
||||||
|
|
||||||
|
**Status: a complete first draft exists** in `SCRIPT.md` — all 60 beats, plus
|
||||||
|
order lines, per-character brew reactions and ambient lines, every line
|
||||||
|
checked against the 26-character box. What remains is your edit pass, which is
|
||||||
|
a different and much smaller job than writing from nothing.
|
||||||
|
|
||||||
|
### Art — the long pole
|
||||||
|
|
||||||
|
12 portraits and one background. Tractable at four characters; at eight it
|
||||||
|
roughly doubles and becomes the thing that stalls the project.
|
||||||
|
|
||||||
|
**Open question:** who draws these? The plan assumes 12 portraits exist by M6.
|
||||||
|
If that's commissioned or AI-assisted work, it needs starting well before M6,
|
||||||
|
since it's the only item here with external lead time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Deliberate cuts
|
||||||
|
|
||||||
|
| `DESIGN.md` says | Plan says | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| 6–8 regulars | **4** | 8 × 15 scenes and 24 portraits is where this project dies. Four in a small provincial kissaten is also *more* intimate — the cut serves the atmosphere pillar rather than fighting it. |
|
||||||
|
| Money economy | Minimal until M9 | Gates nothing before the record player. |
|
||||||
|
| "Endings" (plural) | **One** quiet ending | Per-character arc completions already provide the sense of closure. Multiple endings multiply testing for a game with no fail state to contrast against. |
|
||||||
|
| Morning menu planning | Deferred, possibly cut | Adds a decision phase before the loop that carries the game. Revisit only if open hours feel thin. |
|
||||||
|
| Branching dialogue choices | Defer to M4 | M3 will reveal whether they're needed at all. My expectation: rarely. |
|
||||||
|
|
||||||
|
"Small, finishable scope. When in doubt, cut." — `CLAUDE.md`, design pillars.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risks and known hazards
|
||||||
|
|
||||||
|
**Seasons are procrastination.** M7 is the most enjoyable thing on this list —
|
||||||
|
palette work produces beautiful results in minutes with no writing involved.
|
||||||
|
It will call loudly every time the script gets difficult. It also adds nothing
|
||||||
|
to whether the game is *good*. Keep it as the reward after two arcs work.
|
||||||
|
|
||||||
|
**Engine-before-content.** The reason M3 is hardcoded. Dialogue systems get
|
||||||
|
over-built when designed before any dialogue exists; you end up supporting
|
||||||
|
features nothing uses and discovering the one thing you need is awkward.
|
||||||
|
|
||||||
|
**Cast creep.** Four will feel thin around M5, when two arcs exist and the
|
||||||
|
shop seems empty. It won't feel thin at M8. Do not add a fifth regular before
|
||||||
|
all four are complete.
|
||||||
|
|
||||||
|
**`BankedCall = true` is a known hazard.** Enabling it with nothing actually
|
||||||
|
banked corrupted RAM globals — the state machine ran wild. It's off, and M4 is
|
||||||
|
the deliberate point to turn it back on, with the banked data that justifies
|
||||||
|
it. See the platform gotchas in `../CLAUDE.md`.
|
||||||
|
|
||||||
|
**Keyboard reads need interrupt protection.** Documented in `../CLAUDE.md`;
|
||||||
|
applies to every new input added.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Decision queue
|
||||||
|
|
||||||
|
Everything the milestones above need from you, in the order it becomes
|
||||||
|
blocking. Items marked **lead time** should be started well before the
|
||||||
|
milestone that consumes them; items marked **defer** are parked on purpose —
|
||||||
|
captured so the reasoning survives, but not to be decided until their
|
||||||
|
milestone arrives.
|
||||||
|
|
||||||
|
| # | Needed for | Decision | My recommendation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | M2 | ~~Save medium~~ → **save disk (.dsk)** | ✅ working, via MSXgl's `tool/disk_save` module. Saves `KISSAT00.SAV` to drive A. Four non-obvious requirements — see the disk section in `../CLAUDE.md` |
|
||||||
|
| 2 | M2 | ~~Day length~~ → **5 min / 5 customers** | ✅ done. One constant, `CUSTOMERS_PER_DAY` in `kissaten.c` |
|
||||||
|
| 3 | M2 | ~~Clock model~~ → **event-driven** | ✅ done. Customers are the clock |
|
||||||
|
| 4 | M3 | ~~Who writes the dialogue~~ → **drafted by me, you edit** | ✅ resolved |
|
||||||
|
| 5 | M3 | ~~Aki's six beats~~ | ✅ drafted — all four arcs are in `SCRIPT.md`, awaiting your edit |
|
||||||
|
| 6 | M3 | Scratch portrait for Aki | Crude is fine, but *something* — the test needs a face |
|
||||||
|
| 7 | M3 | Item list at slice stage | Coffee + cream soda only |
|
||||||
|
| 8 | M6 | ~~Who makes the art~~ → **PixelLab, from my prompts** | ✅ resolved. Prompts, palette spec and return format in `ART.md`; palette reference images in `art/` |
|
||||||
|
| 9 | M4 | Scene schema sign-off | Ten-minute review, saves a rewrite |
|
||||||
|
| 10 | M4 | String IDs for future JP text | Yes — nearly free now, expensive later |
|
||||||
|
| 11 | M5 | Arrival weights | React to a table I propose |
|
||||||
|
| 12 | M6 | Palette spec to artist **before** drawing | Non-negotiable if M7 is to stay cheap |
|
||||||
|
| 13 | M7 | Number of palette sets | Fewer, art-directed well |
|
||||||
|
| 14 | M8 | How overt Nakajima's reveal gets | Never stated by anyone but her |
|
||||||
|
| 15 | M9 | Sound chip: PSG / FM / retarget | PSG-only via your existing lVGM pipeline |
|
||||||
|
| 16 | M10 · **defer** | How many save slots | 3 — the limit is screen space, not disk room |
|
||||||
|
| 17 | M10 · **defer** | Whether slots are named | Skip naming. Text entry on a machine with no line editor is real work; "Day 34 · 210 cups" identifies a shop better than a filename anyway |
|
||||||
|
| 18 | M10 · **defer** | Slot presentation: shops, or save files | Shops. A slot is somewhere with days behind it, not a numbered file |
|
||||||
|
| 19 | M10 · **defer** | Deletion guard | Two-step confirm. This is the only unrecoverable action in a game with no fail states — the cozy contract applies to the save screen too |
|
||||||
|
| 20 | M10 · **defer** | Day length as a player-facing choice | Ask once when a new shop is started, on the slot screen. One saved byte |
|
||||||
|
| 21 | M11 | Hardware access + 50/60Hz target | — |
|
||||||
|
|
||||||
|
**Deferred items (16–20)** are recorded so the thinking isn't lost, but they
|
||||||
|
are deliberately *not* being decided now. They only bite at M10, and every one
|
||||||
|
of them is easier to judge with real save content on screen than in the
|
||||||
|
abstract. Don't let them pull scope forward.
|
||||||
|
|
||||||
|
With 8 resolved, nothing on the live list is externally blocked — the
|
||||||
|
remaining items are all things I can propose and you can react to.
|
||||||
|
|
||||||
|
## 9. Immediate next actions
|
||||||
|
|
||||||
|
1. ✅ **M2 day spine** — day counter, three phases, journal line. Done and
|
||||||
|
playable. **Persistence still blocked**; see the disk section in
|
||||||
|
`../CLAUDE.md` for everything established so far.
|
||||||
|
2. ✅ **Aki's beats as prose** — done, along with the other three arcs, in
|
||||||
|
`SCRIPT.md`. Awaiting your edit pass.
|
||||||
|
3. ⬜ **M3 vertical slice** — Aki's first six beats, hardcoded, with a scratch
|
||||||
|
portrait. The next real milestone, and the one that decides whether the
|
||||||
|
game works (§2, M3).
|
||||||
|
4. ⬜ **Generate Aki's three portraits** from the prompts in `ART.md`, so M3
|
||||||
|
can be judged against a real face rather than a brown rectangle.
|
||||||
|
|
||||||
|
3 and 4 are independent, and 4 unblocks the honest version of 3.
|
||||||
@@ -0,0 +1,492 @@
|
|||||||
|
# Kissaten Yūgure — Script
|
||||||
|
|
||||||
|
First draft of all four character arcs, plus order lines, brew reactions and
|
||||||
|
ambient lines. Written as prose-first per `PLAN.md` §9 — no data format yet.
|
||||||
|
|
||||||
|
**This is a draft to edit, not to approve.** The characters are yours; if
|
||||||
|
anyone's voice is wrong, it's wrong now and cheap to fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to read this
|
||||||
|
|
||||||
|
Every beat is one dialogue box: **3 lines × 26 characters**, ASCII, as laid
|
||||||
|
out in `DESIGN.md` §4. Every line here has been checked against that limit.
|
||||||
|
|
||||||
|
```
|
||||||
|
**B4 · the crack** — `day>=7` — troubled
|
||||||
|
Mock exam results came.
|
||||||
|
Law faculty. Tokyo.
|
||||||
|
Father framed the sheet.
|
||||||
|
```
|
||||||
|
|
||||||
|
- **B4** — beat number. Beats fire in order; a beat's trigger is *also*
|
||||||
|
gated on the previous beat having played (`arc_stage`).
|
||||||
|
- **Trigger** — conditions from the vocabulary in `PLAN.md` §4.
|
||||||
|
- **Expression** — `neutral` / `happy` / `troubled`. Three per character, per
|
||||||
|
`DESIGN.md` §5.
|
||||||
|
|
||||||
|
Triggers are deliberately loose. Tighten them once pacing is playable —
|
||||||
|
day numbers here assume roughly one visit per character per 2 days.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Hoshino Aki — the exam student
|
||||||
|
|
||||||
|
> Cream soda. Weekday evenings, scarcer as exams approach.
|
||||||
|
> **Want:** pass the Tokyo entrance exams.
|
||||||
|
> **Secret:** the course is her father's choice. She wants astronomy, at a
|
||||||
|
> small college in Sendai with an observatory on the roof.
|
||||||
|
> **Change:** orders coffee instead of the soda.
|
||||||
|
|
||||||
|
The dusk sky through the shop window is her arc's spine — she is the reason
|
||||||
|
that window is there. **Beats 1–6 are the M3 vertical slice.**
|
||||||
|
|
||||||
|
**B1** — `day>=2, phase=evening` — neutral
|
||||||
|
Cream soda, please.
|
||||||
|
The big one, if that's
|
||||||
|
all right.
|
||||||
|
|
||||||
|
**B2** — `day>=3` — neutral
|
||||||
|
Exams in the spring.
|
||||||
|
They say third year goes
|
||||||
|
fast. It hasn't.
|
||||||
|
|
||||||
|
**B3** — `day>=5, served=CREAM_SODA` — happy
|
||||||
|
You remembered.
|
||||||
|
Nobody at home remembers
|
||||||
|
what I like.
|
||||||
|
|
||||||
|
**B4 · the crack** — `day>=7` — troubled
|
||||||
|
Mock exam results came.
|
||||||
|
Law faculty. Tokyo.
|
||||||
|
Father framed the sheet.
|
||||||
|
|
||||||
|
**B5** — `day>=9, weather=rain` — neutral
|
||||||
|
Can't see the sky today.
|
||||||
|
...Sorry. That was a
|
||||||
|
strange thing to say.
|
||||||
|
|
||||||
|
**B6 · slice ends** — `day>=11, served=CREAM_SODA` — neutral
|
||||||
|
Master, do you ever look
|
||||||
|
out that window and think
|
||||||
|
about anything else?
|
||||||
|
|
||||||
|
**B7** — `day>=13` — troubled
|
||||||
|
Father's friend teaches
|
||||||
|
at the Tokyo faculty.
|
||||||
|
It's arranged, basically.
|
||||||
|
|
||||||
|
**B8** — `day>=15, absent>=3` — troubled
|
||||||
|
Sorry I haven't come.
|
||||||
|
Cram school added hours.
|
||||||
|
I missed this place.
|
||||||
|
|
||||||
|
**B9** — `day>=17, affinity>=60` — neutral
|
||||||
|
There's a small college
|
||||||
|
in Sendai. They have an
|
||||||
|
observatory on the roof.
|
||||||
|
|
||||||
|
**B10** — `day>=19` — happy
|
||||||
|
I wrote for a prospectus.
|
||||||
|
It came in a brown
|
||||||
|
envelope. I hid it.
|
||||||
|
|
||||||
|
**B11** — `day>=21, weather=clear, phase=evening` — happy
|
||||||
|
Look - that's Vega.
|
||||||
|
You can see it even from
|
||||||
|
town, if you know where.
|
||||||
|
|
||||||
|
**B12** — `day>=23` — troubled
|
||||||
|
Applications close on
|
||||||
|
the twentieth.
|
||||||
|
I haven't told them.
|
||||||
|
|
||||||
|
**B13 · the change** — `day>=25, served=COFFEE` — neutral
|
||||||
|
Not the soda today.
|
||||||
|
Coffee. Black, like you
|
||||||
|
make for Ota-san.
|
||||||
|
|
||||||
|
**B14** — `day>=27` — troubled
|
||||||
|
I told them.
|
||||||
|
Mother cried. Father
|
||||||
|
hasn't spoken since.
|
||||||
|
|
||||||
|
**B15 · resolution** — `day>=30` — happy
|
||||||
|
Sendai. I posted it.
|
||||||
|
Father drove me to the
|
||||||
|
post office. Said nothing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ōta-san — the retired stationmaster
|
||||||
|
|
||||||
|
> Black coffee, no sugar. Early mornings. Rain makes him certain.
|
||||||
|
> **Want:** somewhere to be at a fixed time.
|
||||||
|
> **Secret:** the ten-fifteen was cut in his last spring. His precision keeps
|
||||||
|
> a timetable that no longer exists.
|
||||||
|
> **Change:** stops apologising for lingering; orders a second cup.
|
||||||
|
|
||||||
|
**B1** — `day>=1, phase=morning` — neutral
|
||||||
|
Coffee. Black.
|
||||||
|
Seven forty. You open
|
||||||
|
at seven forty.
|
||||||
|
|
||||||
|
**B2** — `day>=3, phase=morning` — neutral
|
||||||
|
Your father opened at
|
||||||
|
seven forty as well.
|
||||||
|
I never asked him why.
|
||||||
|
|
||||||
|
**B3** — `day>=5, weather=rain` — neutral
|
||||||
|
Rain. I always came in
|
||||||
|
the rain. The platform
|
||||||
|
had no roof at the end.
|
||||||
|
|
||||||
|
**B4** — `day>=7, served=COFFEE` — happy
|
||||||
|
Thirty-one years I
|
||||||
|
poured tea on that
|
||||||
|
platform. Never coffee.
|
||||||
|
|
||||||
|
**B5 · the crack** — `day>=9` — troubled
|
||||||
|
They cut the ten-fifteen
|
||||||
|
in my last spring.
|
||||||
|
Nobody came to say so.
|
||||||
|
|
||||||
|
**B6** — `day>=11` — neutral
|
||||||
|
I still wake at five.
|
||||||
|
The body keeps a
|
||||||
|
timetable of its own.
|
||||||
|
|
||||||
|
**B7** — `day>=13, absent>=2` — troubled
|
||||||
|
Forgive me. I'm keeping
|
||||||
|
your counter. I'll go.
|
||||||
|
...No. Not yet.
|
||||||
|
|
||||||
|
**B8** — `day>=15, affinity>=50` — neutral
|
||||||
|
The house is not small.
|
||||||
|
That is the trouble.
|
||||||
|
It was sized for four.
|
||||||
|
|
||||||
|
**B9** — `day>=17` — neutral
|
||||||
|
My daughter telephones
|
||||||
|
on Sundays. Eleven a.m.
|
||||||
|
She is very punctual.
|
||||||
|
|
||||||
|
**B10** — `day>=19, weather=rain` — happy
|
||||||
|
You knew I'd come.
|
||||||
|
The pot was already on.
|
||||||
|
That is a fine thing.
|
||||||
|
|
||||||
|
**B11** — `day>=21` — neutral
|
||||||
|
The station is a
|
||||||
|
convenience store now.
|
||||||
|
I have not gone in.
|
||||||
|
|
||||||
|
**B12** — `day>=23` — troubled
|
||||||
|
I went in. For batteries.
|
||||||
|
The ticket window is
|
||||||
|
where the freezers are.
|
||||||
|
|
||||||
|
**B13** — `day>=25, affinity>=80` — neutral
|
||||||
|
You keep good time,
|
||||||
|
Master. That is not a
|
||||||
|
small compliment from me.
|
||||||
|
|
||||||
|
**B14 · the change** — `day>=27` — happy
|
||||||
|
I'll have a second cup.
|
||||||
|
I am not going anywhere
|
||||||
|
in particular today.
|
||||||
|
|
||||||
|
**B15 · resolution** — `day>=30` — happy
|
||||||
|
Seven forty.
|
||||||
|
Tomorrow also, I think.
|
||||||
|
That is all I wanted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fujimoto — the manga artist
|
||||||
|
|
||||||
|
> Strongest coffee, endless refills, toast set when reminded to eat.
|
||||||
|
> Very late. Frequency and panic follow a monthly deadline cycle.
|
||||||
|
> **Want:** finish the chapter.
|
||||||
|
> **Secret:** the series is being cancelled. Comes here to be anywhere that
|
||||||
|
> isn't the desk.
|
||||||
|
> **Change:** the shop appears in the final chapter.
|
||||||
|
|
||||||
|
Fujimoto carries the comedy. A cozy game with four sad people is not cozy —
|
||||||
|
it's a wake. Let them be funny about their own misery.
|
||||||
|
|
||||||
|
**B1** — `day>=2, phase=evening` — neutral
|
||||||
|
Strongest you have.
|
||||||
|
No, stronger. I have
|
||||||
|
eleven pages by Friday.
|
||||||
|
|
||||||
|
**B2** — `day>=4` — troubled
|
||||||
|
Do you know how long
|
||||||
|
eleven pages is?
|
||||||
|
Neither do I anymore.
|
||||||
|
|
||||||
|
**B3** — `day>=6, served=COFFEE` — neutral
|
||||||
|
Refill. And again after.
|
||||||
|
I'll pay for the pot.
|
||||||
|
Cheaper by the hour.
|
||||||
|
|
||||||
|
**B4** — `day>=8` — troubled
|
||||||
|
Editor called me
|
||||||
|
'reliable' today. That's
|
||||||
|
what they say before.
|
||||||
|
|
||||||
|
**B5** — `day>=10, absent>=4` — troubled
|
||||||
|
Four days at the desk.
|
||||||
|
I have drawn one hand.
|
||||||
|
It's a bad hand.
|
||||||
|
|
||||||
|
**B6 · the crack** — `day>=12` — troubled
|
||||||
|
I don't come here to
|
||||||
|
work, Master.
|
||||||
|
I come here to not.
|
||||||
|
|
||||||
|
**B7** — `day>=14, served=TOAST` — happy
|
||||||
|
When did I last eat?
|
||||||
|
Don't answer that.
|
||||||
|
...Thank you for this.
|
||||||
|
|
||||||
|
**B8** — `day>=16` — neutral
|
||||||
|
Thirty-two volumes.
|
||||||
|
Nine years. My whole
|
||||||
|
twenties, in a drawer.
|
||||||
|
|
||||||
|
**B9 · the secret** — `day>=18, affinity>=50` — troubled
|
||||||
|
They're ending it.
|
||||||
|
Two more chapters.
|
||||||
|
I asked for three.
|
||||||
|
|
||||||
|
**B10** — `day>=20` — troubled
|
||||||
|
Nine years and I get
|
||||||
|
two chapters to say
|
||||||
|
goodbye properly.
|
||||||
|
|
||||||
|
**B11** — `day>=22` — neutral
|
||||||
|
I've been drawing this
|
||||||
|
counter. Not for work.
|
||||||
|
Just drawing it.
|
||||||
|
|
||||||
|
**B12** — `day>=24, weather=rain` — neutral
|
||||||
|
Rain's good for pages.
|
||||||
|
Nobody expects you
|
||||||
|
anywhere in the rain.
|
||||||
|
|
||||||
|
**B13 · the change** — `day>=26, affinity>=70` — happy
|
||||||
|
The last chapter is
|
||||||
|
set in a coffee shop.
|
||||||
|
I hope that's all right.
|
||||||
|
|
||||||
|
**B14** — `day>=28` — happy
|
||||||
|
Here. Page forty.
|
||||||
|
That's your counter.
|
||||||
|
That's your lamp.
|
||||||
|
|
||||||
|
**B15 · resolution** — `day>=31` — happy
|
||||||
|
It's done. Nine years.
|
||||||
|
I'm starting something
|
||||||
|
new. Set here, I think.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Nakajima-san — the widow
|
||||||
|
|
||||||
|
> Two coffees. One with milk, one black. Slow, seasonal, weather-tied.
|
||||||
|
> **Want:** keep a routine that belonged to two people.
|
||||||
|
> **Secret:** the second coffee is his. It goes cold every time.
|
||||||
|
> **Change:** one day she orders one.
|
||||||
|
|
||||||
|
**Handle with care.** Her hook is the strongest in the game because the
|
||||||
|
player solves it unaided — the order is visible in the serve mechanic long
|
||||||
|
before anything is said. Per `PLAN.md` §8 item 14: **no one but Nakajima-san
|
||||||
|
ever explains it**, and she does so obliquely, at B4, and never apologises
|
||||||
|
for it. Do not let another character mention her situation.
|
||||||
|
|
||||||
|
Her arc runs on a slower day scale than the others by design — she should
|
||||||
|
feel like someone you see less often.
|
||||||
|
|
||||||
|
**B1** — `day>=4` — neutral
|
||||||
|
Two coffees, please.
|
||||||
|
One with milk.
|
||||||
|
The other black.
|
||||||
|
|
||||||
|
**B2** — `day>=8` — neutral
|
||||||
|
The same, please.
|
||||||
|
You remember which is
|
||||||
|
which. That's good.
|
||||||
|
|
||||||
|
**B3** — `day>=12, served=TWO_COFFEE` — neutral
|
||||||
|
No, don't clear it yet.
|
||||||
|
Leave it a while.
|
||||||
|
I'm not finished.
|
||||||
|
|
||||||
|
**B4 · the crack** — `day>=16, season=autumn` — neutral
|
||||||
|
He liked the autumn.
|
||||||
|
Said the town looked
|
||||||
|
better in brown.
|
||||||
|
|
||||||
|
**B5** — `day>=20` — neutral
|
||||||
|
Forty-one years of
|
||||||
|
two coffees.
|
||||||
|
It's a hard habit.
|
||||||
|
|
||||||
|
**B6** — `day>=24, weather=rain` — troubled
|
||||||
|
He'd have hated today.
|
||||||
|
He walked everywhere.
|
||||||
|
Never once took a bus.
|
||||||
|
|
||||||
|
**B7** — `day>=28, affinity>=40` — neutral
|
||||||
|
Your father served us
|
||||||
|
at that end table.
|
||||||
|
We were young then.
|
||||||
|
|
||||||
|
**B8** — `day>=32` — neutral
|
||||||
|
I know it goes cold.
|
||||||
|
That isn't the point
|
||||||
|
of it, Master.
|
||||||
|
|
||||||
|
**B9** — `day>=36, season=winter` — troubled
|
||||||
|
Three years this month.
|
||||||
|
People stop asking after
|
||||||
|
the second one.
|
||||||
|
|
||||||
|
**B10** — `day>=40` — neutral
|
||||||
|
My son says it's time
|
||||||
|
I stopped. He means
|
||||||
|
well. He means well.
|
||||||
|
|
||||||
|
**B11** — `day>=44, season=spring` — neutral
|
||||||
|
The cherry by the
|
||||||
|
station is out.
|
||||||
|
He'd have said it's early.
|
||||||
|
|
||||||
|
**B12** — `day>=48, affinity>=70` — neutral
|
||||||
|
I don't cry about it.
|
||||||
|
I just like ordering
|
||||||
|
for two. It's an hour.
|
||||||
|
|
||||||
|
**B13** — `day>=52` — neutral
|
||||||
|
You never asked me why.
|
||||||
|
That's why I come here
|
||||||
|
and not the new place.
|
||||||
|
|
||||||
|
**B14 · the change** — `day>=56` — neutral
|
||||||
|
One coffee today.
|
||||||
|
...With milk.
|
||||||
|
Just the one.
|
||||||
|
|
||||||
|
**B15 · resolution** — `day>=60, affinity>=90` — happy
|
||||||
|
One, please.
|
||||||
|
I'll take the window
|
||||||
|
seat. He never liked it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Order lines
|
||||||
|
|
||||||
|
Spoken on arrival, before the brew. Rotate at random within a character;
|
||||||
|
they're texture, not story.
|
||||||
|
|
||||||
|
**Aki**
|
||||||
|
Cream soda, please.
|
||||||
|
The usual, Master.
|
||||||
|
Soda. I've earned it.
|
||||||
|
|
||||||
|
**Ōta-san**
|
||||||
|
Coffee. Black.
|
||||||
|
The usual, if you please.
|
||||||
|
Coffee. No sugar. Ever.
|
||||||
|
|
||||||
|
**Fujimoto**
|
||||||
|
Coffee. Keep it coming.
|
||||||
|
Something that will hurt.
|
||||||
|
Strongest. And toast.
|
||||||
|
|
||||||
|
**Nakajima-san**
|
||||||
|
Two coffees, please.
|
||||||
|
The same as always.
|
||||||
|
Two. Milk in one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Brew reactions
|
||||||
|
|
||||||
|
Replaces the generic quality lines currently hardcoded in `kissaten.c`.
|
||||||
|
Quality bands as implemented: 3 = perfect, 2 = good, 1 = fair, 0 = off
|
||||||
|
(thin or over-strong depending on which side of centre you locked).
|
||||||
|
|
||||||
|
| | Aki | Ōta-san | Fujimoto | Nakajima-san |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **3** | The ice is just right. | That is correct coffee. | Oh, that's cruel. Good. | He'd have liked that one. |
|
||||||
|
| **2** | Mm. That's a good one. | Good. Very good. | That'll do damage. | Very good, Master. |
|
||||||
|
| **1** | Thank you, Master. | Thank you. | Fine. It's wet. | Thank you, dear. |
|
||||||
|
| **0 thin** | It's a bit flat today. | A little pale today. | Is this tea? | A little weak today. |
|
||||||
|
| **0 strong** | Ooh. That's a lot. | Strong. I'll manage. | Perfect. Awful. Perfect. | Oh my. That's bracing. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Ambient lines
|
||||||
|
|
||||||
|
Shown while the shop is empty. The four currently in `kissaten.c` are
|
||||||
|
placeholders; these replace them. Weather- and season-gated where marked.
|
||||||
|
|
||||||
|
**Any time**
|
||||||
|
The radio hums softly.
|
||||||
|
The kettle ticks as it
|
||||||
|
cools.
|
||||||
|
Steam on the window glass.
|
||||||
|
A bicycle bell, outside.
|
||||||
|
The lamp buzzes, once.
|
||||||
|
|
||||||
|
**Rain** — `weather=rain`
|
||||||
|
Rain taps at the window.
|
||||||
|
Someone runs past under
|
||||||
|
a newspaper.
|
||||||
|
The gutter is singing.
|
||||||
|
|
||||||
|
**Clear evening** — `weather=clear, phase=evening`
|
||||||
|
Dusk settles over the
|
||||||
|
street.
|
||||||
|
The sky goes orange, then
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
**Autumn** — `season=autumn`
|
||||||
|
A leaf is stuck to the
|
||||||
|
door.
|
||||||
|
|
||||||
|
**Winter** — `season=winter`
|
||||||
|
The window fogs faster
|
||||||
|
than you can wipe it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Notes and open questions
|
||||||
|
|
||||||
|
**Aki's soda doesn't fit the brew minigame.** The siphon minigame is
|
||||||
|
coffee-specific, but her usual is a cream soda. My recommendation: **soda
|
||||||
|
skips the minigame entirely** — it's poured, not brewed. That's not a
|
||||||
|
workaround, it's an improvement: her visits get a different, lighter rhythm
|
||||||
|
than the three coffee drinkers, which suits a teenager dropping in after cram
|
||||||
|
school. Her B13 switch to coffee then means she enters the minigame for the
|
||||||
|
first time, which is a lovely mechanical echo of the story beat.
|
||||||
|
|
||||||
|
**Nakajima-san's two coffees imply a two-brew serve.** Simplest reading: one
|
||||||
|
minigame, both cups. Making the player brew twice would be tedious and would
|
||||||
|
also make the second cup feel like a chore rather than a small sadness.
|
||||||
|
|
||||||
|
**Cross-references are deliberate.** Aki's B13 names Ōta-san; Ōta's B2 and
|
||||||
|
Nakajima's B7 both reference your father running the shop before you. These
|
||||||
|
cost nothing and make the cast feel like one place rather than four
|
||||||
|
unrelated visitors. Add more of these freely — but never let anyone discuss
|
||||||
|
Nakajima-san's second cup.
|
||||||
|
|
||||||
|
**Nobody dies during the game, nobody is cured.** All four arcs resolve by
|
||||||
|
someone deciding something small. That is the genre working correctly.
|
||||||
|
|
||||||
|
**Beat count is 60.** At three beats per sitting that's the twenty writing
|
||||||
|
sessions budgeted in `PLAN.md` §5 — already spent, if this draft survives
|
||||||
|
editing.
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
// Kissaten Yūgure — stage 1 of the scope ladder (see docs/DESIGN.md §8):
|
||||||
|
// shop scene + one customer + siphon brew minigame + serve loop. No story,
|
||||||
|
// no money, no day cycle yet. All "art" is procedural placeholder fills;
|
||||||
|
// real bitmaps will replace the scene drawing and sprite data later.
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// INCLUDES
|
||||||
|
//=============================================================================
|
||||||
|
#include "msxgl.h"
|
||||||
|
#include "tool/disk_save.h"
|
||||||
|
|
||||||
|
// Fonts data
|
||||||
|
#include "font/font_mgl_sample6.h"
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DEFINES
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
// Screen layout (DESIGN.md §4): 16px status bar, 132px shop scene, 64px dialogue
|
||||||
|
#define STATUS_Y 0
|
||||||
|
#define STATUS_H 16
|
||||||
|
#define SCENE_Y 16
|
||||||
|
#define SCENE_H 132
|
||||||
|
#define DLG_Y 148
|
||||||
|
#define DLG_H 64
|
||||||
|
|
||||||
|
// Palette slots (DESIGN.md §5: sky in 8-11, wood/warm in 4-7, so seasons and
|
||||||
|
// time-of-day can later be palette swaps without touching the bitmap)
|
||||||
|
#define C_OUTLINE 1 // near black
|
||||||
|
#define C_WOOD_D 2 // dark wood / floor / shadow
|
||||||
|
#define C_WOOD_M 3 // mid wood (wall panels, door)
|
||||||
|
#define C_WOOD_L 4 // light wood (counter top)
|
||||||
|
#define C_LAMP 5 // warm lamp glow
|
||||||
|
#define C_CREAM 6 // cream (glassware, cup)
|
||||||
|
#define C_WALL 7 // muted upper wall
|
||||||
|
#define C_SKY1 8 // dusk sky, deep
|
||||||
|
#define C_SKY2 9 // dusk violet
|
||||||
|
#define C_SKY3 10 // dusk rose
|
||||||
|
#define C_SKY4 11 // horizon amber
|
||||||
|
#define C_COFFEE 12 // coffee brown
|
||||||
|
#define C_COAT 13 // customer coat blue
|
||||||
|
#define C_SKIN 14 // skin
|
||||||
|
#define C_TEXT 15 // warm white
|
||||||
|
|
||||||
|
// G4 mode: one byte = two pixels of the same palette index
|
||||||
|
#define PX(c) (u8)(((c) << 4) | (c))
|
||||||
|
|
||||||
|
// Shop scene landmarks
|
||||||
|
#define COUNTER_TOP_Y 98
|
||||||
|
#define DOOR_X 216 // where the customer appears/exits
|
||||||
|
#define SEAT_X 104 // where the customer stands at the counter
|
||||||
|
#define CUST_Y 84 // sprite top: bottom edge rests on the counter top
|
||||||
|
#define SIPHON_X 32 // siphon rig center
|
||||||
|
#define CUP_X 86 // served cup position on the counter
|
||||||
|
|
||||||
|
// Sprites (mode 2 is implicit in SCREEN 5; two layers per customer)
|
||||||
|
#define SPR_CUST_FILL 0 // front layer
|
||||||
|
#define SPR_CUST_LINE 1 // outline layer behind
|
||||||
|
#define SPR_STEAM 2
|
||||||
|
#define PAT_CUST_FILL 0 // 16x16 patterns: indices are multiples of 4
|
||||||
|
#define PAT_CUST_LINE 4
|
||||||
|
#define PAT_STEAM_A 8
|
||||||
|
#define PAT_STEAM_B 12
|
||||||
|
|
||||||
|
// Day length. Event-driven clock: customers *are* the clock, so this single
|
||||||
|
// constant is the whole pacing dial (PLAN.md §8 items 2-3).
|
||||||
|
#define CUSTOMERS_PER_DAY 5
|
||||||
|
|
||||||
|
// Brew minigame gauge (drawn in the dialogue window)
|
||||||
|
#define GAUGE_X 64
|
||||||
|
#define GAUGE_Y 186
|
||||||
|
#define GAUGE_W 148
|
||||||
|
#define GAUGE_H 10
|
||||||
|
#define GAUGE_CENTER (GAUGE_W / 2)
|
||||||
|
|
||||||
|
// Dialogue text layout: 48px portrait zone left, 3 text lines right
|
||||||
|
#define DLG_TEXT_X 64
|
||||||
|
#define DLG_LINE_Y(n) (u8)(156 + (n) * 14)
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// READ-ONLY DATA
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
// Palette: u16 = G<<8 | R<<4 | B, components 0-7
|
||||||
|
#define RGB(r, g, b) (u16)(((g) << 8) | ((r) << 4) | (b))
|
||||||
|
const u16 g_Palette[16] = {
|
||||||
|
RGB(0, 0, 0), // 0 (transparent)
|
||||||
|
RGB(1, 1, 1), // 1 outline
|
||||||
|
RGB(2, 1, 1), // 2 dark wood
|
||||||
|
RGB(4, 2, 1), // 3 mid wood
|
||||||
|
RGB(5, 3, 2), // 4 light wood
|
||||||
|
RGB(7, 5, 2), // 5 lamp glow
|
||||||
|
RGB(7, 6, 4), // 6 cream
|
||||||
|
RGB(3, 2, 2), // 7 muted wall
|
||||||
|
RGB(2, 1, 4), // 8 dusk deep
|
||||||
|
RGB(4, 2, 5), // 9 dusk violet
|
||||||
|
RGB(6, 3, 3), // 10 dusk rose
|
||||||
|
RGB(7, 4, 2), // 11 horizon amber
|
||||||
|
RGB(3, 1, 0), // 12 coffee
|
||||||
|
RGB(2, 3, 5), // 13 coat blue
|
||||||
|
RGB(7, 5, 4), // 14 skin
|
||||||
|
RGB(7, 7, 6), // 15 warm white
|
||||||
|
};
|
||||||
|
|
||||||
|
// 16x16 sprite patterns: 32 bytes each, left column rows 0-15 then right column.
|
||||||
|
// Customer silhouette, fill layer
|
||||||
|
const u8 g_SprCustFill[32] = {
|
||||||
|
0x07, 0x0F, 0x1F, 0x1F, 0x1F, 0x0F, 0x07, 0x1F, // left, rows 0-7
|
||||||
|
0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x1F, 0x0E, 0x0E, // left, rows 8-15
|
||||||
|
0xE0, 0xF0, 0xF8, 0xF8, 0xF8, 0xF0, 0xE0, 0xF8, // right, rows 0-7
|
||||||
|
0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xF8, 0x70, 0x70, // right, rows 8-15
|
||||||
|
};
|
||||||
|
// Customer silhouette dilated by 1px: black outline layer drawn behind the fill
|
||||||
|
const u8 g_SprCustLine[32] = {
|
||||||
|
0x1F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x7F,
|
||||||
|
0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x3F, 0x1F,
|
||||||
|
0xF8, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFE,
|
||||||
|
0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFC, 0xF8,
|
||||||
|
};
|
||||||
|
// Steam wisps, two animation frames
|
||||||
|
const u8 g_SprSteamA[32] = {
|
||||||
|
0x01, 0x03, 0x07, 0x0E, 0x1C, 0x1E, 0x0F, 0x07,
|
||||||
|
0x03, 0x03, 0x07, 0x0E, 0x06, 0x00, 0x00, 0x00,
|
||||||
|
0x80, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||||
|
0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
const u8 g_SprSteamB[32] = {
|
||||||
|
0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x80, 0xC0, 0xE0, 0x70, 0x38, 0x78, 0xF0, 0xE0,
|
||||||
|
0xC0, 0xC0, 0xE0, 0x70, 0x60, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ambient lines shown while the shop is empty
|
||||||
|
const c8* const g_Ambient[4] = {
|
||||||
|
"The radio hums softly.",
|
||||||
|
"Rain taps at the window.",
|
||||||
|
"The kettle ticks as it cools.",
|
||||||
|
"Dusk settles over the street.",
|
||||||
|
};
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// GAME STATE
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
enum State
|
||||||
|
{
|
||||||
|
STATE_WAIT, // shop empty, waiting for the next customer
|
||||||
|
STATE_ARRIVE, // customer walks from door to counter
|
||||||
|
STATE_ORDER, // order shown, waiting for SPACE
|
||||||
|
STATE_BREW, // siphon minigame running
|
||||||
|
STATE_SERVE, // reaction to the brew
|
||||||
|
STATE_DRINK, // quiet moment with the cup
|
||||||
|
STATE_LEAVE, // customer walks back to the door
|
||||||
|
};
|
||||||
|
|
||||||
|
// Day phases (DESIGN.md §2). The open-hours phase holds the STATE_* loop above.
|
||||||
|
enum Phase
|
||||||
|
{
|
||||||
|
PHASE_MORNING, // prep — day title card, waiting to open
|
||||||
|
PHASE_OPEN, // the serve loop runs
|
||||||
|
PHASE_EVENING, // close — tally, journal line, save
|
||||||
|
};
|
||||||
|
|
||||||
|
u8 g_State = STATE_WAIT;
|
||||||
|
u16 g_Timer = 120; // frames left in timed states
|
||||||
|
u8 g_CustX = DOOR_X; // customer sprite X
|
||||||
|
u16 g_Served = 0; // cups served, lifetime
|
||||||
|
u8 g_Ambience = 0; // rotating ambient line index
|
||||||
|
i16 g_Needle = 0; // brew gauge needle position
|
||||||
|
i8 g_NeedleDir = 3;
|
||||||
|
u8 g_Quality = 0; // 0 weak/overdone .. 3 perfect
|
||||||
|
bool g_Overdone = FALSE; // which side of center the lock landed on
|
||||||
|
u8 g_Frame = 0;
|
||||||
|
bool g_SpaceHit = FALSE; // SPACE pressed this frame (edge, sampled after Halt)
|
||||||
|
u8 g_SpacePrev = 0;
|
||||||
|
|
||||||
|
u8 g_Phase = PHASE_MORNING;
|
||||||
|
u8 g_DayServed = 0; // customers served so far today
|
||||||
|
u8 g_DayQuality = 0; // summed brew quality today, for the journal line
|
||||||
|
bool g_SaveOk = TRUE; // did the last write succeed (shown next morning)
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// SAVE STATE (DESIGN.md §6)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
// Bumped whenever the layout below changes; a mismatch is treated as "no save"
|
||||||
|
// rather than migrated — there is no shipped version to be compatible with yet.
|
||||||
|
#define SAVE_MAGIC 0x594B // 'KY'
|
||||||
|
#define SAVE_VERSION 1
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
u8 id;
|
||||||
|
u8 arc_stage; // progress through their story
|
||||||
|
u8 affinity; // 0-255, raised by right orders
|
||||||
|
u8 last_visit_day;
|
||||||
|
} Regular;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
u16 magic;
|
||||||
|
u8 version;
|
||||||
|
u16 day;
|
||||||
|
u8 season; // derives palette set
|
||||||
|
u8 weather; // affects visitor table
|
||||||
|
u16 money;
|
||||||
|
u16 served;
|
||||||
|
u8 unlocked_items; // bitmask: grinder, records, cat...
|
||||||
|
Regular regulars[8];
|
||||||
|
u8 checksum; // sum of all preceding bytes, negated
|
||||||
|
} SaveState;
|
||||||
|
|
||||||
|
SaveState g_Save;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DISK SAVE
|
||||||
|
//=============================================================================
|
||||||
|
//
|
||||||
|
// Uses MSXgl's tool/disk_save module (added in v1.3.0, "save to disk from a
|
||||||
|
// ROM application"). It writes a real named file — KISSAT0.SAV — through the
|
||||||
|
// Disk ROM, so the save disk stays a normal, inspectable FAT disk.
|
||||||
|
//
|
||||||
|
// This replaced a hand-rolled PHYDIO/DSKIO sector writer that never worked:
|
||||||
|
// both entry points returned carry-clear without transferring anything. Do
|
||||||
|
// not go back to raw sectors; use this module.
|
||||||
|
//
|
||||||
|
// Requirements, both non-obvious:
|
||||||
|
// - LibModules must include BOTH "tool/disk_save" and "dos".
|
||||||
|
// - ROMDelayBoot = true, or the Disk ROM's INIT never runs (see CLAUDE.md).
|
||||||
|
|
||||||
|
// Save slot. Multiple save files would be entries 1, 2, ... on the same disk.
|
||||||
|
#define SAVE_ENTRY 0
|
||||||
|
|
||||||
|
bool g_HasDisk = FALSE; // a usable save disk was found at boot
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
u8 Save_Checksum(const SaveState* s)
|
||||||
|
{
|
||||||
|
const u8* p = (const u8*)s;
|
||||||
|
u8 sum = 0;
|
||||||
|
for (u8 i = 0; i < sizeof(SaveState) - 1; i++)
|
||||||
|
sum += p[i];
|
||||||
|
return (u8)(0 - sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// A missing drive or a blank disk is not an error the player has to deal
|
||||||
|
// with — it just means nothing will persist.
|
||||||
|
void Disk_Init()
|
||||||
|
{
|
||||||
|
DiskSave_SetName("KISSAT"); // 6 chars max
|
||||||
|
DiskSave_SetExtension("SAV"); // 3 chars max
|
||||||
|
g_HasDisk = (DiskSave_Initialize() == SAVEDATA_VALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// TRUE if a valid save was loaded; FALSE means "start a new game", which is
|
||||||
|
// also what a blank disk gives us.
|
||||||
|
bool Save_Load()
|
||||||
|
{
|
||||||
|
if (!g_HasDisk)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
// SAVEDATA_UNSIGNED is expected, not an error: with AppSignature on,
|
||||||
|
// DiskSave_Check() wants the file's first 4 bytes to be g_AppSignature,
|
||||||
|
// but DiskSave_Save() writes the payload raw and never adds it — so a
|
||||||
|
// perfectly good file always reports UNSIGNED. Only a genuinely absent or
|
||||||
|
// unreadable file is a reason to bail; the magic and checksum below are a
|
||||||
|
// stronger check than the signature would have been anyway.
|
||||||
|
u8 status = DiskSave_Check(SAVE_ENTRY);
|
||||||
|
if ((status != SAVEDATA_VALID) && (status != SAVEDATA_UNSIGNED))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (!DiskSave_Load(SAVE_ENTRY, (u8*)&g_Save, sizeof(SaveState)))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
// Trust nothing that came off a disk
|
||||||
|
if (g_Save.magic != SAVE_MAGIC || g_Save.version != SAVE_VERSION)
|
||||||
|
return FALSE;
|
||||||
|
if (g_Save.checksum != Save_Checksum(&g_Save))
|
||||||
|
return FALSE;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
bool Save_Write()
|
||||||
|
{
|
||||||
|
if (!g_HasDisk)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
g_Save.magic = SAVE_MAGIC;
|
||||||
|
g_Save.version = SAVE_VERSION;
|
||||||
|
g_Save.checksum = Save_Checksum(&g_Save);
|
||||||
|
|
||||||
|
return DiskSave_Save(SAVE_ENTRY, (const u8*)&g_Save, sizeof(SaveState));
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
void Save_Reset()
|
||||||
|
{
|
||||||
|
Mem_Set(0, &g_Save, sizeof(SaveState));
|
||||||
|
g_Save.day = 1;
|
||||||
|
g_Save.season = 0;
|
||||||
|
g_Save.money = 0;
|
||||||
|
g_Save.served = 0;
|
||||||
|
for (u8 i = 0; i < 8; i++)
|
||||||
|
g_Save.regulars[i].id = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// HELPERS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Customer sprites: fill layer in front, outline layer behind
|
||||||
|
void Customer_Show(u8 x, u8 y)
|
||||||
|
{
|
||||||
|
VDP_SetSpritePosition(SPR_CUST_FILL, x, y);
|
||||||
|
VDP_SetSpritePosition(SPR_CUST_LINE, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Customer_Hide()
|
||||||
|
{
|
||||||
|
VDP_HideSprite(SPR_CUST_FILL);
|
||||||
|
VDP_HideSprite(SPR_CUST_LINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Steam wisp: alternates two patterns every 16 frames
|
||||||
|
void Steam_Show(u8 x, u8 y)
|
||||||
|
{
|
||||||
|
VDP_SetSpriteExUniColor(SPR_STEAM, x, y,
|
||||||
|
(g_Frame & 16) ? PAT_STEAM_A : PAT_STEAM_B, C_CREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Steam_Hide()
|
||||||
|
{
|
||||||
|
VDP_HideSprite(SPR_STEAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Status bar: title left, served count right
|
||||||
|
void DrawStatusBar()
|
||||||
|
{
|
||||||
|
VDP_CommandHMMV(0, STATUS_Y, 256, STATUS_H, PX(C_OUTLINE));
|
||||||
|
VDP_CommandWait();
|
||||||
|
|
||||||
|
Print_SetColor(C_LAMP, C_OUTLINE);
|
||||||
|
Print_SetPosition(4, 4);
|
||||||
|
Print_DrawText("DAY ");
|
||||||
|
Print_DrawInt(g_Save.day);
|
||||||
|
|
||||||
|
// Phase marker sits mid-bar; the clock is the day phase, nothing finer
|
||||||
|
Print_SetColor(C_CREAM, C_OUTLINE);
|
||||||
|
Print_SetPosition(96, 4);
|
||||||
|
switch (g_Phase)
|
||||||
|
{
|
||||||
|
case PHASE_MORNING: Print_DrawText("MORNING"); break;
|
||||||
|
case PHASE_OPEN: Print_DrawText("OPEN"); break;
|
||||||
|
default: Print_DrawText("CLOSED"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print_SetColor(C_TEXT, C_OUTLINE);
|
||||||
|
Print_SetPosition(176, 4);
|
||||||
|
Print_DrawText("SERVED ");
|
||||||
|
Print_DrawInt(g_Save.served);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Shop scene: placeholder fills standing in for the real background bitmap
|
||||||
|
void DrawScene()
|
||||||
|
{
|
||||||
|
// Wall, wood paneling, floor
|
||||||
|
VDP_CommandHMMV(0, SCENE_Y, 256, 60, PX(C_WALL)); // upper wall
|
||||||
|
VDP_CommandHMMV(0, 76, 256, 44, PX(C_WOOD_M)); // wainscot
|
||||||
|
VDP_CommandHMMV(0, 76, 256, 1, PX(C_WOOD_D)); // panel rail
|
||||||
|
VDP_CommandHMMV(0, 120, 256, 28, PX(C_WOOD_D)); // floor
|
||||||
|
VDP_CommandHMMV(0, 120, 256, 1, PX(C_OUTLINE)); // floor edge
|
||||||
|
|
||||||
|
// Window (left): frame + dusk sky gradient in slots 8-11
|
||||||
|
VDP_CommandHMMV(16, 24, 72, 48, PX(C_WOOD_D)); // frame
|
||||||
|
VDP_CommandHMMV(20, 28, 64, 10, PX(C_SKY1));
|
||||||
|
VDP_CommandHMMV(20, 38, 64, 10, PX(C_SKY2));
|
||||||
|
VDP_CommandHMMV(20, 48, 64, 10, PX(C_SKY3));
|
||||||
|
VDP_CommandHMMV(20, 58, 64, 10, PX(C_SKY4));
|
||||||
|
VDP_CommandHMMV(50, 28, 2, 40, PX(C_WOOD_D)); // mullion
|
||||||
|
VDP_CommandHMMV(20, 46, 64, 2, PX(C_WOOD_D)); // transom
|
||||||
|
|
||||||
|
// Door (right)
|
||||||
|
VDP_CommandHMMV(208, 32, 36, 88, PX(C_WOOD_D)); // frame
|
||||||
|
VDP_CommandHMMV(212, 36, 28, 82, PX(C_WOOD_M)); // door
|
||||||
|
VDP_CommandHMMV(218, 40, 16, 18, PX(C_SKY2)); // door pane
|
||||||
|
VDP_CommandHMMV(236, 76, 2, 3, PX(C_LAMP)); // knob
|
||||||
|
|
||||||
|
// Hanging lamp above the counter
|
||||||
|
VDP_CommandHMMV(139, SCENE_Y, 2, 12, PX(C_OUTLINE)); // cord
|
||||||
|
VDP_CommandHMMV(132, 28, 16, 8, PX(C_LAMP)); // shade
|
||||||
|
|
||||||
|
// Counter: top slab + front panel
|
||||||
|
VDP_CommandHMMV(8, COUNTER_TOP_Y, 192, 8, PX(C_WOOD_L));
|
||||||
|
VDP_CommandHMMV(8, COUNTER_TOP_Y + 8, 192, 34, PX(C_WOOD_D));
|
||||||
|
VDP_CommandHMMV(8, COUNTER_TOP_Y, 192, 1, PX(C_CREAM)); // top highlight
|
||||||
|
|
||||||
|
// Siphon rig on the counter (left, kept below the window): stand, globe, flask
|
||||||
|
VDP_CommandHMMV(SIPHON_X + 5, 74, 2, 24, PX(C_OUTLINE)); // stand pole
|
||||||
|
VDP_CommandHMMV(SIPHON_X - 6, 74, 24, 2, PX(C_OUTLINE)); // top arm
|
||||||
|
VDP_CommandHMMV(SIPHON_X - 6, 76, 12, 8, PX(C_CREAM)); // upper globe
|
||||||
|
VDP_CommandHMMV(SIPHON_X - 7, 86, 14, 10, PX(C_CREAM)); // lower flask
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Coffee level in the lower flask, 0-8 px
|
||||||
|
void DrawFlask(u8 level)
|
||||||
|
{
|
||||||
|
VDP_CommandHMMV(SIPHON_X - 6, 87, 12, 8, PX(C_CREAM));
|
||||||
|
if (level > 0)
|
||||||
|
{
|
||||||
|
if (level > 8)
|
||||||
|
level = 8;
|
||||||
|
VDP_CommandHMMV(SIPHON_X - 6, (u16)(95 - level), 12, level, PX(C_COFFEE));
|
||||||
|
}
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Burner flame under the flask (on/off)
|
||||||
|
void DrawFlame(bool on)
|
||||||
|
{
|
||||||
|
VDP_CommandHMMV(SIPHON_X - 3, 96, 6, 2, on ? PX(C_SKY4) : PX(C_WOOD_M));
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Cup on the counter in front of the customer
|
||||||
|
void DrawCup(bool visible)
|
||||||
|
{
|
||||||
|
if (visible)
|
||||||
|
{
|
||||||
|
VDP_CommandHMMV(CUP_X, 91, 10, 7, PX(C_CREAM));
|
||||||
|
VDP_CommandHMMV(CUP_X + 1, 91, 8, 2, PX(C_COFFEE));
|
||||||
|
}
|
||||||
|
else // restore the wainscot the cup stands against, not the counter top
|
||||||
|
VDP_CommandHMMV(CUP_X, 91, 10, 7, PX(C_WOOD_M));
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Dialogue window: dark panel with a portrait placeholder box on the left
|
||||||
|
void Dlg_Clear()
|
||||||
|
{
|
||||||
|
VDP_CommandHMMV(0, DLG_Y, 256, DLG_H, PX(C_WOOD_D)); // border
|
||||||
|
VDP_CommandHMMV(2, DLG_Y + 2, 252, DLG_H - 4, PX(C_OUTLINE)); // panel
|
||||||
|
VDP_CommandHMMV(6, 154, 52, 52, PX(C_WOOD_M)); // portrait frame
|
||||||
|
VDP_CommandHMMV(8, 156, 48, 48, PX(C_WOOD_D)); // portrait (48x48, stage 3+)
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dlg_Line(u8 line, const c8* text)
|
||||||
|
{
|
||||||
|
Print_SetColor(C_TEXT, C_OUTLINE);
|
||||||
|
Print_SetPosition(DLG_TEXT_X, DLG_LINE_Y(line));
|
||||||
|
Print_DrawText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt line in warm accent color
|
||||||
|
void Dlg_Prompt(const c8* text)
|
||||||
|
{
|
||||||
|
Print_SetColor(C_LAMP, C_OUTLINE);
|
||||||
|
Print_SetPosition(DLG_TEXT_X, DLG_LINE_Y(3));
|
||||||
|
Print_DrawText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Brew gauge: zones repainted whole each frame, then the needle on top
|
||||||
|
void DrawGauge()
|
||||||
|
{
|
||||||
|
// Full band first: the needle sticks out 2px above and below the zones
|
||||||
|
VDP_CommandHMMV(GAUGE_X, GAUGE_Y - 2, GAUGE_W + 2, GAUGE_H + 4, PX(C_OUTLINE));
|
||||||
|
VDP_CommandHMMV(GAUGE_X, GAUGE_Y, GAUGE_W, GAUGE_H, PX(C_WOOD_D));
|
||||||
|
VDP_CommandHMMV(GAUGE_X + GAUGE_CENTER - 10, GAUGE_Y, 20, GAUGE_H, PX(C_LAMP));
|
||||||
|
VDP_CommandHMMV(GAUGE_X + GAUGE_CENTER - 3, GAUGE_Y, 6, GAUGE_H, PX(C_SKY4));
|
||||||
|
VDP_CommandHMMV((u16)(GAUGE_X + g_Needle), GAUGE_Y - 2, 2, GAUGE_H + 4, PX(C_TEXT));
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EraseGauge()
|
||||||
|
{
|
||||||
|
VDP_CommandHMMV(GAUGE_X, GAUGE_Y - 2, GAUGE_W + 2, GAUGE_H + 4, PX(C_OUTLINE));
|
||||||
|
VDP_CommandWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// PHASE TRANSITIONS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Morning: day title card. No prep decisions yet — those arrive with the menu
|
||||||
|
// system, if it survives the cut (PLAN.md §6).
|
||||||
|
void EnterMorning()
|
||||||
|
{
|
||||||
|
g_Phase = PHASE_MORNING;
|
||||||
|
g_DayServed = 0;
|
||||||
|
g_DayQuality = 0;
|
||||||
|
Customer_Hide();
|
||||||
|
Steam_Hide();
|
||||||
|
DrawStatusBar();
|
||||||
|
DrawScene();
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, "Morning. The shutters go");
|
||||||
|
Dlg_Line(1, "up, the kettle goes on.");
|
||||||
|
if (!g_HasDisk)
|
||||||
|
Dlg_Line(2, "(no save disk in drive A)");
|
||||||
|
else if (!g_SaveOk)
|
||||||
|
Dlg_Line(2, "(save failed)");
|
||||||
|
Dlg_Prompt("[SPACE] open the shop");
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Evening: tally, one journal line reflecting how the day went, then save.
|
||||||
|
void EnterEvening()
|
||||||
|
{
|
||||||
|
g_Phase = PHASE_EVENING;
|
||||||
|
Customer_Hide();
|
||||||
|
Steam_Hide();
|
||||||
|
DrawStatusBar();
|
||||||
|
Dlg_Clear();
|
||||||
|
|
||||||
|
Dlg_Line(0, "Closing. Cups today: ");
|
||||||
|
Print_DrawInt(g_DayServed);
|
||||||
|
|
||||||
|
// Average quality drives the journal line — no score, just a mood
|
||||||
|
u8 avg = g_DayServed ? (u8)(g_DayQuality / g_DayServed) : 0;
|
||||||
|
if (avg >= 2)
|
||||||
|
Dlg_Line(1, "A good day at the siphon.");
|
||||||
|
else if (avg >= 1)
|
||||||
|
Dlg_Line(1, "An ordinary, decent day.");
|
||||||
|
else
|
||||||
|
Dlg_Line(1, "Tomorrow, a steadier hand.");
|
||||||
|
|
||||||
|
Dlg_Prompt("[SPACE] sleep");
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Save *after* the day rolls over, so the file always describes the morning
|
||||||
|
// the player will wake up to. Saving during the evening instead would make a
|
||||||
|
// reload replay the day just finished and double-count its cups.
|
||||||
|
void AdvanceDay()
|
||||||
|
{
|
||||||
|
g_Save.day++;
|
||||||
|
g_SaveOk = Save_Write();
|
||||||
|
EnterMorning();
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// STATE TRANSITIONS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void EnterWaitState()
|
||||||
|
{
|
||||||
|
g_State = STATE_WAIT;
|
||||||
|
g_Timer = 120 + Math_GetRandomMax8(180);
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, g_Ambient[g_Ambience]);
|
||||||
|
g_Ambience = (g_Ambience + 1) & 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterArrive()
|
||||||
|
{
|
||||||
|
g_State = STATE_ARRIVE;
|
||||||
|
g_CustX = DOOR_X;
|
||||||
|
VDP_SetSpriteExUniColor(SPR_CUST_LINE, DOOR_X, CUST_Y, PAT_CUST_LINE, C_OUTLINE);
|
||||||
|
VDP_SetSpriteExUniColor(SPR_CUST_FILL, DOOR_X, CUST_Y, PAT_CUST_FILL, C_COAT);
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, "The door chime rings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterOrder()
|
||||||
|
{
|
||||||
|
g_State = STATE_ORDER;
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, "\"A coffee, please.\"");
|
||||||
|
Dlg_Prompt("[SPACE] brew");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterBrew()
|
||||||
|
{
|
||||||
|
g_State = STATE_BREW;
|
||||||
|
g_Needle = 0;
|
||||||
|
g_NeedleDir = 3;
|
||||||
|
g_Timer = 0;
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, "Release when the needle");
|
||||||
|
Dlg_Line(1, "crosses the amber mark!");
|
||||||
|
DrawFlame(TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterServe()
|
||||||
|
{
|
||||||
|
g_State = STATE_SERVE;
|
||||||
|
g_Timer = 150;
|
||||||
|
EraseGauge();
|
||||||
|
DrawFlame(FALSE);
|
||||||
|
DrawFlask(0);
|
||||||
|
DrawCup(TRUE);
|
||||||
|
Dlg_Clear();
|
||||||
|
switch (g_Quality)
|
||||||
|
{
|
||||||
|
case 3:
|
||||||
|
Dlg_Line(0, "\"Ah... perfect.\"");
|
||||||
|
Dlg_Line(1, "The evening feels warmer.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
Dlg_Line(0, "\"Mm. Nice and rich.\"");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
Dlg_Line(0, "\"Thank you.\"");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Dlg_Line(0, g_Overdone ? "\"Oh... quite strong.\""
|
||||||
|
: "\"Hm... a little thin.\"");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterDrink()
|
||||||
|
{
|
||||||
|
g_State = STATE_DRINK;
|
||||||
|
g_Timer = 180;
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, "A quiet moment passes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterLeave()
|
||||||
|
{
|
||||||
|
g_State = STATE_LEAVE;
|
||||||
|
Steam_Hide();
|
||||||
|
DrawCup(FALSE);
|
||||||
|
Dlg_Clear();
|
||||||
|
Dlg_Line(0, "\"See you tomorrow.\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// STATE UPDATES (one call per frame)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void UpdateWait()
|
||||||
|
{
|
||||||
|
if (--g_Timer == 0)
|
||||||
|
EnterArrive();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateArrive()
|
||||||
|
{
|
||||||
|
g_CustX--;
|
||||||
|
// Small walk bob
|
||||||
|
Customer_Show(g_CustX, (g_CustX & 8) ? CUST_Y : CUST_Y - 1);
|
||||||
|
if (g_CustX <= SEAT_X)
|
||||||
|
{
|
||||||
|
Customer_Show(SEAT_X, CUST_Y);
|
||||||
|
EnterOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateOrder()
|
||||||
|
{
|
||||||
|
if (g_SpaceHit)
|
||||||
|
EnterBrew();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateBrew()
|
||||||
|
{
|
||||||
|
// Needle sweeps back and forth
|
||||||
|
g_Needle += g_NeedleDir;
|
||||||
|
if (g_Needle <= 0) { g_Needle = 0; g_NeedleDir = 3; }
|
||||||
|
if (g_Needle >= GAUGE_W - 2){ g_Needle = GAUGE_W - 2; g_NeedleDir = -3; }
|
||||||
|
DrawGauge();
|
||||||
|
|
||||||
|
// Coffee slowly rises while the needle sweeps
|
||||||
|
g_Timer++;
|
||||||
|
if ((g_Timer & 15) == 0)
|
||||||
|
DrawFlask((u8)(g_Timer >> 4));
|
||||||
|
Steam_Show(SIPHON_X - 8, 58);
|
||||||
|
|
||||||
|
if (g_SpaceHit)
|
||||||
|
{
|
||||||
|
i16 dist = g_Needle + 1 - GAUGE_CENTER;
|
||||||
|
g_Overdone = (dist > 0);
|
||||||
|
if (dist < 0)
|
||||||
|
dist = -dist;
|
||||||
|
if (dist <= 3) g_Quality = 3;
|
||||||
|
else if (dist <= 10) g_Quality = 2;
|
||||||
|
else if (dist <= 24) g_Quality = 1;
|
||||||
|
else g_Quality = 0;
|
||||||
|
g_DayQuality += g_Quality;
|
||||||
|
EnterServe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateServe()
|
||||||
|
{
|
||||||
|
Steam_Show(CUP_X - 4, 78);
|
||||||
|
if (--g_Timer == 0)
|
||||||
|
EnterDrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateDrink()
|
||||||
|
{
|
||||||
|
Steam_Show(CUP_X - 4, 78);
|
||||||
|
if (--g_Timer == 0)
|
||||||
|
EnterLeave();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateLeave()
|
||||||
|
{
|
||||||
|
g_CustX++;
|
||||||
|
Customer_Show(g_CustX, (g_CustX & 8) ? CUST_Y : CUST_Y - 1);
|
||||||
|
if (g_CustX >= DOOR_X)
|
||||||
|
{
|
||||||
|
Customer_Hide();
|
||||||
|
g_Save.served++;
|
||||||
|
g_DayServed++;
|
||||||
|
DrawStatusBar();
|
||||||
|
|
||||||
|
// Customers are the clock: the day ends when the last one leaves
|
||||||
|
if (g_DayServed >= CUSTOMERS_PER_DAY)
|
||||||
|
EnterEvening();
|
||||||
|
else
|
||||||
|
EnterWaitState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MAIN LOOP
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Program entry point
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
VDP_SetMode(VDP_MODE_SCREEN5);
|
||||||
|
VDP_SetColor(C_OUTLINE);
|
||||||
|
VDP_EnableVBlank(TRUE);
|
||||||
|
VDP_ClearVRAM();
|
||||||
|
|
||||||
|
// Palette (skip entry 0: transparent)
|
||||||
|
for (u8 i = 1; i < 16; i++)
|
||||||
|
VDP_SetPaletteEntry(i, g_Palette[i]);
|
||||||
|
|
||||||
|
// Sprites: 16x16, tables above page 1 (page 1 becomes the asset warehouse later)
|
||||||
|
VDP_EnableSprite(TRUE);
|
||||||
|
VDP_SetSpritePatternTable(0x17000);
|
||||||
|
VDP_SetSpriteAttributeTable(0x17A00);
|
||||||
|
VDP_SetSpriteFlag(VDP_SPRITE_SIZE_16);
|
||||||
|
VDP_LoadSpritePattern(g_SprCustFill, PAT_CUST_FILL, 4);
|
||||||
|
VDP_LoadSpritePattern(g_SprCustLine, PAT_CUST_LINE, 4);
|
||||||
|
VDP_LoadSpritePattern(g_SprSteamA, PAT_STEAM_A, 4);
|
||||||
|
VDP_LoadSpritePattern(g_SprSteamB, PAT_STEAM_B, 4);
|
||||||
|
VDP_HideAllSprites();
|
||||||
|
VDP_DisableSpritesFrom(3);
|
||||||
|
|
||||||
|
Print_SetBitmapFont(g_Font_MGL_Sample6);
|
||||||
|
|
||||||
|
// Explicit game-state init: don't rely on C initializers, the mapper
|
||||||
|
// crt0's data-segment setup has proven unreliable here.
|
||||||
|
g_Served = 0;
|
||||||
|
g_Ambience = 0;
|
||||||
|
g_Frame = 0;
|
||||||
|
g_SpaceHit = FALSE;
|
||||||
|
g_SpacePrev = 1; // treat SPACE as held at boot: no phantom first edge
|
||||||
|
// Save disk. A missing or blank disk is not an error the player has to
|
||||||
|
// deal with — it just means today is day one and nothing will persist.
|
||||||
|
Save_Reset();
|
||||||
|
Disk_Init();
|
||||||
|
Save_Load();
|
||||||
|
|
||||||
|
EnterMorning();
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
Halt(); // Wait V-Blank
|
||||||
|
g_Frame++;
|
||||||
|
|
||||||
|
// Sample input immediately after Halt(): the BIOS ISR drives the PPI
|
||||||
|
// keyboard row selection too, so a read elsewhere in the frame can
|
||||||
|
// race with it and produce phantom edges.
|
||||||
|
// Atomic keyboard read: Keyboard_Read() is a row-select 'out' followed
|
||||||
|
// by an 'in'; if the BIOS ISR lands between them, its own matrix scan
|
||||||
|
// moves the row selection and the read returns garbage (phantom keys).
|
||||||
|
DisableInterrupt();
|
||||||
|
u8 raw = Keyboard_Read(KEY_ROW(KEY_SPACE));
|
||||||
|
EnableInterrupt();
|
||||||
|
u8 space = (raw & KEY_FLAG(KEY_SPACE)) == 0;
|
||||||
|
g_SpaceHit = space && !g_SpacePrev;
|
||||||
|
g_SpacePrev = space;
|
||||||
|
|
||||||
|
// Morning and evening are single-key card screens; only the open
|
||||||
|
// phase runs the serve loop's state machine.
|
||||||
|
if (g_Phase == PHASE_MORNING)
|
||||||
|
{
|
||||||
|
if (g_SpaceHit)
|
||||||
|
{
|
||||||
|
g_Phase = PHASE_OPEN;
|
||||||
|
DrawStatusBar();
|
||||||
|
EnterWaitState();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g_Phase == PHASE_EVENING)
|
||||||
|
{
|
||||||
|
if (g_SpaceHit)
|
||||||
|
AdvanceDay();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (g_State)
|
||||||
|
{
|
||||||
|
case STATE_WAIT: UpdateWait(); break;
|
||||||
|
case STATE_ARRIVE: UpdateArrive(); break;
|
||||||
|
case STATE_ORDER: UpdateOrder(); break;
|
||||||
|
case STATE_BREW: UpdateBrew(); break;
|
||||||
|
case STATE_SERVE: UpdateServe(); break;
|
||||||
|
case STATE_DRINK: UpdateDrink(); break;
|
||||||
|
case STATE_LEAVE: UpdateLeave(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,641 @@
|
|||||||
|
// ____________________________
|
||||||
|
// ██▀▀█▀▀██▀▀▀▀▀▀▀█▀▀█ │ ▄▄▄ ▄▄
|
||||||
|
// ██ ▀ █▄ ▀██▄ ▀ ▄█ ▄▀▀ █ │ ▀█▄ ▄▀██ ▄█▄█ ██▀▄ ██ ▄███
|
||||||
|
// █ █ █ ▀▀ ▄█ █ █ ▀▄█ █▄ │ ▄▄█▀ ▀▄██ ██ █ ██▀ ▀█▄ ▀█▄▄
|
||||||
|
// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀────────┘ ▀▀
|
||||||
|
// Library configuration
|
||||||
|
//─────────────────────────────────────────────────────────────────────────────
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// BUILD
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Target
|
||||||
|
// - TARGET_ROM_8K 8 KB ROM in page 1 (4000h ~ 5FFFh)
|
||||||
|
// - TARGET_ROM_8K_P2 8 KB ROM in page 2 (8000h ~ 9FFFh)
|
||||||
|
// - TARGET_ROM_16K 16 KB ROM in page 1 (4000h ~ 7FFFh)
|
||||||
|
// - TARGET_ROM_16K_P2 16 KB ROM in page 2 (8000h ~ BFFFh)
|
||||||
|
// - TARGET_ROM_32K 32 KB ROM in page 1&2 (4000h ~ BFFFh)
|
||||||
|
// - TARGET_ROM_48K 48 KB ROM in page 0-2 (0000h ~ BFFFh)
|
||||||
|
// - TARGET_ROM_48K_ISR 48 KB ROM in page 0-2 (0000h ~ BFFFh) with ISR replacement
|
||||||
|
// - TARGET_ROM_64K 64 KB ROM in page 0-3 (0000h ~ FFFFh)
|
||||||
|
// - TARGET_ROM_64K_ISR 64 KB ROM in page 0-3 (0000h ~ FFFFh) with ISR replacement
|
||||||
|
// - TARGET_ROM_ASCII8 ASCII-8: 8KB segments for a total of 64 KB to 2 MB
|
||||||
|
// - TARGET_ROM_ASCII16 ASCII-16: 16KB segments for a total of 64 KB to 4 MB
|
||||||
|
// - TARGET_ROM_KONAMI Konami MegaROM (aka Konami4): 8 KB segments for a total of 64 KB to 2 MB
|
||||||
|
// - TARGET_ROM_KONAMI_SCC Konami MegaROM SCC (aka Konami5): 8 KB segments for a total of 64 KB to 2 MB
|
||||||
|
// - TARGET_DOS1 MSX-DOS 1 program (starting at 0100h)
|
||||||
|
// - TARGET_DOS2 MSX-DOS 2 program (starting at 0100h)
|
||||||
|
// - TARGET_DOS0 Direct program boot from disk (starting at 0100h)
|
||||||
|
// - TARGET_BIN BASIC binary program (starting at 8000h)
|
||||||
|
// - TARGET_BIN_USR BASIC USR binary driver (starting at C000h)
|
||||||
|
// TARGET is defined by the build tool
|
||||||
|
|
||||||
|
// MSX version
|
||||||
|
// - MSX_1 ........................ MSX1
|
||||||
|
// - MSX_2 ........................ MSX2
|
||||||
|
// - MSX_12 ....................... MSX1 and 2 (support each)
|
||||||
|
// - MSX_2K ....................... Korean MSX2 (SC9 support)
|
||||||
|
// - MSX_2P ....................... MSX2+
|
||||||
|
// - MSX_22P ...................... MSX2 and 2+ (support each)
|
||||||
|
// - MSX_122P ..................... MSX1, 2 and 2+ (support each)
|
||||||
|
// - MSX_0 ........................ MSX0 (MSX2+)
|
||||||
|
// - MSX_TR ....................... MSX turbo R
|
||||||
|
// - MSX_3 ........................ MSX3
|
||||||
|
// MSX_VERSION is defined by the build tool
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// BIOS MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Default slot BIOS access
|
||||||
|
// - BIOS_CALL_DIRECT ............. Use direct access to Bios routines (ROM slot must be selected in corresponding page)
|
||||||
|
// - BIOS_CALL_INTERSLOT .......... Use inter-slot access to Bios routines (through CALSLT routine)
|
||||||
|
#define BIOS_CALL_MAINROM BIOS_CALL_DIRECT
|
||||||
|
#define BIOS_CALL_SUBROM BIOS_CALL_INTERSLOT
|
||||||
|
#define BIOS_CALL_DISKROM BIOS_CALL_INTERSLOT
|
||||||
|
|
||||||
|
// MAIN-Bios module setting
|
||||||
|
#define BIOS_USE_MAINROM TRUE // Allow use of Main-ROM routines
|
||||||
|
#define BIOS_USE_VDP TRUE // Give access to Main-ROM routines related to VDP
|
||||||
|
#define BIOS_USE_PSG TRUE // Give access to Main-ROM routines related to PSG
|
||||||
|
#define BIOS_USE_SUBROM TRUE // Allow use of Sub-ROM routines (MSX2/2+/turbo R)
|
||||||
|
#define BIOS_USE_DISKROM TRUE // Allow use of Disk-ROM routines
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// VDP MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// VRAM addressing unit
|
||||||
|
// - VDP_VRAM_ADDR_14 ............. Use 14-bits 16K VRAM addressing for MSX1 (u16)
|
||||||
|
// - VDP_VRAM_ADDR_17 ............. Use 17-bits 128K VRAM addressing for MSX2/2+/turbo R (u32)
|
||||||
|
#define VDP_VRAM_ADDR VDP_VRAM_ADDR_17
|
||||||
|
|
||||||
|
// VDP X/Y units
|
||||||
|
// - VDP_UNIT_U8 .................. X and Y use 8-bits values
|
||||||
|
// - VDP_UNIT_X16 ................. X use 16-bits and Y use 8-bits values
|
||||||
|
// - VDP_UNIT_Y16 ................. X use 8-bits and Y use 16-bits values
|
||||||
|
// - VDP_UNIT_U16 ................. X and Y use 16-bits values
|
||||||
|
#define VDP_UNIT VDP_UNIT_X16
|
||||||
|
|
||||||
|
// VDP screen modes (additionnal limitations come from the selected MSX_VERSION)
|
||||||
|
#define VDP_USE_MODE_T1 TRUE // MSX1 Screen 0 Width 40
|
||||||
|
#define VDP_USE_MODE_G1 TRUE // MSX1 Screen 1
|
||||||
|
#define VDP_USE_MODE_G2 TRUE // MSX1 Screen 2
|
||||||
|
#define VDP_USE_MODE_MC TRUE // MSX1 Screen 3
|
||||||
|
#define VDP_USE_MODE_T2 TRUE // MSX2 Screen 0 Width 80
|
||||||
|
#define VDP_USE_MODE_G3 TRUE // MSX2 Screen 4
|
||||||
|
#define VDP_USE_MODE_G4 TRUE // MSX2 Screen 5
|
||||||
|
#define VDP_USE_MODE_G5 TRUE // MSX2 Screen 6
|
||||||
|
#define VDP_USE_MODE_G6 TRUE // MSX2 Screen 7
|
||||||
|
#define VDP_USE_MODE_G7 TRUE // MSX2/2+ Screen 8, 10, 11 & 12
|
||||||
|
|
||||||
|
#define VDP_USE_VRAM16K TRUE // Use 16K VRAM access functions on MSX2
|
||||||
|
#define VDP_USE_SPRITE TRUE // Use sprite handling functions
|
||||||
|
#define VDP_USE_COMMAND TRUE // Use VDP commands wrapper functions
|
||||||
|
#define VDP_USE_CUSTOM_CMD FALSE // Use custom VDP commands through data buffer
|
||||||
|
#define VDP_AUTO_INIT TRUE // Call VDP_Initialize() at the first call to VDP_SetMode()
|
||||||
|
#define VDP_USE_UNDOCUMENTED TRUE // Allow the use of undocumented screen mode (WIP)
|
||||||
|
#define VDP_USE_VALIDATOR TRUE // Handle some option specific for each VDP mode (highly recommended)
|
||||||
|
#define VDP_USE_DEFAULT_PALETTE FALSE // Add data for default MSX2 palette
|
||||||
|
#define VDP_USE_MSX1_PALETTE FALSE // Add data for default MSX1 palette
|
||||||
|
#define VDP_USE_DEFAULT_SETTINGS TRUE // Auto-initialization of common VDP feature
|
||||||
|
#define VDP_USE_16X16_SPRITE TRUE // Use 16x16 sprites mode
|
||||||
|
#define VDP_USE_RESTORE_S0 TRUE // Do restore of status register pointer to S#0 (needed onlt for default BIOS ISR)
|
||||||
|
#define VDP_USE_PALETTE16 FALSE // Use 16 entries palette (use only 15 entries otherwise)
|
||||||
|
|
||||||
|
// ISR protection while modifiying VDP registers
|
||||||
|
// - VDP_ISR_SAFE_NONE ............ No ISR protection (for program not using VDP interruption)
|
||||||
|
// - VDP_ISR_SAFE_DEFAULT ......... Protect only VDP register pair writing (default behavior; ISR can read/write registers but VRAM ones)
|
||||||
|
// - VDP_ISR_SAFE_ALL ............. Protect all VDP writing process
|
||||||
|
#define VDP_ISR_SAFE_MODE VDP_ISR_SAFE_DEFAULT
|
||||||
|
|
||||||
|
// Initial screen mode setting
|
||||||
|
// - VDP_INIT_OFF ................. Force option to be disable
|
||||||
|
// - VDP_INIT_ON .................. Force option to be enable
|
||||||
|
// - VDP_INIT_AUTO ................ Determining the best value for the context
|
||||||
|
// - VDP_INIT_DEFAULT ............. Keep default value
|
||||||
|
#define VDP_INIT_50HZ VDP_INIT_DEFAULT
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// V9990 MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// V9990 screen modes support
|
||||||
|
#define V9_USE_MODE_P1 TRUE // Tile mode 0 256x212
|
||||||
|
#define V9_USE_MODE_P2 TRUE // Tile mode 1 512x212
|
||||||
|
#define V9_USE_MODE_B0 TRUE // Bitmap mode 1 192x240 (Undocumented v9990 mode)
|
||||||
|
#define V9_USE_MODE_B1 TRUE // Bitmap mode 1 256x212
|
||||||
|
#define V9_USE_MODE_B2 TRUE // Bitmap mode 2 384x240
|
||||||
|
#define V9_USE_MODE_B3 TRUE // Bitmap mode 3 512x212
|
||||||
|
#define V9_USE_MODE_B4 TRUE // Bitmap mode 4 768x240
|
||||||
|
#define V9_USE_MODE_B5 TRUE // Bitmap mode 5 640x400 (VGA)
|
||||||
|
#define V9_USE_MODE_B6 TRUE // Bitmap mode 6 640x480 (VGA)
|
||||||
|
#define V9_USE_MODE_B7 TRUE // Bitmap mode 7 1024x212 (Undocumented v9990 mode)
|
||||||
|
|
||||||
|
#define V9_INT_PROTECT TRUE // VRAM access protection mode against interruption
|
||||||
|
// Palette input data format
|
||||||
|
// - V9_PALETTE_YSGBR_16 .......... 16 bits RGB + Ys [Ys|G|G|G|G|G|R|R] [R|R|R|B|B|B|B|B]
|
||||||
|
// - V9_PALETTE_GBR_16 ............ 16 bits RGB [0|G|G|G|G|G|R|R] [R|R|R|B|B|B|B|B]
|
||||||
|
// - V9_PALETTE_RGB_24 ............ 24 bits RGB [0|0|0|R|R|R|R|R] [0|0|0|G|G|G|G|G] [0|0|0|B|B|B|B|B]
|
||||||
|
#define V9_PALETTE_MODE V9_PALETTE_RGB_24
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// INPUT MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Input module setting
|
||||||
|
#define INPUT_USE_JOYSTICK TRUE // Add functions to handle joystick using I/O port
|
||||||
|
#define INPUT_USE_KEYBOARD TRUE // Add functions to handle keyboard using I/O port
|
||||||
|
#define INPUT_USE_MOUSE TRUE // Add support for Mouse handling functions
|
||||||
|
#define INPUT_USE_DETECT TRUE // Add feature to detect device plugged in General purpose ports
|
||||||
|
#define INPUT_USE_ISR_PROTECTION TRUE // Disable interruptions while access PSG registers (needed if you use BIOS or access PSG in your own ISR)
|
||||||
|
#define INPUT_JOY_UPDATE FALSE // Add function to update all joystick states at once
|
||||||
|
#define INPUT_HOLD_SIGNAL FALSE // Determines whether functions that modify signals should keep the state of those they don't need to modify (which slows functions down a bit)
|
||||||
|
// Key update handler
|
||||||
|
// Keep FALSE: the buffered mode stores state in the BIOS NEWKEY/OLDKEY work
|
||||||
|
// area, which C-BIOS does not maintain in the standard matrix format (phantom
|
||||||
|
// key edges). Direct reads are safe if sampled right after Halt() — see main().
|
||||||
|
#define INPUT_KB_UPDATE FALSE // Add function to update all keyboard rows at once
|
||||||
|
#define INPUT_KB_UPDATE_MIN 0 // First row to update
|
||||||
|
#define INPUT_KB_UPDATE_MAX 8 // Last row to update (10 for numerical-pad, 8 otherwise)
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// PADDLE MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Paddle module setting
|
||||||
|
#define PADDLE_USE_CALIB TRUE // Add functions paddle calibration feature
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// MEMORY MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define MEM_USE_VALIDATOR FALSE // Activate validator to handle invalide input value
|
||||||
|
#define MEM_USE_FASTCOPY FALSE // Add support for fast-copy function (using unrolled-LDI loop)
|
||||||
|
#define MEM_USE_FASTSET FALSE // Add support for fast-set function (using unrolled-LDI loop)
|
||||||
|
#define MEM_USE_DYNAMIC FALSE // Add support for malloc style dynamic allocator
|
||||||
|
#define MEM_USE_BUILTIN TRUE // Use SDCC built-in memcpy and memset function instead of MSXgl ones
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// MSX-DOS MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define DOS_USE_FCB TRUE // Add support for file managment features through FCB structure
|
||||||
|
#define DOS_USE_HANDLE TRUE // Add support for file managment features through file handle
|
||||||
|
#define DOS_USE_UTILITIES TRUE // Add support for file managment features through filename
|
||||||
|
#define DOS_USE_VALIDATOR TRUE // Add support for last error backup and return value validation
|
||||||
|
#define DOS_USE_ERROR_HANDLER TRUE // Add support for MSX-DOS 1 error handler callback
|
||||||
|
#define DOS_USE_BIOSCALL TRUE // Add support for call to BIOS routines
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// CLOCK MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define RTC_USE_CLOCK TRUE // Add functions to handle date and time
|
||||||
|
#define RTC_USE_CLOCK_EXTRA TRUE // Add extra date and time functions that require additional data
|
||||||
|
#define RTC_USE_SAVEDATA TRUE // Add functions to read/write into the CMOS
|
||||||
|
#define RTC_USE_SAVESIGNED TRUE // Add signature handling to validate CMOS I/O
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// DRAW MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// PRINT MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Print module setting
|
||||||
|
#define PRINT_USE_TEXT TRUE // Allow use of Text font (T1-T2, G1-G3)
|
||||||
|
#define PRINT_USE_BITMAP TRUE // Allow use of Bitmap font (G4-G7)
|
||||||
|
#define PRINT_USE_VRAM TRUE // Allow use of VRAM stored font (G4-G7)
|
||||||
|
#define PRINT_USE_SPRITE TRUE // Allow use of Sprite font (G3-G7)
|
||||||
|
#define PRINT_USE_FX_SHADOW TRUE // [Bitmap] Allow use of text shadow
|
||||||
|
#define PRINT_USE_FX_OUTLINE TRUE // [Bitmap] Allow use of text outline
|
||||||
|
#define PRINT_USE_2_PASS_FX FALSE // [Bitmap] Allow use 2-pass FX render to prevent character overlap
|
||||||
|
#define PRINT_USE_GRAPH TRUE // Allow use of character lines and boxes
|
||||||
|
#define PRINT_USE_VALIDATOR TRUE // Add validator character code
|
||||||
|
#define PRINT_USE_UNIT FALSE // Display integer type (h: hexadecimal, b: binary)
|
||||||
|
#define PRINT_USE_FORMAT TRUE // Add printf type function
|
||||||
|
#define PRINT_USE_32B TRUE // Allow to print 32-bits integers
|
||||||
|
#define PRINT_USE_MULTIFONT FALSE // Use multiple fonts (each one with its own data structure)
|
||||||
|
#define PRINT_SKIP_SPACE TRUE // Skill space character
|
||||||
|
#define PRINT_COLOR_NUM 12 // 1 color per line
|
||||||
|
// Character width
|
||||||
|
// - PRINT_WIDTH_1 (text mode)
|
||||||
|
// - PRINT_WIDTH_6
|
||||||
|
// - PRINT_WIDTH_8
|
||||||
|
// - PRINT_WIDTH_X (variable)
|
||||||
|
#define PRINT_WIDTH PRINT_WIDTH_X
|
||||||
|
// Character height
|
||||||
|
// - PRINT_HEIGHT_1 (text mode)
|
||||||
|
// - PRINT_HEIGHT_8
|
||||||
|
// - PRINT_HEIGHT_X (variable)
|
||||||
|
#define PRINT_HEIGHT PRINT_HEIGHT_X
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// SPRITE FX MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Supported sprite size
|
||||||
|
#define SPRITEFX_USE_8x8 TRUE // Use 8x8 pixels effects
|
||||||
|
#define SPRITEFX_USE_16x16 TRUE // Use 16x16 pixels effects
|
||||||
|
|
||||||
|
// Supported effect
|
||||||
|
#define SPRITEFX_USE_CROP TRUE // Use cropping effect
|
||||||
|
#define SPRITEFX_USE_FLIP TRUE // Use flipping effect
|
||||||
|
#define SPRITEFX_USE_MASK TRUE // Use masking effect
|
||||||
|
#define SPRITEFX_USE_ROTATE TRUE // Use rotating effect
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// GAME MAIN MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Game state setting
|
||||||
|
#define GAME_USE_STATE TRUE // Add state machine features
|
||||||
|
#define GAME_USE_VSYNC TRUE // Add vertical synchronization features
|
||||||
|
#define GAME_USE_LOOP TRUE // Add game main loop with call to v-synch and state
|
||||||
|
#define GAME_USE_SYNC_50HZ TRUE // Force 50Hz synchronization on 60Hz machine
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// GAME PAWN MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Pawn setting
|
||||||
|
#define PAWN_ID_PER_LAYER FALSE // Set sprite ID for each layer (otherwise set per pawn)
|
||||||
|
#define PAWN_USE_RT_LOAD TRUE // Load sprite pattern data on the fly (real-time)
|
||||||
|
#define PAWN_USE_SPRT_FX TRUE // Allow sprite effects (crop, flip, mask, rotate)
|
||||||
|
#define PAWN_SPRITE_SIZE 16 // Sprite size mode (8 for 8x8 pixel mode, or 16 for 16x16)
|
||||||
|
#define PAWN_BLEND_OFFSET 12 // Sprite pattern offset for blending mode
|
||||||
|
#define PAWN_USE_PHYSICS TRUE // Add physics and collision features
|
||||||
|
// Pawn coordinate unit
|
||||||
|
// - PAWN_UNIT_SCREEN Default screen (pixel) unit (8-bit unsigned int)
|
||||||
|
// - PAWN_UNIT_QMN(n) Fixed-point (Qm.n) unit (16-bit signed int)
|
||||||
|
#define PAWN_UNIT PAWN_UNIT_SCREEN
|
||||||
|
// Pawn's bound (can be fixed for all pawn, or setable for each one)
|
||||||
|
#define PAWN_BOUND_X PAWN_BOUND_CUSTOM
|
||||||
|
#define PAWN_BOUND_Y PAWN_BOUND_CUSTOM
|
||||||
|
// Collision position options for each pawn's side
|
||||||
|
// - PAWN_COL_0
|
||||||
|
// - PAWN_COL_25
|
||||||
|
// - PAWN_COL_50
|
||||||
|
// - PAWN_COL_75
|
||||||
|
// - PAWN_COL_100
|
||||||
|
#define PAWN_COL_DOWN (PAWN_COL_25|PAWN_COL_75)
|
||||||
|
#define PAWN_COL_UP PAWN_COL_50
|
||||||
|
#define PAWN_COL_RIGHT PAWN_COL_50
|
||||||
|
#define PAWN_COL_LEFT PAWN_COL_50
|
||||||
|
// Options to determine which border collide or trigger events
|
||||||
|
// - PAWN_BORDER_NONE
|
||||||
|
// - PAWN_BORDER_DOWN
|
||||||
|
// - PAWN_BORDER_UP
|
||||||
|
// - PAWN_BORDER_RIGHT
|
||||||
|
// - PAWN_BORDER_LEFT
|
||||||
|
#define PAWN_BORDER_EVENT (PAWN_BORDER_UP|PAWN_BORDER_DOWN|PAWN_BORDER_LEFT|PAWN_BORDER_RIGHT)
|
||||||
|
#define PAWN_BORDER_BLOCK (PAWN_BORDER_UP|PAWN_BORDER_DOWN|PAWN_BORDER_LEFT|PAWN_BORDER_RIGHT)
|
||||||
|
// Top/bottom border position (in pixel)
|
||||||
|
#define PAWN_BORDER_MIN_Y 0 // High border Y coordinade
|
||||||
|
#define PAWN_BORDER_MAX_Y 211 // Low border Y coordinate
|
||||||
|
#define PAWN_TILEMAP_WIDTH 32 // Width of the tiles map
|
||||||
|
#define PAWN_TILEMAP_HEIGHT 27 // Height of the tiles map
|
||||||
|
// Collision tilemap source
|
||||||
|
// - PAWN_TILEMAP_SRC_AUTO ........ Backward compatibility option
|
||||||
|
// - PAWN_TILEMAP_SRC_RAM ......... Tilemap located in a buffer in RAM (best for performance)
|
||||||
|
// - PAWN_TILEMAP_SRC_VRAM ........ Tilemap located in VRAM (slow but don't need additionnal data)
|
||||||
|
// - PAWN_TILEMAP_SRC_V9 .......... Tilemap located in V9990's VRAM
|
||||||
|
#define PAWN_TILEMAP_SRC PAWN_TILEMAP_SRC_RAM
|
||||||
|
// Pawn's sprite mode
|
||||||
|
// - PAWN_SPT_MODE_AUTO ........... Backward compatibility option
|
||||||
|
// - PAWN_SPT_MODE_MSX1 ........... Sprite Mode 1 (MSX1 screens)
|
||||||
|
// - PAWN_SPT_MODE_MSX2 ........... Sprite Mode 2 unicolor (MSX2 screens)
|
||||||
|
// - PAWN_SPT_MODE_MSX2_MULTI ..... Sprite Mode 2 multi-color (MSX2 screens)
|
||||||
|
// - PAWN_SPT_MODE_MSX12 .......... Sprite Mode 1 & 2 unicolor (MSX1 & MSX2 screens)
|
||||||
|
// - PAWN_SPT_MODE_V9_P1 .......... V9990 sprite in P1 mode
|
||||||
|
// - PAWN_SPT_MODE_V9_P2 .......... V9990 sprite in P2 mode
|
||||||
|
#define PAWN_SPT_MODE PAWN_SPT_MODE_MSX2
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// GAME MENU MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define MENU_USE_DEFAULT_CALLBACK TRUE // Use default input/print callback
|
||||||
|
#define MENU_SCREEN_WIDTH MENU_VARIABLE // Screen width
|
||||||
|
#define MENU_FRAME_X 0 // Frame position X
|
||||||
|
#define MENU_FRAME_Y 6 // Frame position Y
|
||||||
|
#define MENU_FRAME_WIDTH 32 // Frame width
|
||||||
|
#define MENU_FRAME_HEIGHT 8 // Frame height
|
||||||
|
#define MENU_CHAR_CLEAR '\0' // Clear character
|
||||||
|
#define MENU_CHAR_CURSOR '@' // Cursor character
|
||||||
|
#define MENU_CHAR_TRUE 'O' // True character
|
||||||
|
#define MENU_CHAR_FALSE 'X' // False character
|
||||||
|
#define MENU_CHAR_LEFT '<' // Left edit character
|
||||||
|
#define MENU_CHAR_RIGHT '>' // Right edit character
|
||||||
|
#define MENU_TITLE_X 4 // Title position X
|
||||||
|
#define MENU_TITLE_Y 6 // Title position Y
|
||||||
|
#define MENU_ITEM_X 6 // Item label X position
|
||||||
|
#define MENU_ITEM_Y 8 // Item label X position
|
||||||
|
#define MENU_ITEM_X_GOTO 6 // Goto type item label X position
|
||||||
|
#define MENU_ITEM_ALIGN MENU_ITEM_ALIGN_LEFT // Item label alignment
|
||||||
|
#define MENU_ITEM_ALIGN_GOTO MENU_ITEM_ALIGN_LEFT // Goto type item label alignment
|
||||||
|
#define MENU_VALUE_X 14 // Item value X position
|
||||||
|
// Type of cursor
|
||||||
|
// - MENU_CURSOR_MODE_NONE ........ No cursor
|
||||||
|
// - MENU_CURSOR_MODE_CHAR ........ Character cursor
|
||||||
|
// - MENU_CURSOR_MODE_SPRT ........ Sprite cursor
|
||||||
|
#define MENU_CURSOR_MODE MENU_CURSOR_MODE_CHAR
|
||||||
|
#define MENU_CURSOR_OFFSET (-2) // Cursor X position offset
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// GAME QUEST VARIABLES MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define BITFIELD_MAX 64 // Maximum number of quest variables (8 variables per byte)
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// STRING MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Support for integer to ASCII string converter functions
|
||||||
|
#define STRING_USE_FROM_INT8 TRUE // Signed 8-bits integer to string
|
||||||
|
#define STRING_USE_FROM_UINT8 TRUE // Unsigned 8-bits integer to string
|
||||||
|
#define STRING_USE_FROM_INT16 TRUE // Signed 16-bits integer to string
|
||||||
|
#define STRING_USE_FROM_UINT16 TRUE // Unsigned 16-bits integer to string
|
||||||
|
|
||||||
|
// Support for sprintf style formating function
|
||||||
|
#define STRING_USE_FORMAT TRUE
|
||||||
|
#define STRING_USE_INT32 TRUE // Add support for 32-bits integer
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// SCROLL MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Allow horizontal and/or vertical scrolling
|
||||||
|
#define SCROLL_HORIZONTAL TRUE // Activate horizontal scrolling
|
||||||
|
#define SCROLL_VERTICAL TRUE // Activate vertical scrolling
|
||||||
|
// Source data info
|
||||||
|
#define SCROLL_SRC_X 64 // Start X coordinate of the source data
|
||||||
|
#define SCROLL_SRC_Y 0 // Start Y coordinate of the source data
|
||||||
|
#define SCROLL_SRC_W 128 // Width of the source data
|
||||||
|
#define SCROLL_SRC_H 24 // Height of the source data
|
||||||
|
// Destination data info
|
||||||
|
#define SCROLL_DST_X 0 // Destination x coordinate (in layout table)
|
||||||
|
#define SCROLL_DST_Y 2 // Destination y coordinate (in layout table)
|
||||||
|
#define SCROLL_DST_W 32 // Destination width
|
||||||
|
#define SCROLL_DST_H 20 // Destination height
|
||||||
|
#define SCROLL_SCREEN_W 32 // Screen width in tile number
|
||||||
|
// Allow scroll data looping (only for horizontal scrolling)
|
||||||
|
#define SCROLL_WRAP TRUE
|
||||||
|
// Use screen position adjust register (allow per-pixel scrolling) [MSX2]
|
||||||
|
#define SCROLL_ADJUST TRUE // Global ajustement
|
||||||
|
#define SCROLL_ADJUST_SPLIT TRUE // Destination windows ajustement using screen split
|
||||||
|
// Use sprite mask (allow smooth per-pixel scrolling) [MSX2]
|
||||||
|
#define SCROLL_MASK TRUE // Use sprite to mask
|
||||||
|
#define SCROLL_MASK_ID 0 // First sprite ID to use
|
||||||
|
#define SCROLL_MASK_COLOR COLOR_BLACK // Must be the same than border color
|
||||||
|
#define SCROLL_MASK_PATTERN 0 // Sprite pattern to use
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// TILE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define TILE_WIDTH 8 // Tile width
|
||||||
|
#define TILE_HEIGHT 8 // Tile height
|
||||||
|
#define TILE_BPP 4 // Screen bits-per-pixel
|
||||||
|
#define TILE_SCREEN_WIDTH 256 // Screen width
|
||||||
|
#define TILE_SCREEN_HEIGHT 212 // Screen height
|
||||||
|
#define TILE_USE_SKIP TRUE // Skip drawing of a given index
|
||||||
|
#define TILE_SKIP_INDEX 0 // The index tile to skip
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// AUDIO
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// PSG options
|
||||||
|
// - PSG_INTERNAL ................. Use internal PSG chip (port A0-A2)
|
||||||
|
// - PSG_EXTERNAL ................. Use external PSG chip (port 10-12)
|
||||||
|
// - PSG_BOTH ..................... Use both internal and external PSG chips
|
||||||
|
#define PSG_CHIP PSG_INTERNAL
|
||||||
|
// - PSG_DIRECT ................... Function set directly the PSG registers
|
||||||
|
// - PSG_INDIRECT ................. Function set a buffer (Apply() function must be call once a frame)
|
||||||
|
#define PSG_ACCESS PSG_INDIRECT
|
||||||
|
#define PSG_USE_NOTES FALSE // Add notes table to convert note to tone
|
||||||
|
#define PSG_USE_EXTRA TRUE // Add helper functions to handle PSG settings
|
||||||
|
#define PSG_USE_RESUME TRUE // Add function to allow playback pause and resume
|
||||||
|
|
||||||
|
// MSX-Music options
|
||||||
|
#define MSXAUDIO_USE_RESUME TRUE // Add function to allow playback pause and resume
|
||||||
|
|
||||||
|
// MSX-Audio options
|
||||||
|
#define MSXMUSIC_USE_RESUME TRUE // Add function to allow playback pause and resume
|
||||||
|
|
||||||
|
// SCC options
|
||||||
|
#define SCC_USE_EXTA TRUE // Add helper functions to handle PSG settings
|
||||||
|
#define SCC_USE_RESUME TRUE // Add function to allow playback pause and resume
|
||||||
|
// - SCC_SLOT_DIRECT .............. Program on a SCC cartridge
|
||||||
|
// - SCC_SLOT_FIXED ............... Fixed slot-id (non-expanded second cartridge slot)
|
||||||
|
// - SCC_SLOT_USER ................ Defined by the user
|
||||||
|
// - SCC_SLOT_AUTO ................ First auto-detected cartridge
|
||||||
|
#define SCC_SLOT_MODE SCC_SLOT_AUTO
|
||||||
|
|
||||||
|
// VGM supported chip
|
||||||
|
#define VGM_USE_PSG TRUE // Allow PSG data parsing and audio output
|
||||||
|
#define VGM_USE_MSXMUSIC TRUE // Allow MSX-Music data parsing and audio output
|
||||||
|
#define VGM_USE_MSXAUDIO TRUE // Allow MSX-Audio data parsing and audio output
|
||||||
|
#define VGM_USE_SCC TRUE // Allow SCC data parsing and audio output
|
||||||
|
|
||||||
|
// PCM-Encoder supported frequency (more than 1 value allowed)
|
||||||
|
// - PCMENC_NONE, PCMENC_8K, PCMENC_11K, PCMENC_22K and PCMENC_44K
|
||||||
|
#define PCMENC_FREQ PCMENC_8K | PCMENC_11K | PCMENC_22K | PCMENC_44K
|
||||||
|
|
||||||
|
// PCMPlay
|
||||||
|
// - PCMPLAY_8K or PCMPLAY_11K
|
||||||
|
#define PCMPLAY_FREQ PCMPLAY_8K
|
||||||
|
#define PCMPLAY_USE_RESTORE FALSE
|
||||||
|
|
||||||
|
// PT3 options
|
||||||
|
#define PT3_SKIP_HEADER TRUE // Don't use PT3 data header (first 100 bytes must be truncated)
|
||||||
|
#define PT3_AUTOPLAY TRUE // Play music automatically
|
||||||
|
#define PT3_EXTRA TRUE // Add helper functions
|
||||||
|
|
||||||
|
// ayFX options
|
||||||
|
// - AYFX_BUFFER_DEFAULT .......... Use PSG module's PSG registers buffer
|
||||||
|
// - AYFX_BUFFER_PSG2 ............. Use PSG module's 2nd PSG registers buffer (only if PSG_CHIP == PSG_BOTH)
|
||||||
|
// - AYFX_BUFFER_PT3 .............. Use PT3 module's PSG registers buffer
|
||||||
|
#define AYFX_BUFFER AYFX_BUFFER_PT3
|
||||||
|
|
||||||
|
// TriloTracker options
|
||||||
|
#define TRILO_USE_SFXPLAY TRUE // Add SFX playback through Trilo SCC player (ayFX + SCC format)
|
||||||
|
#define TRILO_USE_TREMOLO TRUE // Add support for tremolo effect (little bit expensive)
|
||||||
|
#define TRILO_USE_TAIL FALSE // Add tail to prevent volume to fall to zero
|
||||||
|
|
||||||
|
// LVGM replayer options
|
||||||
|
#define LVGM_USE_PSG TRUE // Add parser for PSG data
|
||||||
|
#define LVGM_USE_MSXMUSIC TRUE // Add parser for MSX-Music data
|
||||||
|
#define LVGM_USE_MSXAUDIO TRUE // Add parser for MSX-Audio data
|
||||||
|
#define LVGM_USE_SCC TRUE // Add parser for Konami SCC data
|
||||||
|
#define LVGM_USE_SCCI FALSE // Add parser for Konami SCC+ data
|
||||||
|
#define LVGM_USE_PSG2 FALSE // Add parser for secondary PSG data
|
||||||
|
#define LVGM_USE_OPL4 FALSE // Add parser for OPL4 data
|
||||||
|
#define LVGM_USE_NOTIFY TRUE // Add parser for PSG data
|
||||||
|
|
||||||
|
// WYZ Tracker replayer options
|
||||||
|
// Channels number
|
||||||
|
// - WYZ_3CH
|
||||||
|
// - WYZ_6CH
|
||||||
|
#define WYZ_CHANNELS WYZ_3CH // Number of supported channels (can be 3 for 1 PSG or 6 for 2 PSG)
|
||||||
|
#define WYZ_USE_DIRECT_ACCESS FALSE // Send data directly to PSG registers (otherwise, write in a RAM buffer)
|
||||||
|
#define WYZ_CHAN_BUFFER_SIZE 0x20 // Size of the channel buffer
|
||||||
|
|
||||||
|
// Arkos Tracker options
|
||||||
|
#define ARKOS_BUFFER_ADDR 0xF000 // Replayer working area address in RAM
|
||||||
|
#define ARKOS_ISR_PROTECTION FALSE // Prevent interruption during audio update
|
||||||
|
#define ARKOS_SFX_START_IDX 0 // Do SFX indexes start at 0 or 1? Default is 0 but Arkos Tracker use 1
|
||||||
|
#define ARKOS_USE_EVENT FALSE // Support for event callback function (AKG replayer only)
|
||||||
|
|
||||||
|
// NDP player options
|
||||||
|
#define NDP_USE_INDERCT FALSE // Output music data into RAM buffer (need to use with ayFX for example)
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// MATH MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Random methods
|
||||||
|
// - RANDOM_8_NONE ................ No 8-bits random
|
||||||
|
// - RANDOM_8_REGISTER ............ R Register value (7-bits)
|
||||||
|
// - RANDOM_8_RACC ................ R Register accumulation (7-bits)
|
||||||
|
// - RANDOM_8_ION ................. Ion Random
|
||||||
|
// - RANDOM_8_MEMORY .............. Memory Peek from R
|
||||||
|
#define RANDOM_8_METHOD RANDOM_8_ION
|
||||||
|
// - RANDOM_16_NONE ............... No 16-bits random
|
||||||
|
// - RANDOM_16_LINEAR ............. Linear congruential
|
||||||
|
// - RANDOM_16_XORSHIFT ........... XOR Shift
|
||||||
|
// - RANDOM_16_LFSR_LCG ........... Combined LFSR/LCG
|
||||||
|
#define RANDOM_16_METHOD RANDOM_16_XORSHIFT
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// COMPRESS
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// RLEp compression
|
||||||
|
#define COMPRESS_USE_RLEP TRUE // Use RLEp unpacker
|
||||||
|
#define COMPRESS_USE_RLEP_DEFAULT TRUE // Data include the default value (0 otherwise)
|
||||||
|
#define COMPRESS_USE_RLEP_FIXSIZE FALSE // Gize the data size as function input (loop up to terminator otherwise)
|
||||||
|
|
||||||
|
// Pletter compression
|
||||||
|
#define PLETTER_LENGTHINDATA FALSE // Is length included in data (first 2 bytes)
|
||||||
|
// Disable interruption mode
|
||||||
|
// - PLETTER_DI_NONE .............. Don't disable interruption
|
||||||
|
// - PLETTER_DI_FULL .............. Disable interruption during the whole function
|
||||||
|
// - PLETTER_DI_LOOP .............. Disable interruption during VRAM write loop
|
||||||
|
#define PLETTER_DI_MODE PLETTER_DI_LOOP
|
||||||
|
// VRAM write timing mode
|
||||||
|
// - PLETTER_WRITE_SAFE ........... Safe VRAM write speed (30 t-states)
|
||||||
|
// - PLETTER_WRITE_NODISPLAY ...... Safe VRAM write speed when screen display disable (22 t-states)
|
||||||
|
// - PLETTER_WRITE_MINIMAL ........ Minimal wait beetween write (17 t-states)
|
||||||
|
// - PLETTER_WRITE_QUICK .......... No wait beetween write (12 t-states)
|
||||||
|
// - PLETTER_WRITE_AUTO ........... Determine the worst case according to selected screen mode (12~30 t-states)
|
||||||
|
#define PLETTER_WRITE_MODE PLETTER_WRITE_SAFE
|
||||||
|
|
||||||
|
// BitBuster compression
|
||||||
|
// VRAM write timing mode
|
||||||
|
// - BITBUSTER_WRITE_SAFE ......... Safe VRAM write speed (include nop between write)
|
||||||
|
// - BITBUSTER_WRITE_QUICK ........ No wait beetween write
|
||||||
|
#define BITBUSTER_WRITE_MODE BITBUSTER_WRITE_SAFE
|
||||||
|
|
||||||
|
// ZX0 compression
|
||||||
|
// Unpack mode
|
||||||
|
// - ZX0_MODE_STANDARD ............ Standard routine: 68 bytes only
|
||||||
|
// - ZX0_MODE_TURBO ............... Turbo routine: 126 bytes, about 21% faster
|
||||||
|
// - ZX0_MODE_FAST ................ Fast routine: 187 bytes, about 25% faster
|
||||||
|
// - ZX0_MODE_MEGA ................ Mega routine: 673 bytes, about 28% faster
|
||||||
|
#define ZX0_MODE ZX0_MODE_STANDARD
|
||||||
|
|
||||||
|
// LZ48 compression
|
||||||
|
// - LZ48_MODE_STANDARD ........... Standard routine
|
||||||
|
// - LZ48_MODE_SPEED .............. Version optimized for speed
|
||||||
|
// - LZ48_MODE_SIZE ............... Version optimized for size
|
||||||
|
#define LZ48_MODE LZ48_MODE_STANDARD
|
||||||
|
|
||||||
|
// MSXi compressor support
|
||||||
|
#define MSXi_USE_COMP_NONE TRUE
|
||||||
|
#define MSXi_USE_COMP_CROP16 TRUE
|
||||||
|
#define MSXi_USE_COMP_CROP32 TRUE
|
||||||
|
#define MSXi_USE_COMP_CROP256 TRUE
|
||||||
|
#define MSXi_USE_COMP_CROPLINE16 TRUE
|
||||||
|
#define MSXi_USE_COMP_CROPLINE32 TRUE
|
||||||
|
#define MSXi_USE_COMP_CROPLINE256 TRUE
|
||||||
|
#define MSXi_USE_COMP_RLE0 TRUE
|
||||||
|
#define MSXi_USE_COMP_RLE4 TRUE
|
||||||
|
#define MSXi_USE_COMP_RLE8 TRUE
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// NINJATAP MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Supported driver(s)
|
||||||
|
// - NTAP_DRIVER_MSXGL ............ MSXgl custom driver (based on DM-System2 one)
|
||||||
|
// - NTAP_DRIVER_GIGAMIX .......... Original Gigamix's DM-System2 driver
|
||||||
|
// - NTAP_DRIVER_SHINOBI .......... Shinobi Tap driver by Danjovic
|
||||||
|
#define NTAP_DRIVER NTAP_DRIVER_MSXGL | NTAP_DRIVER_GIGAMIX | NTAP_DRIVER_SHINOBI
|
||||||
|
#define NTAP_USE_PREVIOUS TRUE // Backup previous data to allow push/release detection
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// PAC MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define PAC_USE_SIGNATURE TRUE // Handle application signature to validate saved data
|
||||||
|
#define PAC_USE_VALIDATOR TRUE // Add code to validate input parameters
|
||||||
|
#define PAC_DEVICE_MAX 4 // Maximum number of supported PAC devices
|
||||||
|
// SRAM access method
|
||||||
|
// - PAC_ACCESS_DIRECT ............ Direct access to SRAM (must be selected in page 1)
|
||||||
|
// - PAC_ACCESS_BIOS .............. Access through BIOS routines
|
||||||
|
// - PAC_ACCESS_SWITCH_BIOS ....... Access through BIOS routines with BIOS switched in
|
||||||
|
// - PAC_ACCESS_SYSTEM ............ Access through MSXgl routine (no need BIOS)
|
||||||
|
#define PAC_ACCESS PAC_ACCESS_BIOS
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// QR CODE MODULE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define QRCODE_VERSION_MIN 1 // The minimum version number supported in the QR Code Model 2 standard
|
||||||
|
#define QRCODE_VERSION_MAX 20 // The maximum version number supported in the QR Code Model 2 standard
|
||||||
|
#define QRCODE_VERSION_CUSTOM FALSE // TRUE: Allow to define version using <QRCode_SetVersion>. FALSE: Use hardcoded min/max version
|
||||||
|
#define QRCODE_USE_BYTE_ONLY TRUE // TRUE: Allow only BYTE mode. FALSE allow all mode including NUMERIC, ALPHANUMERIC, KANJI and ECI.
|
||||||
|
#define QRCODE_USE_EXTRA FALSE // TRUE: Add extra function to generate custom data segments.
|
||||||
|
#define QRCODE_BOOST_ECL FALSE // If boostEcl is TRUE, then the ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. QRCODE_PARAM_CUSTOM: Allow to use <QRCode_SetBoostECL> function.
|
||||||
|
|
||||||
|
#define QRCODE_TINY_VERSION 10 // The version number supported in the QR Code Model 2 standard
|
||||||
|
// Error correction level
|
||||||
|
// - QRCODE_ECC_LOW ............... The QR Code can tolerate about 7% erroneous codewords
|
||||||
|
// - QRCODE_ECC_MEDIUM ............ The QR Code can tolerate about 15% erroneous codewords
|
||||||
|
// - QRCODE_ECC_QUARTILE .......... The QR Code can tolerate about 25% erroneous codewords
|
||||||
|
// - QRCODE_ECC_HIGH .............. The QR Code can tolerate about 30% erroneous codewords
|
||||||
|
#define QRCODE_TINY_ECC QRCODE_ECC_LOW
|
||||||
|
// Mask pattern
|
||||||
|
// - QRCODE_MASK_0 ................ (i + j) % 2 = 0
|
||||||
|
// - QRCODE_MASK_1 ................ i % 2 = 0
|
||||||
|
// - QRCODE_MASK_2 ................ j % 3 = 0
|
||||||
|
// - QRCODE_MASK_3 ................ (i + j) % 3 = 0
|
||||||
|
// - QRCODE_MASK_4 ................ (i / 2 + j / 3) % 2 = 0
|
||||||
|
// - QRCODE_MASK_5 ................ (i * j) % 2 + (i * j) % 3 = 0
|
||||||
|
// - QRCODE_MASK_6 ................ ((i * j) % 3 + i * j) % 2 = 0
|
||||||
|
// - QRCODE_MASK_7 ................ ((i * j) % 3 + i + j) % 2 = 0
|
||||||
|
#define QRCODE_TINY_MASK QRCODE_MASK_0
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// DEBUG & PROFILE
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Debugger options
|
||||||
|
// - DEBUG_DISABLE ................ No debug tool
|
||||||
|
// - DEBUG_EMULICIOUS ............. Debug features for Emulicious
|
||||||
|
// - DEBUG_OPENMSX ................ Debug features for openMSX using 'debugdevice' extension
|
||||||
|
// - DEBUG_OPENMSX_P .............. Debug features for openMSX using PVM script (tools/script/openMSX/debugger_pvm.tcl)
|
||||||
|
#define DEBUG_TOOL DEBUG_DISABLE
|
||||||
|
// Profiler options
|
||||||
|
// - PROFILE_DISABLE .............. No profile tool
|
||||||
|
// - PROFILE_OPENMSX_G ............ Profiler features for openMSX using Grauw script (tools/script/openMSX/profiler_grauw.tcl)
|
||||||
|
// - PROFILE_OPENMSX_S ............ Profiler features for openMSX using Salutte script (tools/script/openMSX/profiler_salutte.tcl)
|
||||||
|
#define PROFILE_TOOL PROFILE_DISABLE
|
||||||
|
#define PROFILE_LEVEL 10
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Kissaten Yūgure - MSXgl project configuration
|
||||||
|
// MSX2 · SCREEN 5 · ASCII16 MegaROM (see docs/DESIGN.md §5)
|
||||||
|
|
||||||
|
// ASCII-safe name: used for output filenames (ROM, symbols, intermediates)
|
||||||
|
ProjName = "kissaten";
|
||||||
|
ProjModules = [ ProjName ];
|
||||||
|
// "tool/disk_save" needs "dos" alongside it — see projects/samples/s_save.js
|
||||||
|
LibModules = [ "system", "bios", "vdp", "print", "input", "memory", "math",
|
||||||
|
"tool/disk_save", "dos" ];
|
||||||
|
|
||||||
|
Machine = "2";
|
||||||
|
|
||||||
|
// ASCII16 MegaROM from day one: dialogue, portraits and music banks will not
|
||||||
|
// fit in 32KB (DESIGN.md §5). 16KB segments; segments 0-1 are the fixed main
|
||||||
|
// code at 4000h~BFFFh, segments 2+ are banked data (dialogue, art, music).
|
||||||
|
Target = "ROM_ASCII16";
|
||||||
|
ROMSize = 128;
|
||||||
|
ROMMainSegments = 2;
|
||||||
|
// Enable when banked data/code actually lands (stage 3 dialogue banks);
|
||||||
|
// with it on from day one, RAM globals were getting corrupted at runtime.
|
||||||
|
BankedCall = false;
|
||||||
|
|
||||||
|
CheckVersion = true; // MSX2 required (SCREEN 5, V9938 commands)
|
||||||
|
AddROMSignature = true;
|
||||||
|
|
||||||
|
// Required for disk saving. A cartridge's INIT normally runs *during* the BIOS
|
||||||
|
// slot scan and MSXgl never returns from it, so the Disk ROM's own INIT never
|
||||||
|
// happens — NMBDRV stays 0 and the PHYDIO hook is never patched. This option
|
||||||
|
// instead installs an H.STKE hook and returns to the scan, so the game starts
|
||||||
|
// after the disk system is up. Boot takes visibly longer as a result.
|
||||||
|
ROMDelayBoot = true;
|
||||||
|
|
||||||
|
AppSignature = true;
|
||||||
|
AppCompany = "JL";
|
||||||
|
AppID = "KY";
|
||||||
|
|
||||||
|
Verbose = true;
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Emit the Kissaten palette in every format the art tools want.
|
||||||
|
|
||||||
|
python3 tools/make_palettes.py
|
||||||
|
|
||||||
|
.gpl GIMP / Inkscape / Krita / Aseprite
|
||||||
|
.ase Affinity (Photo, Designer, Publisher), Illustrator
|
||||||
|
.aco Affinity, Photoshop
|
||||||
|
.act Photoshop indexed-colour tables
|
||||||
|
.png 16x1 exact + a swatch strip, and a palette reference for generators
|
||||||
|
|
||||||
|
Two sets are written: the full 16 (scene work) and the 11-colour portrait
|
||||||
|
subset with the sky slots and the transparent index removed.
|
||||||
|
"""
|
||||||
|
import struct, os, sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from pixtool import PALETTE, write_png
|
||||||
|
|
||||||
|
NAMES = ["transparent", "outline", "dark wood", "mid wood", "light wood",
|
||||||
|
"lamp glow", "cream", "muted wall", "dusk deep", "dusk violet",
|
||||||
|
"dusk rose", "horizon amber", "coffee", "coat blue", "skin",
|
||||||
|
"warm white"]
|
||||||
|
PORTRAIT_IDX = [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15]
|
||||||
|
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "art")
|
||||||
|
|
||||||
|
|
||||||
|
def gpl(path, cols, names, title):
|
||||||
|
L = [f"GIMP Palette", f"Name: {title}", f"Columns: {len(cols)}", "#"]
|
||||||
|
for c, n in zip(cols, names):
|
||||||
|
L.append(f"{c[0]:3d} {c[1]:3d} {c[2]:3d}\t{n}")
|
||||||
|
open(path, "w").write("\n".join(L) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def ase(path, cols, names):
|
||||||
|
blocks = b""
|
||||||
|
for c, n in zip(cols, names):
|
||||||
|
nm = n.encode("utf-16-be") + b"\x00\x00"
|
||||||
|
body = (struct.pack(">H", len(n) + 1) + nm + b"RGB "
|
||||||
|
+ struct.pack(">fff", c[0] / 255, c[1] / 255, c[2] / 255)
|
||||||
|
+ struct.pack(">H", 2)) # 2 = normal colour
|
||||||
|
blocks += struct.pack(">HI", 0x0001, len(body)) + body
|
||||||
|
open(path, "wb").write(b"ASEF" + struct.pack(">HHI", 1, 0, len(cols)) + blocks)
|
||||||
|
|
||||||
|
|
||||||
|
def aco(path, cols, names):
|
||||||
|
# v1 then v2 concatenated — v1 for old readers, v2 carries the names
|
||||||
|
v1 = struct.pack(">HH", 1, len(cols))
|
||||||
|
for c in cols:
|
||||||
|
v1 += struct.pack(">HHHHH", 0, c[0] * 257, c[1] * 257, c[2] * 257, 0)
|
||||||
|
v2 = struct.pack(">HH", 2, len(cols))
|
||||||
|
for c, n in zip(cols, names):
|
||||||
|
v2 += struct.pack(">HHHHH", 0, c[0] * 257, c[1] * 257, c[2] * 257, 0)
|
||||||
|
v2 += struct.pack(">I", len(n) + 1) + n.encode("utf-16-be") + b"\x00\x00"
|
||||||
|
open(path, "wb").write(v1 + v2)
|
||||||
|
|
||||||
|
|
||||||
|
def act(path, cols):
|
||||||
|
d = bytearray()
|
||||||
|
for c in cols:
|
||||||
|
d += bytes(c)
|
||||||
|
d += bytes(768 - len(d))
|
||||||
|
open(path, "wb").write(bytes(d))
|
||||||
|
|
||||||
|
|
||||||
|
def swatches(path, cols, sw=32, h=64):
|
||||||
|
write_png(path, [[c for c in cols for _ in range(sw)]] * h)
|
||||||
|
|
||||||
|
|
||||||
|
# 3x5 glyphs, enough for indices and hex codes. No font library here.
|
||||||
|
GLYPHS = {
|
||||||
|
"0": "111101101101111", "1": "010110010010111", "2": "111001111100111",
|
||||||
|
"3": "111001111001111", "4": "101101111001001", "5": "111100111001111",
|
||||||
|
"6": "111100111101111", "7": "111001001001001", "8": "111101111101111",
|
||||||
|
"9": "111101111001111", "A": "111101111101101", "B": "110101110101110",
|
||||||
|
"C": "111100100100111", "D": "110101101101110", "E": "111100111100111",
|
||||||
|
"F": "111100111100100", "#": "101111101111101", " ": "000000000000000",
|
||||||
|
"-": "000000111000000",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _text(px, x, y, s, col, scale=2):
|
||||||
|
for ch in s.upper():
|
||||||
|
g = GLYPHS.get(ch, GLYPHS[" "])
|
||||||
|
for gy in range(5):
|
||||||
|
for gx in range(3):
|
||||||
|
if g[gy*3 + gx] == "1":
|
||||||
|
for sy in range(scale):
|
||||||
|
for sx in range(scale):
|
||||||
|
yy, xx = y + gy*scale + sy, x + gx*scale + sx
|
||||||
|
if 0 <= yy < len(px) and 0 <= xx < len(px[0]):
|
||||||
|
px[yy][xx] = col
|
||||||
|
x += 4 * scale
|
||||||
|
|
||||||
|
|
||||||
|
def card(path, cols, names, idx, scale=2):
|
||||||
|
"""Labelled reference: swatch, palette index, hex code, role name."""
|
||||||
|
rowh, sww, pad = 7*scale + 8, 22*scale, 6
|
||||||
|
W = sww + pad*2 + 4*scale*22
|
||||||
|
H = rowh*len(cols) + pad*2
|
||||||
|
bg, fg = (26, 26, 26), (255, 255, 219)
|
||||||
|
px = [[bg]*W for _ in range(H)]
|
||||||
|
for i, (c, n, ix) in enumerate(zip(cols, names, idx)):
|
||||||
|
y = pad + i*rowh
|
||||||
|
for yy in range(y, y + 7*scale):
|
||||||
|
for xx in range(pad, pad + sww):
|
||||||
|
px[yy][xx] = c
|
||||||
|
label = f"{ix:02d} #{c[0]:02X}{c[1]:02X}{c[2]:02X}"
|
||||||
|
_text(px, pad + sww + pad, y + scale, label, fg, scale)
|
||||||
|
write_png(path, px)
|
||||||
|
|
||||||
|
|
||||||
|
def emit(stem, idx, title):
|
||||||
|
cols = [PALETTE[i] for i in idx]
|
||||||
|
names = [NAMES[i] for i in idx]
|
||||||
|
p = lambda ext: os.path.normpath(os.path.join(OUT, f"{stem}.{ext}"))
|
||||||
|
gpl(p("gpl"), cols, names, title)
|
||||||
|
ase(p("ase"), cols, names)
|
||||||
|
aco(p("aco"), cols, names)
|
||||||
|
act(p("act"), cols)
|
||||||
|
swatches(p("png"), cols)
|
||||||
|
card(os.path.normpath(os.path.join(OUT, f"{stem}-card.png")), cols, names, idx)
|
||||||
|
print(f"{stem}: {len(cols)} colours -> .gpl .ase .aco .act .png + -card.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
emit("kissaten-full", list(range(16)), "Kissaten Yugure (full)")
|
||||||
|
emit("kissaten-portrait", PORTRAIT_IDX, "Kissaten Yugure (portrait)")
|
||||||
|
# exact 1px-per-entry image, for generators that take a palette image
|
||||||
|
write_png(os.path.normpath(os.path.join(OUT, "palette.png")), [list(PALETTE)])
|
||||||
|
print("palette.png: 16x1 exact")
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dependency-free PNG read/write plus helpers for the Kissaten art pipeline.
|
||||||
|
|
||||||
|
No Pillow on this machine, so decode/encode are hand-rolled. Supports the
|
||||||
|
colour types PixelLab and friends actually emit: 2 (RGB), 6 (RGBA), 3
|
||||||
|
(palette), 0 (grey), 4 (grey+alpha), 8 bits per channel.
|
||||||
|
"""
|
||||||
|
import zlib, struct, binascii, sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_png(path):
|
||||||
|
"""-> (w, h, pixels) where pixels is a list of rows of (r,g,b,a) tuples."""
|
||||||
|
d = open(path, "rb").read()
|
||||||
|
assert d[:8] == b"\x89PNG\r\n\x1a\x0a", f"{path}: not a PNG"
|
||||||
|
pos, idat, plte, trns = 8, b"", None, None
|
||||||
|
w = h = depth = ctype = None
|
||||||
|
while pos < len(d):
|
||||||
|
ln = struct.unpack(">I", d[pos:pos + 4])[0]
|
||||||
|
typ = d[pos + 4:pos + 8]
|
||||||
|
body = d[pos + 8:pos + 8 + ln]
|
||||||
|
if typ == b"IHDR":
|
||||||
|
w, h, depth, ctype = struct.unpack(">IIBB", body[:10])
|
||||||
|
elif typ == b"PLTE":
|
||||||
|
plte = body
|
||||||
|
elif typ == b"tRNS":
|
||||||
|
trns = body
|
||||||
|
elif typ == b"IDAT":
|
||||||
|
idat += body
|
||||||
|
elif typ == b"IEND":
|
||||||
|
break
|
||||||
|
pos += 12 + ln
|
||||||
|
assert depth == 8, f"{path}: only 8-bit channels supported (got {depth})"
|
||||||
|
|
||||||
|
nch = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[ctype]
|
||||||
|
raw = zlib.decompress(idat)
|
||||||
|
stride = w * nch
|
||||||
|
out, prev = [], bytearray(stride)
|
||||||
|
p = 0
|
||||||
|
for _ in range(h):
|
||||||
|
f = raw[p]; p += 1
|
||||||
|
line = bytearray(raw[p:p + stride]); p += stride
|
||||||
|
for i in range(stride):
|
||||||
|
a = line[i - nch] if i >= nch else 0
|
||||||
|
b = prev[i]
|
||||||
|
c = prev[i - nch] if i >= nch else 0
|
||||||
|
if f == 1: line[i] = (line[i] + a) & 255
|
||||||
|
elif f == 2: line[i] = (line[i] + b) & 255
|
||||||
|
elif f == 3: line[i] = (line[i] + (a + b) // 2) & 255
|
||||||
|
elif f == 4:
|
||||||
|
pa, pb, pc = abs(b - c), abs(a - c), abs(a + b - 2 * c)
|
||||||
|
pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
||||||
|
line[i] = (line[i] + pr) & 255
|
||||||
|
prev = line
|
||||||
|
|
||||||
|
row = []
|
||||||
|
for x in range(w):
|
||||||
|
v = line[x * nch:(x + 1) * nch]
|
||||||
|
if ctype == 2: row.append((v[0], v[1], v[2], 255))
|
||||||
|
elif ctype == 6: row.append((v[0], v[1], v[2], v[3]))
|
||||||
|
elif ctype == 0: row.append((v[0], v[0], v[0], 255))
|
||||||
|
elif ctype == 4: row.append((v[0], v[0], v[0], v[1]))
|
||||||
|
else:
|
||||||
|
i = v[0]
|
||||||
|
a = trns[i] if (trns and i < len(trns)) else 255
|
||||||
|
row.append((plte[i * 3], plte[i * 3 + 1], plte[i * 3 + 2], a))
|
||||||
|
out.append(row)
|
||||||
|
return w, h, out
|
||||||
|
|
||||||
|
|
||||||
|
def write_png(path, rows):
|
||||||
|
h, w = len(rows), len(rows[0])
|
||||||
|
raw = b"".join(b"\x00" + bytes(c for px in r for c in px[:3]) for r in rows)
|
||||||
|
|
||||||
|
def chunk(t, b):
|
||||||
|
c = t + b
|
||||||
|
return struct.pack(">I", len(b)) + c + struct.pack(">I", binascii.crc32(c) & 0xffffffff)
|
||||||
|
|
||||||
|
open(path, "wb").write(
|
||||||
|
b"\x89PNG\r\n\x1a\n"
|
||||||
|
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
|
||||||
|
+ chunk(b"IDAT", zlib.compress(raw, 9))
|
||||||
|
+ chunk(b"IEND", b""))
|
||||||
|
|
||||||
|
|
||||||
|
def zoom(rows, n):
|
||||||
|
"""Nearest-neighbour upscale, so pixels stay square and countable."""
|
||||||
|
return [[px for px in r for _ in range(n)] for r in rows for _ in range(n)]
|
||||||
|
|
||||||
|
|
||||||
|
# The game palette, index-aligned with g_Palette in kissaten.c
|
||||||
|
PALETTE = [
|
||||||
|
(0x00, 0x00, 0x00), (0x24, 0x24, 0x24), (0x49, 0x24, 0x24), (0x92, 0x49, 0x24),
|
||||||
|
(0xB6, 0x6D, 0x49), (0xFF, 0xB6, 0x49), (0xFF, 0xDB, 0x92), (0x6D, 0x49, 0x49),
|
||||||
|
(0x49, 0x24, 0x92), (0x92, 0x49, 0xB6), (0xDB, 0x6D, 0x6D), (0xFF, 0x92, 0x49),
|
||||||
|
(0x6D, 0x24, 0x00), (0x49, 0x6D, 0xB6), (0xFF, 0xB6, 0x92), (0xFF, 0xFF, 0xDB),
|
||||||
|
]
|
||||||
|
SKY = {8, 9, 10, 11}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
w, h, rows = read_png(path)
|
||||||
|
flat = [px for r in rows for px in r]
|
||||||
|
cols = {}
|
||||||
|
for px in flat:
|
||||||
|
cols[px[:3]] = cols.get(px[:3], 0) + 1
|
||||||
|
alphas = sorted({px[3] for px in flat})
|
||||||
|
exact = sum(n for c, n in cols.items() if c in PALETTE)
|
||||||
|
print(f"\n{path} {w}x{h}")
|
||||||
|
print(f" unique colours : {len(cols)}")
|
||||||
|
print(f" alpha values : {alphas if len(alphas) < 6 else str(alphas[:5]) + '...'}")
|
||||||
|
print(f" already in our palette: {len(cols) - len([c for c in cols if c not in PALETTE])}"
|
||||||
|
f" of {len(cols)} ({100*exact//len(flat)}% of pixels)")
|
||||||
|
print(" top 8 colours:")
|
||||||
|
for c, n in sorted(cols.items(), key=lambda kv: -kv[1])[:8]:
|
||||||
|
tag = " <- in palette" if c in PALETTE else ""
|
||||||
|
print(f" #{c[0]:02X}{c[1]:02X}{c[2]:02X} {n:5d} px {100*n//len(flat):3d}%{tag}")
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""SVG portrait -> 48x48 PNG in the Kissaten palette.
|
||||||
|
|
||||||
|
python3 tools/svg2portrait.py art/aki-neutral.svg [more.svg ...]
|
||||||
|
|
||||||
|
Why this route beats exporting a bitmap and resizing:
|
||||||
|
|
||||||
|
Magnific's SVG export is flat-shaded vector — hundreds of paths, each a solid
|
||||||
|
fill, with only a handful of gradients. Rendering that at an exact multiple of
|
||||||
|
48 and then taking the *most common* colour in each block is a true pixelate:
|
||||||
|
no blending, no half-pixels, no anti-aliased edges. Bicubic resizing of a
|
||||||
|
bitmap averages instead, which turns every flat region into a gradient and
|
||||||
|
makes the later palette reduction speckle badly.
|
||||||
|
|
||||||
|
Requires qlmanage (macOS built-in) to rasterise. No third-party deps.
|
||||||
|
"""
|
||||||
|
import subprocess, sys, os, tempfile
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from pixtool import read_png, write_png, zoom, PALETTE
|
||||||
|
|
||||||
|
SIZE = 48
|
||||||
|
SCALE = 16 # render at SIZE*SCALE, then block-reduce
|
||||||
|
PORTRAIT_IDX = [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15] # no sky, no index 0
|
||||||
|
PP = [PALETTE[i] for i in PORTRAIT_IDX]
|
||||||
|
|
||||||
|
|
||||||
|
def nearest(c):
|
||||||
|
r, g, b = c[:3]
|
||||||
|
# green weighted heaviest: keeps eyes and mouth from dissolving into skin
|
||||||
|
return min(PP, key=lambda p: 3*(p[0]-r)**2 + 6*(p[1]-g)**2 + (p[2]-b)**2)
|
||||||
|
|
||||||
|
|
||||||
|
def rasterise(svg, px):
|
||||||
|
"""SVG -> PNG at px wide, via Quick Look. Returns decoded pixels."""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
subprocess.run(["qlmanage", "-t", "-s", str(px), "-o", td, svg],
|
||||||
|
capture_output=True, check=True)
|
||||||
|
out = os.path.join(td, os.path.basename(svg) + ".png")
|
||||||
|
if not os.path.exists(out):
|
||||||
|
raise SystemExit(f"qlmanage produced nothing for {svg}")
|
||||||
|
return read_png(out)
|
||||||
|
|
||||||
|
|
||||||
|
def mode_reduce(rows, n):
|
||||||
|
"""Each n*n block collapses to its most common colour — a real pixelate."""
|
||||||
|
return [[Counter(rows[by*n + y][bx*n + x][:3]
|
||||||
|
for y in range(n) for x in range(n)).most_common(1)[0][0]
|
||||||
|
for bx in range(SIZE)] for by in range(SIZE)]
|
||||||
|
|
||||||
|
|
||||||
|
def convert(svg):
|
||||||
|
w, h, big = rasterise(svg, SIZE * SCALE)
|
||||||
|
if w != SIZE * SCALE:
|
||||||
|
raise SystemExit(f"{svg}: expected {SIZE*SCALE}px render, got {w}")
|
||||||
|
small = mode_reduce(big, SCALE)
|
||||||
|
quant = [[nearest(p) for p in r] for r in small]
|
||||||
|
|
||||||
|
stem = os.path.splitext(svg)[0]
|
||||||
|
write_png(f"{stem}-{SIZE}.png", [[p + (255,) for p in r] for r in quant])
|
||||||
|
write_png(f"{stem}-{SIZE}-x6.png",
|
||||||
|
zoom([[p + (255,) for p in r] for r in quant], 6))
|
||||||
|
print(f"{os.path.basename(svg)}: {len({p for r in small for p in r})} colours "
|
||||||
|
f"after pixelate -> {len({p for r in quant for p in r})} after palette")
|
||||||
|
print(f" wrote {stem}-{SIZE}.png and {stem}-{SIZE}-x6.png (preview)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
raise SystemExit(__doc__)
|
||||||
|
for f in sys.argv[1:]:
|
||||||
|
convert(f)
|
||||||