// 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" // 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 u8 g_DiskErr = 0; // last disk error code (0 = fine) bool g_HasDisk = FALSE; // a usable save disk was found at boot //============================================================================= // 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; // One 512-byte sector staging buffer. Must live in RAM (page 3) — PHYDIO // writes straight into it, and pages 1-2 are ROM on a cartridge target. u8 g_Sector[512]; // Sector holding the save, derived from the disk's own BPB at boot rather // than hardcoded, so a 360K disk works as well as the 720K one. u16 g_SaveSector = 14; //============================================================================= // DISK ACCESS //============================================================================= // Number of disk drives, maintained by the Disk ROM at boot. Zero means there // is no disk interface at all — and on such a machine (C-BIOS, for one) the // PHYDIO hook is not patched and calling it is not safe. #define M_NMBDRV_ADDR 0xF347 #define M_MASTER_ADDR 0xF348 // main Disk ROM slot, 0 if there is none // PHYDIO trampoline, executed from RAM. // // PHYDIO (Main-ROM 0144h) reaches the Disk ROM through an inter-slot call, and // the Disk ROM lives at 4000h — the same page as this cartridge. For the // duration of the call our ROM is switched out of page 1, so the instruction // making the call must not itself be in page 1. Copying these twelve bytes to // RAM and calling *them* keeps the call site alive across the switch. u8 g_DiskStub[21]; void Disk_InitStub() { // Calls DSKIO (Disk ROM 4010h) through CALSLT rather than the Main ROM's // PHYDIO wrapper at 0144h: PHYDIO jumps to a RAM hook that this machine // leaves unpatched, so it returns carry-clear — reporting success while // never touching the buffer. Going straight at the Disk ROM avoids it. // // IY high byte must hold the slot ID, and MASTER lives at F348h, so a // word load from F347h lands it in IYh exactly as CALSLT wants. static const u8 code[21] = { 0xF3, // di 0xFD, 0x2A, 0x47, 0xF3, // ld iy, (0xF347) ; IYh = MASTER 0xDD, 0x21, 0x10, 0x40, // ld ix, #0x4010 ; DSKIO 0xCD, 0x1C, 0x00, // call 0x001C ; CALSLT 0x38, 0x04, // jr c, err 0x2E, 0x00, // ld l, #0 ; success 0xFB, // ei 0xC9, // ret 0x6F, // err: ld l, a ; BIOS error code 0xFB, // ei 0xC9, // ret }; Mem_Copy(code, g_DiskStub, sizeof(code)); } //----------------------------------------------------------------------------- // Absolute sector read/write into g_Sector. // carry clear = read, carry set = write // A=drive B=sector count C=media ID DE=first sector HL=buffer // Returns the BIOS error code, or 0 on success. u8 Disk_Access(u16 sector, u8 write) __NAKED { sector; // on stack write; __asm push ix ld ix, #0 add ix, sp ld e, 4(ix) // sector low ld d, 5(ix) // sector high ld a, 6(ix) // write flag ld hl, #_g_Sector ld b, #1 // one sector ld c, #0xF9 // media ID: 720K DS/DD or a // test flag; also clears carry (= read) jr z, phydio_go scf // carry set = write phydio_go: ld a, #0 // drive A:. 'ld a,n' preserves flags call _g_DiskStub // -> PHYDIO, from RAM. Returns code in L pop ix ret __endasm; } //----------------------------------------------------------------------------- // Read the BIOS Parameter Block and work out where the data area starts. // Returns FALSE if no disk responded at all. bool Disk_Probe() { u8 drives = *(const u8*)M_NMBDRV_ADDR; // Both of these are set by the Disk ROM's INIT. Without them the entry // points below are not safe to call — on C-BIOS this work area is never // initialised at all, and calling anyway black-screens the machine. if (drives == 0 || drives > 8) return FALSE; if (*(const u8*)M_MASTER_ADDR == 0) return FALSE; Disk_InitStub(); // Sentinel: the disk entry points can return carry-clear ("success") // without having touched the buffer, so a clean error code is not on its // own proof that a read happened. Verify the buffer actually changed. Mem_Set(0xAA, g_Sector, 16); g_DiskErr = Disk_Access(0, 0); if (g_DiskErr != 0) return FALSE; if (g_Sector[0] == 0xAA && g_Sector[11] == 0xAA) return FALSE; // nothing was read; treat the disk as unusable u16 bytesPerSec = g_Sector[11] | ((u16)g_Sector[12] << 8); u16 reserved = g_Sector[14] | ((u16)g_Sector[15] << 8); u8 numFATs = g_Sector[16]; u16 rootEntries = g_Sector[17] | ((u16)g_Sector[18] << 8); u16 secPerFAT = g_Sector[22] | ((u16)g_Sector[23] << 8); if (bytesPerSec != 512) // anything else is not a disk we made return FALSE; // Root directory occupies ceil(entries * 32 / 512) sectors u16 rootSecs = (u16)((rootEntries + 15) >> 4); g_SaveSector = reserved + (u16)numFATs * secPerFAT + rootSecs; return TRUE; } //----------------------------------------------------------------------------- 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); } //----------------------------------------------------------------------------- // TRUE if a valid save was loaded; FALSE means "start a new game", which is // also what a blank disk gives us — no error path for the player to hit. bool Save_Load() { if (!g_HasDisk) return FALSE; g_DiskErr = Disk_Access(g_SaveSector, 0); if (g_DiskErr != 0) return FALSE; SaveState* s = (SaveState*)g_Sector; if (s->magic != SAVE_MAGIC || s->version != SAVE_VERSION) return FALSE; if (s->checksum != Save_Checksum(s)) return FALSE; Mem_Copy(s, &g_Save, sizeof(SaveState)); 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); Mem_Set(0, g_Sector, sizeof(g_Sector)); Mem_Copy(&g_Save, g_Sector, sizeof(SaveState)); g_DiskErr = Disk_Access(g_SaveSector, 1); return (g_DiskErr == 0); } //----------------------------------------------------------------------------- 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."); 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."); if (Save_Write()) Dlg_Prompt("[SPACE] sleep (saved)"); else if (!g_HasDisk) Dlg_Prompt("[SPACE] sleep (no disk)"); else Dlg_Prompt("[SPACE] sleep (SAVE FAILED)"); } //----------------------------------------------------------------------------- void AdvanceDay() { g_Save.day++; 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 g_DiskErr = 0; g_SaveSector = 14; // 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(); g_HasDisk = Disk_Probe(); 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; } } }