diff --git a/CHANGELOG.md b/CHANGELOG.md index 04df741..bf8b4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,13 @@ ## Unreleased - Made built-in Cria entity fields matter in the generated starter playtest: - pickup `amount` awards score; enemy `patrol` moves horizontally at its - `speed` interval; enemy `damage` spends multiple lives. Defaults now give - authored pickups one point and enemies one speed/damage unit; START restores - entity positions and all derived state. The behavior probe covers score, - patrol, damage, game-over, and checkpoint reset while custom renamed fields - remain metadata-only. + pickup `amount` awards score; pickup `respawn` restores a collected pickup + after finite-life loss while reversing its score and objective count; enemy + `patrol` moves horizontally at its `speed` interval; enemy `damage` spends + multiple lives. Defaults now give authored pickups one point and enemies one + speed/damage unit; START restores entity positions and all derived state. The + behavior probe covers score, pickup respawn, patrol, damage, game-over, and + checkpoint reset while custom renamed fields remain metadata-only. - Made ELIS prereleases directly deployable through `/ztash this`: the binary workflow now publishes an immutable two-platform `ztash-release.json` bound to the source commit, exact archive sizes, and SHA-256 hashes, with focused diff --git a/MEMORY.md b/MEMORY.md index 1139de2..e353346 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -37,10 +37,10 @@ Run `bash scripts/verify.sh`. It builds in `ReleaseSafe`, uses dummy SDL drivers - Full third-party ports are staged under `ports/` until content completeness, license isolation, deterministic package/simulator gates, and named physical-device proof. `demos/mr-rescue/` is the sole explicit physical-validation exception and remains unapproved; all 100 Hex-a-Hop levels follow only after named-board approval. - `.elisworld` schema v7 adds bounded starting lives, metadata/checkpoint trigger behavior, and the named built-in starter behaviors `enemy.speed`, -`enemy.patrol`, `enemy.damage`, and `pickup.amount`; v6 added the -reach-goal/collect-all objective rule, v5 added cardinal/blob terrain, and v4 -added project-defined names plus four typed fields per entity slot. These edits -use project snapshots in the same ordered history as map edits. History +`enemy.patrol`, `enemy.damage`, `pickup.amount`, and `pickup.respawn`; v6 +added the reach-goal/collect-all objective rule, v5 added cardinal/blob terrain, +and v4 added project-defined names plus four typed fields per entity slot. +- These edits use project snapshots in the same ordered history as map edits. History revisions remain monotonic across snapshots so dirty state cannot collide after save; project/settings writes use collision-free atomic temporary files, and dirty close requests require Save and Exit or explicit discard. V1 and v2 load diff --git a/README.md b/README.md index daefe5a..71b1a27 100644 --- a/README.md +++ b/README.md @@ -187,9 +187,10 @@ over the same authoritative project and command history: Save/Discard guard for mouse, keyboard, and controller users; - a chrome-free validated map preview, plus an interactive starter playtest that moves through collision, renders typed entities, counts and scores pickups, - patrols authored enemies, applies enemy damage, enforces authored objectives, - spends finite lives, and respawns at authored checkpoint triggers without - mutating the saved project; + restores authored respawning pickups after finite-life loss, patrols authored + enemies, applies enemy damage, enforces authored objectives, spends finite + lives, and respawns at authored checkpoint triggers without mutating the saved + project; Open a project using the game manifest and palette source as its asset workspace: diff --git a/STUDIO.md b/STUDIO.md index fb02bee..47818f6 100644 --- a/STUDIO.md +++ b/STUDIO.md @@ -48,7 +48,9 @@ world. Arrow buttons move one cell, solid cells block movement, and pickup entities are counted and award their authored `amount` score. Built-in entity field names also drive bounded behavior: enemy `patrol` moves horizontally at the authored `speed` turn interval and `damage` spends that many lives. -Patrolling enemies bounce from blocked or occupied cells. The authored +Collected pickups with `respawn` set to `1` return after a finite-life loss; +their score and objective count are reversed so collect-all objectives remain +honest. Patrolling enemies bounce from blocked or occupied cells. The authored objective either wins at the goal immediately or keeps it locked until every pickup is collected. Starting lives can be unlimited or finite; in finite mode game-over occurs at zero, and checkpoint-enabled trigger entities become the @@ -122,9 +124,9 @@ editor map preview, not a claim that game-specific mechanics are running. fields with defaults and bounds. The schema editor renames types and fields, adds or removes trailing fields, cycles field kinds, and edits defaults/ranges through the shared undo/redo history. The starter playtest recognizes the - built-in behavior names `enemy.speed`, `enemy.patrol`, `enemy.damage`, and - `pickup.amount`; custom names remain data-only. Non-decoration entities on - collision produce a warning. + built-in behavior names `enemy.speed`, `enemy.patrol`, `enemy.damage`, + `pickup.amount`, and `pickup.respawn`; custom names remain data-only. + Non-decoration entities on collision produce a warning. Removing or reenabling a trailing field initializes instances from its validated default; undo restores their previous schema and distinct values. Failed entity placement leaves the project and pending undo gesture unchanged. @@ -245,11 +247,12 @@ editor frame. `--playtest-smoke` generates the same temporary interactive runtime wrapper, copies the selected tilesets, runs objective, finite-life, game-over, -checkpoint-respawn, pickup-score, patrol, and enemy-damage probes plus one real -simulator screenshot, verifies the frame, and cleans the wrapper directory before -returning. The generated starter loop exercises movement, collision, entity -rendering, pickup scoring, patrol behavior, both goal rules, both trigger modes, -feedback, and full reset wiring; it does not replace hand-authored game logic. +checkpoint-respawn, pickup-score, pickup-respawn, patrol, and enemy-damage +probes plus one real simulator screenshot, verifies the frame, and cleans the +wrapper directory before returning. The generated starter loop exercises +movement, collision, entity rendering, pickup scoring and respawn, patrol +behavior, both goal rules, both trigger modes, feedback, and full reset +wiring; it does not replace hand-authored game logic. The toolbar PLAYTEST button and F9 use the interactive form and return to Cria after the simulator closes. diff --git a/scripts/studio_smoke.sh b/scripts/studio_smoke.sh index 3786d9c..29cd03c 100755 --- a/scripts/studio_smoke.sh +++ b/scripts/studio_smoke.sh @@ -36,7 +36,7 @@ env SDL_VIDEODRIVER=dummy \ --playtest-smoke \ --capture="$work/studio.bmp" \ --smoke 2>&1 | tee "$work/studio.log" -grep -q 'Cria generated playtest: pass (objectives, lives, checkpoints, entity behaviors)' "$work/studio.log" +grep -q 'Cria generated playtest: pass (objectives, lives, checkpoints, entity behaviors, pickup respawn)' "$work/studio.log" test -s "$work/starter.elisworld" test -s "$work/starter.lua" diff --git a/src/studio_app.zig b/src/studio_app.zig index eaa0529..5850511 100644 --- a/src/studio_app.zig +++ b/src/studio_app.zig @@ -1068,6 +1068,17 @@ const Studio = struct { \\ end \\end \\ + \\local function respawn_collected_pickups() + \\ for index, entity in ipairs(entities) do + \\ if entity.kind == "pickup" and collected[index] and + \\ entity_field(entity, "respawn", 0) == 1 then + \\ collected[index] = nil + \\ pickup_count = pickup_count - 1 + \\ score = score - math.max(0, entity_field(entity, "amount", 1)) + \\ end + \\ end + \\end + \\ \\local function lose_life(damage) \\ damage = math.max(1, damage or 1) \\ if starting_lives > 0 then @@ -1079,6 +1090,7 @@ const Studio = struct { \\ end \\ player_x = respawn_x \\ player_y = respawn_y + \\ if starting_lives > 0 then respawn_collected_pickups() end \\end \\ \\remember_entity_positions() @@ -1191,27 +1203,18 @@ const Studio = struct { \\ \\ width, height = 5, 1 \\ solid = {} - \\ entities = { { kind = "enemy", x = 2, y = 0, fields = { 1, 1, 1, 0 } } } + \\ entities = { { kind = "pickup", x = 1, y = 0, fields = { 0, 2, 1, 0 } }, { kind = "enemy", x = 2, y = 0, fields = { 0, 0, 1, 0 } } } \\ spawn = { x = 0, y = 0 } \\ goal = { x = 4, y = 0 } + \\ goal_rule = "reach" \\ starting_lives, trigger_rule = 3, "metadata" + \\ pickup_total = 1 \\ remember_entity_positions() \\ reset() \\ try_move(1, 0) - \\ assert(entities[1].x == 3 and lives == 3) - \\ try_move(1, 0) - \\ assert(player_x == 2 and entities[1].x == 4) - \\ - \\ entities = { { kind = "enemy", x = 1, y = 0, fields = { 0, 0, 2, 0 } } } - \\ spawn = { x = 0, y = 0 } - \\ goal = { x = 3, y = 0 } - \\ starting_lives, trigger_rule = 3, "metadata" - \\ remember_entity_positions() - \\ reset() - \\ try_move(1, 0) - \\ assert(player_x == 0 and lives == 1 and not game_over) + \\ assert(score == 2 and pickup_count == 1) \\ try_move(1, 0) - \\ assert(game_over and lives == -1) + \\ assert(lives == 2 and player_x == 0 and score == 0 and pickup_count == 0 and not collected[1]) \\ \\ width, height = 4, 1 \\ solid = {} @@ -1376,7 +1379,7 @@ const Studio = struct { self.notice = .playtest_failed; return; } - std.debug.print("Cria generated playtest: pass (objectives, lives, checkpoints, entity behaviors)\n", .{}); + std.debug.print("Cria generated playtest: pass (objectives, lives, checkpoints, entity behaviors, pickup respawn)\n", .{}); } else { const argv = [_][]const u8{ runtime_path, root }; if (!runPlaytestRuntime(self.allocator, io, root, &argv)) {