wip(0.S2): migrate - a var nothing writes becomes a let; a state is keyed by its module, directory or program, so every program's plan agrees; paths normalized; program states named SceneDemoState / scene_demo_st; the LSP reads state, mut and entry/handler parameters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 15:31:34 +03:00
parent 3eda72e8c2
commit 5ffe50ed02
463 changed files with 72045 additions and 67421 deletions

View file

@ -8,43 +8,43 @@
enum KnightAct { Attack, Guard, Item, Flee }
enum MageAct { Attack, Heal, Guard }
handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if games_chronorift_st.game_mode == 1 {
handler Battle phase Update {
if game_mode == 1 {
let k = Input.key()
for (e) in query [Stats, {Enemy}] {
machine games_chronorift_st.turn_phase {
machine turn_phase {
state KnightMenu {
for (st, pt) in query [Stats, Party] {
if pt.slot == 0 {
if st.hp <= 0 {
become MageMenu
games_chronorift_st.menu_cursor = 0
menu_cursor = 0
} else {
if k == 'w' { games_chronorift_st.menu_cursor = max(0, games_chronorift_st.menu_cursor - 1) }
if k == 's' { games_chronorift_st.menu_cursor = min(3, games_chronorift_st.menu_cursor + 1) }
if k == 'w' { menu_cursor = max(0, menu_cursor - 1) }
if k == 's' { menu_cursor = min(3, menu_cursor + 1) }
if is_confirm(k) {
match games_chronorift_st.menu_cursor {
match menu_cursor {
KnightAct.Attack => {
let d = max(1, st.atk - e.def + Random.range(0, 4))
e.hp -= d
games_chronorift_st.player_hit = d
player_hit = d
}
KnightAct.Guard => { st.guard = 1; games_chronorift_st.player_hit = 0 }
KnightAct.Guard => { st.guard = 1; player_hit = 0 }
KnightAct.Item => {
if games_chronorift_st.potions > 0 {
games_chronorift_st.potions -= 1
if potions > 0 {
potions -= 1
st.hp = min(st.maxhp, st.hp + 24)
games_chronorift_st.player_hit = 24
} else { games_chronorift_st.player_hit = 0 }
player_hit = 24
} else { player_hit = 0 }
}
KnightAct.Flee => {
if Random.chance(50) { e.hp = 0; games_chronorift_st.game_mode = 0 }
else { games_chronorift_st.player_hit = 0 }
if Random.chance(50) { e.hp = 0; game_mode = 0 }
else { player_hit = 0 }
}
}
become KnightResolve
games_chronorift_st.menu_cursor = 0
menu_cursor = 0
}
}
}
@ -55,13 +55,13 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if k != 0 {
if e.hp <= 0 {
grant_xp(e.xp)
games_chronorift_st.gold += e.xp
if games_chronorift_st.enemy_type == 2 { games_chronorift_st.boss_down = 1; games_chronorift_st.game_mode = 3; games_chronorift_st.ack = 0 }
else { games_chronorift_st.game_mode = 0 }
gold += e.xp
if enemy_type == 2 { boss_down = 1; game_mode = 3; ack = 0 }
else { game_mode = 0 }
become KnightMenu
} else {
become MageMenu
games_chronorift_st.menu_cursor = 0
menu_cursor = 0
}
}
}
@ -72,14 +72,14 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if st.hp <= 0 {
become EnemyTurn
} else {
if k == 'i' { games_chronorift_st.menu_cursor = max(0, games_chronorift_st.menu_cursor - 1) }
if k == 'k' { games_chronorift_st.menu_cursor = min(2, games_chronorift_st.menu_cursor + 1) }
if k == 'i' { menu_cursor = max(0, menu_cursor - 1) }
if k == 'k' { menu_cursor = min(2, menu_cursor + 1) }
if k == 'j' {
match games_chronorift_st.menu_cursor {
match menu_cursor {
MageAct.Attack => {
let d = max(1, st.atk + 5 - e.def + Random.range(0, 5))
e.hp -= d
games_chronorift_st.player_hit = d
player_hit = d
}
MageAct.Heal => {
if st.mp >= 4 {
@ -87,13 +87,13 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
for (t2, p2) in query [Stats, Party] {
if p2.slot == 0 { t2.hp = min(t2.maxhp, t2.hp + 16) }
}
games_chronorift_st.player_hit = 16
} else { games_chronorift_st.player_hit = 0 }
player_hit = 16
} else { player_hit = 0 }
}
MageAct.Guard => { st.guard = 1; games_chronorift_st.player_hit = 0 }
MageAct.Guard => { st.guard = 1; player_hit = 0 }
}
become MageResolve
games_chronorift_st.menu_cursor = 0
menu_cursor = 0
}
}
}
@ -104,9 +104,9 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
if k != 0 {
if e.hp <= 0 {
grant_xp(e.xp)
games_chronorift_st.gold += e.xp
if games_chronorift_st.enemy_type == 2 { games_chronorift_st.boss_down = 1; games_chronorift_st.game_mode = 3; games_chronorift_st.ack = 0 }
else { games_chronorift_st.game_mode = 0 }
gold += e.xp
if enemy_type == 2 { boss_down = 1; game_mode = 3; ack = 0 }
else { game_mode = 0 }
become KnightMenu
} else {
become EnemyTurn
@ -123,7 +123,7 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
var ed = max(1, e.atk - t3.def + Random.range(0, 4))
if t3.guard == 1 { ed /= 2; t3.guard = 0 }
t3.hp -= ed
games_chronorift_st.enemy_hit = ed
enemy_hit = ed
done = 1
}
}
@ -132,10 +132,10 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
for (t4) in query [Stats, {Party}] {
if t4.hp > 0 { alive += 1 }
}
if alive == 0 { games_chronorift_st.game_mode = 2; games_chronorift_st.ack = 0 }
if alive == 0 { game_mode = 2; ack = 0 }
else {
become KnightMenu
games_chronorift_st.menu_cursor = 0
menu_cursor = 0
}
}
}
@ -144,29 +144,29 @@ handler Battle(games_chronorift_st: mut GamesChronoriftState) phase Update {
}
}
handler CleanBattle(games_chronorift_st: GamesChronoriftState) phase LateUpdate {
if games_chronorift_st.game_mode != 1 {
handler CleanBattle phase LateUpdate {
if game_mode != 1 {
for (e) in query [Stats, {Enemy}] {
despawn self()
}
}
}
handler Meta(games_chronorift_st: mut GamesChronoriftState) phase Update {
handler Meta phase Update {
let k = Input.key()
if games_chronorift_st.game_mode == 2 {
if k == 0 { games_chronorift_st.ack = 1 }
if game_mode == 2 {
if k == 0 { ack = 1 }
if k != 0 {
if games_chronorift_st.ack == 1 {
if ack == 1 {
for (h) in query [Stats, {Party}] { h.hp = h.maxhp }
games_chronorift_st.game_mode = 0
game_mode = 0
}
}
}
if games_chronorift_st.game_mode == 3 {
if k == 0 { games_chronorift_st.ack = 1 }
if game_mode == 3 {
if k == 0 { ack = 1 }
if k != 0 {
if games_chronorift_st.ack == 1 { games_chronorift_st.game_mode = 0 }
if ack == 1 { game_mode = 0 }
}
}
}

View file

@ -1,7 +1,7 @@
# draw.ludic — the Render system (overworld, battle, and overlay screens).
handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
let m = games_chronorift_st.game_mode
handler Draw phase Render {
let m = game_mode
Screen.clear(0x101018)
if m == 0 {
@ -31,7 +31,7 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
if c == 'D' { Screen.sprite(SPR_DOOR, sx, sy) }
if c == 'S' { Screen.sprite(SPR_DOOR, sx, sy) }
if c == 'B' {
if games_chronorift_st.boss_down == 0 { Screen.sprite(SPR_BOSS, sx, sy) }
if boss_down == 0 { Screen.sprite(SPR_BOSS, sx, sy) }
}
}
}
@ -47,9 +47,9 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.draw_text(8, 18, "HP", 0x88ff88, 1)
Screen.draw_number(24, 18, h.hp, 0xffffff, 1)
Screen.draw_text(8, 28, "G", 0xffd24a, 1)
Screen.draw_number(18, 28, games_chronorift_st.gold, 0xffffff, 1)
Screen.draw_number(18, 28, gold, 0xffffff, 1)
Screen.draw_text(54, 28, "PO", 0x88ffcc, 1)
Screen.draw_number(72, 28, games_chronorift_st.potions, 0xffffff, 1)
Screen.draw_number(72, 28, potions, 0xffffff, 1)
}
}
}
@ -58,13 +58,13 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.fill_rectangle(0, 0, 320, 176, 0x241a2e)
for (e, a) in query [Stats, Actor, {Enemy}] {
var bs = 5
if games_chronorift_st.enemy_type == 2 { bs = 7 }
if enemy_type == 2 { bs = 7 }
Screen.sprite_scaled(a.kind, 160 - bs * 8, 44, bs)
Screen.draw_text(100, 20, enemy_name(games_chronorift_st.enemy_type), 0xffffff, 1)
Screen.draw_text(100, 20, enemy_name(enemy_type), 0xffffff, 1)
Screen.fill_rectangle(100, 30, 120, 8, 0x401018)
Screen.fill_rectangle(100, 30, e.hp * 120 / e.maxhp, 8, 0xe04040)
}
let ph = games_chronorift_st.turn_phase
let ph = turn_phase
var act = 0
if ph >= 2 { act = 1 }
if ph >= 4 { act = 2 }
@ -83,14 +83,14 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.draw_text(hx + 36, 207, "MP", 0x88aaff, 1)
Screen.draw_number(hx + 56, 207, st.mp, 0xffffff, 1)
}
let cur = games_chronorift_st.menu_cursor
let cur = menu_cursor
if ph == 0 or ph == 1 {
Screen.draw_text(206, 181, "P1 KNIGHT", 0xffe08a, 1)
Screen.draw_text(214, 192, "ATTACK", 0xffffff, 1)
Screen.draw_text(214, 203, "DEFEND", 0xffffff, 1)
Screen.draw_text(214, 214, "ITEM", 0xffffff, 1)
Screen.draw_text(256, 214, "x", 0x777777, 1)
Screen.draw_number(264, 214, games_chronorift_st.potions, 0xffd24a, 1)
Screen.draw_number(264, 214, potions, 0xffd24a, 1)
Screen.draw_text(214, 225, "RUN", 0xffffff, 1)
if ph == 0 { Screen.draw_text(204, 192 + cur * 11, ">", 0xffff00, 1) }
}
@ -104,11 +104,11 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
if ph == 4 { Screen.draw_text(206, 200, "ENEMY TURN", 0xff8888, 1) }
if ph == 1 or ph == 3 {
Screen.draw_text(6, 232, "DMG", 0xffff88, 1)
Screen.draw_number(34, 232, games_chronorift_st.player_hit, 0xffffff, 1)
Screen.draw_number(34, 232, player_hit, 0xffffff, 1)
}
if ph == 4 {
Screen.draw_text(6, 232, "FOE HITS", 0xff8888, 1)
Screen.draw_number(66, 232, games_chronorift_st.enemy_hit, 0xffffff, 1)
Screen.draw_number(66, 232, enemy_hit, 0xffffff, 1)
}
}
@ -128,11 +128,11 @@ handler Draw(games_chronorift_st: GamesChronoriftState) phase Render {
Screen.draw_text(80, 104, "BUY POTION", 0xffffff, 1)
Screen.draw_text(180, 104, "10 G", 0xffd24a, 1)
Screen.draw_text(80, 122, "LEAVE", 0xffffff, 1)
Screen.draw_text(70, 104 + games_chronorift_st.menu_cursor * 18, ">", 0xffff00, 1)
Screen.draw_text(70, 104 + menu_cursor * 18, ">", 0xffff00, 1)
Screen.draw_text(80, 150, "GOLD", 0xffd24a, 1)
Screen.draw_number(120, 150, games_chronorift_st.gold, 0xffffff, 1)
Screen.draw_number(120, 150, gold, 0xffffff, 1)
Screen.draw_text(160, 150, "POTIONS", 0x88ffcc, 1)
Screen.draw_number(226, 150, games_chronorift_st.potions, 0xffffff, 1)
Screen.draw_number(226, 150, potions, 0xffffff, 1)
}
Screen.show()
}

View file

@ -1,6 +1,6 @@
# overworld.ludic — boot, movement/encounter input, and the shop system.
handler Boot(games_chronorift_st: mut GamesChronoriftState) phase Start {
handler Boot phase Start {
png_load("assets/kenney/tiny-town/Tiles/tile_0000.png") # 0 grass
png_load("assets/kenney/tiny-town/Tiles/tile_0004.png") # 1 tree
png_load("assets/kenney/tiny-town/Tiles/tile_0025.png") # 2 path
@ -25,16 +25,16 @@ handler Boot(games_chronorift_st: mut GamesChronoriftState) phase Start {
Party { slot: 1 }
Stats { hp: 26, maxhp: 26, mp: 16, maxmp: 16, atk: 8, def: 4, lvl: 1, xp: 0, guard: 0 }
}
games_chronorift_st.game_mode = 0
games_chronorift_st.encounter = 4
games_chronorift_st.world_map = 0
games_chronorift_st.boss_down = 0
games_chronorift_st.gold = 20
games_chronorift_st.potions = 2
game_mode = 0
encounter = 4
world_map = 0
boss_down = 0
gold = 20
potions = 2
}
handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
if games_chronorift_st.game_mode == 0 {
handler Control phase Input {
if game_mode == 0 {
for (p) in query [Pos, {Player}] {
let k = Input.key()
var nx = p.x
@ -57,43 +57,43 @@ handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
build_dungeon(0)
p.x = 3
p.y = 2
games_chronorift_st.world_map = 1
world_map = 1
}
if tc == 'S' {
build_field(0)
p.x = 36
p.y = 3
games_chronorift_st.world_map = 0
world_map = 0
}
if tc == 'B' {
if games_chronorift_st.boss_down == 0 {
games_chronorift_st.enemy_type = 2
if boss_down == 0 {
enemy_type = 2
spawn Enemy {
Actor { kind: 8 }
Stats { hp: 120, maxhp: 120, mp: 0, maxmp: 0, atk: 18, def: 10, lvl: 5, xp: 300, guard: 0 }
}
games_chronorift_st.game_mode = 1
games_chronorift_st.turn_phase = 0
games_chronorift_st.menu_cursor = 0
game_mode = 1
turn_phase = 0
menu_cursor = 0
}
}
if tc == 'h' {
games_chronorift_st.game_mode = 4
games_chronorift_st.menu_cursor = 0
game_mode = 4
menu_cursor = 0
}
if tc != 'D' {
if tc != 'S' {
if tc != 'B' {
if tc != 'h' {
games_chronorift_st.encounter -= 1
if games_chronorift_st.encounter <= 0 {
games_chronorift_st.encounter = Random.range(4, 9)
encounter -= 1
if encounter <= 0 {
encounter = Random.range(4, 9)
var et = 0
if games_chronorift_st.world_map == 1 { et = 1 }
if games_chronorift_st.world_map == 0 {
if world_map == 1 { et = 1 }
if world_map == 0 {
if Random.chance(40) { et = 1 }
}
games_chronorift_st.enemy_type = et
enemy_type = et
if et == 0 {
spawn Enemy {
Actor { kind: 6 }
@ -106,9 +106,9 @@ handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
Stats { hp: 30, maxhp: 30, mp: 0, maxmp: 0, atk: 11, def: 5, lvl: 1, xp: 15, guard: 0 }
}
}
games_chronorift_st.game_mode = 1
games_chronorift_st.turn_phase = 0
games_chronorift_st.menu_cursor = 0
game_mode = 1
turn_phase = 0
menu_cursor = 0
}
}
}
@ -122,19 +122,19 @@ handler Control(games_chronorift_st: mut GamesChronoriftState) phase Input {
}
# shop (at the house 'h' tile): buy potions with gold
handler Shop(games_chronorift_st: mut GamesChronoriftState) phase Update {
if games_chronorift_st.game_mode == 4 {
handler Shop phase Update {
if game_mode == 4 {
let k = Input.key()
if k == 'w' { games_chronorift_st.menu_cursor = max(0, games_chronorift_st.menu_cursor - 1) }
if k == 's' { games_chronorift_st.menu_cursor = min(1, games_chronorift_st.menu_cursor + 1) }
if k == 'w' { menu_cursor = max(0, menu_cursor - 1) }
if k == 's' { menu_cursor = min(1, menu_cursor + 1) }
if is_confirm(k) {
if games_chronorift_st.menu_cursor == 0 {
if games_chronorift_st.gold >= 10 {
games_chronorift_st.gold -= 10
games_chronorift_st.potions += 1
if menu_cursor == 0 {
if gold >= 10 {
gold -= 10
potions += 1
}
}
if games_chronorift_st.menu_cursor == 1 { games_chronorift_st.game_mode = 0 }
if menu_cursor == 1 { game_mode = 0 }
}
}
}

View file

@ -29,17 +29,15 @@ const SPR_MAGE: int = 9
const SPR_FLOOR: int = 10
const SPR_WALL: int = 11
export state GamesChronoriftState {
game_mode: int = 0 # 0 field, 1 battle, 2 gameover, 3 victory
turn_phase: int = 0
menu_cursor: int = 0
encounter: int = 0
enemy_type: int = 0 # 0 slime, 1 skeleton, 2 boss
player_hit: int = 0
enemy_hit: int = 0
gold: int = 0
world_map: int = 0 # 0 overworld, 1 dungeon
boss_down: int = 0 # boss defeated flag
ack: int = 0 # debounce for end screens
potions: int = 0 # potions in inventory
}
var game_mode: int = 0 # 0 field, 1 battle, 2 gameover, 3 victory
var turn_phase: int = 0
var menu_cursor: int = 0
var encounter: int = 0
var enemy_type: int = 0 # 0 slime, 1 skeleton, 2 boss
var player_hit: int = 0
var enemy_hit: int = 0
var gold: int = 0
var world_map: int = 0 # 0 overworld, 1 dungeon
var boss_down: int = 0 # boss defeated flag
var ack: int = 0 # debounce for end screens
var potions: int = 0 # potions in inventory

View file

@ -9,24 +9,22 @@ program Menu {
const FONT_PATH: string = "/System/Library/Fonts/Supplemental/Arial.ttf"
const BACKDROP: int = 0x0e0e16
state MenuState {
title_font: int = 0
}
var title_font: int = 0
ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c border: 0x4a4a72 skin: "assets/ui/panel.png" inset: 10 align: center {
label text: "CHRONO RIFT" font: MenuState.title_font size: 26 fg: 0xffe060 align: center
label text: "a Ludic retained-UI demo" font: MenuState.title_font size: 14 fg: 0x9aa0c8 align: center
label text: "CHRONO RIFT" font: title_font size: 26 fg: 0xffe060 align: center
label text: "a Ludic retained-UI demo" font: title_font size: 14 fg: 0x9aa0c8 align: center
spacer h: 4
button id: NewGame text: "New Game" font: MenuState.title_font size: 16 w: 236
button id: Continue text: "Continue" font: MenuState.title_font size: 16 w: 236
button id: Options text: "Options" font: MenuState.title_font size: 16 w: 236
button id: Quit text: "Quit" font: MenuState.title_font size: 16 w: 236
button id: NewGame text: "New Game" font: title_font size: 16 w: 236
button id: Continue text: "Continue" font: title_font size: 16 w: 236
button id: Options text: "Options" font: title_font size: 16 w: 236
button id: Quit text: "Quit" font: title_font size: 16 w: 236
}
}
handler Boot(menu_st: mut MenuState) phase Start {
menu_st.title_font = Font.load(FONT_PATH)
handler Boot phase Start {
title_font = Font.load(FONT_PATH)
Ui.build()
Ui.open(UI_MainMenu)
}

View file

@ -33,10 +33,8 @@ program NpcAiDemo {
@OnSpawn(Ally) handler AInit { }
@OnSpawn(Dummy) handler DInit { }
state NpcaidemoState {
spotted: int = 0
}
@On(TargetSpotted) handler OnSpot(npcaidemo_st: mut NpcaidemoState) { npcaidemo_st.spotted += 1 }
var spotted: int = 0
@On(TargetSpotted) handler OnSpot { spotted += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@ -57,7 +55,7 @@ program NpcAiDemo {
return -1
}
entry (npcaidemo_st: NpcaidemoState) {
entry {
let PBr = World.prop_id("Brain")
Weapon.def("blaster", 4, 10, 10, 0, 1, 0)
@ -74,7 +72,7 @@ program NpcAiDemo {
# ---- A. perception: the enemy spots the hostile dummy -------------------
tick_n(4)
print(bi(npcaidemo_st.spotted >= 1)) # 1 — TargetSpotted fired
print(bi(spotted >= 1)) # 1 — TargetSpotted fired
print(bi(Ai.target(enemy) == dummy)) # 1 — remembers the right target
# ---- B. FSM attack: in range -> auto-aim + fire -> dummy takes damage ---

View file

@ -32,15 +32,13 @@ program PlatformerDemo {
@OnSpawn(Floor) handler FloorInit { }
@OnSpawn(Wall) handler WallInit { }
state PlatformerdemoState {
jumps: int = 0
have_power: int = 0
vetoed_air: int = 0
}
@On(JumpPerformed) handler CountJumps(platformerdemo_st: mut PlatformerdemoState) { platformerdemo_st.jumps += 1 }
var jumps: int = 0
@On(JumpPerformed) handler CountJumps { jumps += 1 }
# a game rule: forbid the air (double) jump unless a "power" flag is set — pure
# lever-4 veto, no controller edit.
@On(JumpRequested) handler GateAir(platformerdemo_st: mut PlatformerdemoState) { if (kind == 2) and (platformerdemo_st.have_power == 0) { platformerdemo_st.vetoed_air = 1; cancel } }
var have_power: int = 0
var vetoed_air: int = 0
@On(JumpRequested) handler GateAir { if (kind == 2) and (have_power == 0) { vetoed_air = 1; cancel } }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@ -49,7 +47,7 @@ program PlatformerDemo {
return World.get(e, P, World.field_id(P, "coyote_t"))
}
entry (platformerdemo_st: mut PlatformerdemoState) {
entry {
let PB = World.prop_id("Body")
let PP = World.prop_id("Position")
let PL = World.prop_id("Platformer")
@ -91,16 +89,16 @@ program PlatformerDemo {
print(bi(pf_coyote(h) == 6)) # 1
# ---- D. double jump gated by the veto hook -----------------------------
platformerdemo_st.have_power = 0
have_power = 0
Platformer.jump(h); tick_n(3) # ground jump -> airborne
let after_ground = platformerdemo_st.jumps
let after_ground = jumps
Platformer.jump(h); tick_n(2) # air jump attempt -> vetoed
print(bi((platformerdemo_st.jumps == after_ground) and (platformerdemo_st.vetoed_air == 1))) # 1
print(bi((jumps == after_ground) and (vetoed_air == 1))) # 1
platformerdemo_st.have_power = 1
let before_power = platformerdemo_st.jumps
have_power = 1
let before_power = jumps
Platformer.jump(h); tick_n(2) # air jump now allowed
print(bi(platformerdemo_st.jumps == before_power + 1)) # 1
print(bi(jumps == before_power + 1)) # 1
tick_n(60) # settle on the floor
# ---- E. horizontal run into the wall (collision reuse) -----------------

View file

@ -33,19 +33,17 @@ program PlatformerScaffolding {
@OnSpawn(Bouncer) handler BI { }
@OnSpawn(Spike) handler SI { }
state PlatformerscaffoldingState {
picked: int = 0
sprang: int = 0
hurt: int = 0
}
@On(ItemPickedUp) handler OnPick(platformerscaffolding_st: mut PlatformerscaffoldingState) { platformerscaffolding_st.picked += 1 }
@On(Sprang) handler OnSpring(platformerscaffolding_st: mut PlatformerscaffoldingState) { platformerscaffolding_st.sprang += 1 }
@On(Hurt) handler OnHurt(platformerscaffolding_st: mut PlatformerscaffoldingState) { platformerscaffolding_st.hurt += 1 }
var picked: int = 0
@On(ItemPickedUp) handler OnPick { picked += 1 }
var sprang: int = 0
@On(Sprang) handler OnSpring { sprang += 1 }
var hurt: int = 0
@On(Hurt) handler OnHurt { hurt += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
entry (platformerscaffolding_st: PlatformerscaffoldingState) {
entry {
let PP = World.prop_id("Position")
let PB = World.prop_id("Body")
let PL = World.prop_id("Platformer")
@ -71,7 +69,7 @@ program PlatformerScaffolding {
spawn Coin { Position { x: World.get(h, PP, fx), y: World.get(h, PP, fy) },
Collider { w: 10, h: 10, is_trigger: 1 }, Pickup { kind: 0, value: 5 } }
tick_n(2)
print(bi(platformerscaffolding_st.picked == 1)) # 1 — ItemPickedUp fired
print(bi(picked == 1)) # 1 — ItemPickedUp fired
print(bi(Score.get(h) == 5)) # 1 — value scored
# ---- spring launches the hero upward -----------------------------------
@ -81,13 +79,13 @@ program PlatformerScaffolding {
let vy_before = World.get(h, PB, World.field_id(PB, "vy"))
tick_n(2)
let vy_after = World.get(h, PB, World.field_id(PB, "vy"))
print(bi((platformerscaffolding_st.sprang >= 1) and (vy_after < 0))) # 1 — launched (vy negative = up)
print(bi((sprang >= 1) and (vy_after < 0))) # 1 — launched (vy negative = up)
# ---- hazard damages a Life --------------------------------------------
spawn Spike { Position { x: World.get(h, PP, fx), y: World.get(h, PP, fy) },
Collider { w: 12, h: 12, is_trigger: 1 }, Hazard { dmg: 1, knockback: 6 } }
tick_n(2)
print(bi(platformerscaffolding_st.hurt >= 1)) # 1 — Hurt fired
print(bi(hurt >= 1)) # 1 — Hurt fired
let PLife = World.prop_id("Life")
print(bi(World.get(h, PLife, World.field_id(PLife, "hp")) < 3)) # 1 — hp dropped

View file

@ -38,16 +38,14 @@ program RpgDemo {
@OnSpawn(Cfg) handler CI { }
# a locked-door rule: veto a rightward grid step while `locked` is set (lever 4).
state RpgdemoState {
locked: int = 0
entered: int = 0
talked: int = 0
quest_done: int = 0
}
@On(MoveRequested) handler LockDoor(rpgdemo_st: RpgdemoState) { if (rpgdemo_st.locked == 1) and (dx == 1) { cancel } }
@On(TileEntered) handler CountEnter(rpgdemo_st: mut RpgdemoState) { rpgdemo_st.entered += 1 }
@On(Interacted) handler OnTalk(rpgdemo_st: mut RpgdemoState) { if target >= 0 { rpgdemo_st.talked += 1 } }
@On(QuestCompleted) handler OnQuestDone(rpgdemo_st: mut RpgdemoState) { rpgdemo_st.quest_done += 1 }
var locked: int = 0
@On(MoveRequested) handler LockDoor { if (locked == 1) and (dx == 1) { cancel } }
var entered: int = 0
@On(TileEntered) handler CountEnter { entered += 1 }
var talked: int = 0
@On(Interacted) handler OnTalk { if target >= 0 { talked += 1 } }
var quest_done: int = 0
@On(QuestCompleted) handler OnQuestDone { quest_done += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@ -55,7 +53,7 @@ program RpgDemo {
function py(e: int) -> int { let P = World.prop_id("Position"); return World.get(e, P, World.field_id(P, "y")) }
function hp(e: int) -> int { let P = World.prop_id("Stats"); return World.get(e, P, World.field_id(P, "hp")) }
entry (rpgdemo_st: mut RpgdemoState) {
entry {
let PM = World.prop_id("Mover")
# a 6x3 tile world; a wall glyph at cell (3,1)
@ -73,7 +71,7 @@ program RpgDemo {
# move down into the open cell (16,32)
Mover.move(h, 0, 1); tick_n(1); Mover.move(h, 0, 0); tick_n(1)
print(bi((px(h) == 16) and (py(h) == 32))) # 1 — stepped one cell down
print(bi(rpgdemo_st.entered >= 1)) # 1 — TileEntered fired
print(bi(entered >= 1)) # 1 — TileEntered fired
# try to walk into the wall at cell (3,1)=(48,16): from (32,16) moving right blocked
World.set(h, World.prop_id("Position"), World.field_id(World.prop_id("Position"), "x"), 32)
@ -83,17 +81,17 @@ program RpgDemo {
# MoveRequested veto: a locked door forbids the rightward step (open cell now)
World.set(h, World.prop_id("Position"), World.field_id(World.prop_id("Position"), "y"), 32) # row 2 open
rpgdemo_st.locked = 1
locked = 1
Mover.move(h, 1, 0); tick_n(1); Mover.move(h, 0, 0)
print(bi(px(h) == 32)) # 1 — vetoed, no move
rpgdemo_st.locked = 0
locked = 0
Mover.move(h, 1, 0); tick_n(1); Mover.move(h, 0, 0)
print(bi(px(h) == 48)) # 1 — now allowed
# interact: an NPC one cell ahead (facing right after the last move)
spawn Npc { Position { x: 64, y: 32 } }
print(bi(Mover.interact(h) >= 0)) # 1 — found the NPC ahead
print(bi(rpgdemo_st.talked == 1)) # 1 — Interacted fired
print(bi(talked == 1)) # 1 — Interacted fired
# ================= Module B: inventory + equipment =================
Item.def("herb", 99, 1, 2)
@ -122,7 +120,7 @@ program RpgDemo {
Quest.start("hunt")
Quest.notify(0, 0, 1); Quest.notify(0, 0, 1); Quest.notify(0, 0, 1)
print(bi(Quest.state_of("hunt") == 3)) # 1 — auto-completed
print(bi(rpgdemo_st.quest_done == 1)) # 1 — QuestCompleted fired
print(bi(quest_done == 1)) # 1 — QuestCompleted fired
# ================= Module E: dialog =================
let dn0 = Dialog.node("elder", "hello")

View file

@ -30,21 +30,19 @@ program ShooterDemo {
@OnSpawn(Mob) handler MInit { }
@OnSpawn(Nest) handler NInit { }
state ShooterdemoState {
spawn_reqs: int = 0
waves_cleared: int = 0
spawned: int = 0
hits: int = 0
deaths: int = 0
}
@On(SpawnRequested) handler OnSpawnReq(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.spawn_reqs += 1 }
@On(WaveCleared) handler OnWaveClear(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.waves_cleared += 1 }
var spawn_reqs: int = 0
@On(SpawnRequested) handler OnSpawnReq { spawn_reqs += 1 }
var waves_cleared: int = 0
@On(WaveCleared) handler OnWaveClear { waves_cleared += 1 }
@On(ProjectileSpawned) handler CountSpawn(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.spawned += 1 }
@On(ProjectileHit) handler CountHit(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.hits += 1 }
var spawned: int = 0
@On(ProjectileSpawned) handler CountSpawn { spawned += 1 }
var hits: int = 0
@On(ProjectileHit) handler CountHit { hits += 1 }
# reap the dead (the realistic policy: a Died body leaves the world) so stale
# corpses do not sit in the faction/targeting scans.
@On(Died) handler Reap(shooterdemo_st: mut ShooterdemoState) { shooterdemo_st.deaths += 1; despawn e }
var deaths: int = 0
@On(Died) handler Reap { deaths += 1; despawn e }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
@ -74,7 +72,7 @@ program ShooterDemo {
return -1
}
entry (shooterdemo_st: ShooterdemoState) {
entry {
let PT = World.prop_id("TopDown")
let PB = World.prop_id("Body")
let PP = World.prop_id("Position")
@ -112,7 +110,7 @@ program ShooterDemo {
TopDown.fire(g, 0)
let hp_before = hp_of(mob)
tick_n(40) # bullet flies ~120px and hits
print(bi(shooterdemo_st.spawned >= 1)) # 1 — a projectile spawned
print(bi(spawned >= 1)) # 1 — a projectile spawned
print(bi(hp_of(mob) < hp_before)) # 1 — enemy took damage
# ---- C. friendly fire is ignored (same faction) ------------------------
@ -127,29 +125,29 @@ program ShooterDemo {
print(bi(hp_of(ally) == ally_hp)) # 1 — ally unharmed (faction)
# ---- D. shotgun spread emits multiple pellets in one shot ---------------
let s0 = shooterdemo_st.spawned
let s0 = spawned
Weapon.equip(g, "shotgun")
TopDown.aim(g, 0)
TopDown.fire(g, 1); tick_n(1); TopDown.fire(g, 0)
print(bi(shooterdemo_st.spawned - s0 == 5)) # 1 — 5 pellets
print(bi(spawned - s0 == 5)) # 1 — 5 pellets
# ---- E. a fresh enemy is eventually killed (Combat -> Died -> reaped) --
let d0 = shooterdemo_st.deaths
let d0 = deaths
Weapon.equip(g, "rifle")
TopDown.aim(g, 0)
var f = 0
while (shooterdemo_st.deaths == d0) and (f < 400) {
while (deaths == d0) and (f < 400) {
TopDown.fire(g, 1); tick_fixed(); TopDown.fire(g, 0); tick_fixed()
f += 1
}
print(bi(shooterdemo_st.deaths == d0 + 1)) # 1 — enemy destroyed (Died fired)
print(bi(deaths == d0 + 1)) # 1 — enemy destroyed (Died fired)
# ---- F. ring pattern (bullet-hell) emits N bullets around 360 ----------
let r0 = shooterdemo_st.spawned
let r0 = spawned
let ring = Weapon.def("nova", 30, 3, 6, 0, 8, 2) # pattern 2 = ring, 8 pellets
Weapon.equip(g, "nova")
TopDown.fire(g, 1); tick_n(1); TopDown.fire(g, 0)
print(bi(shooterdemo_st.spawned - r0 == 8)) # 1 — 8-way ring
print(bi(spawned - r0 == 8)) # 1 — 8-way ring
# ---- G. homing shot curves onto an off-axis enemy ----------------------
let seek = Weapon.def("seeker", 30, 40, 6, 0, 1, 0)
@ -168,8 +166,8 @@ program ShooterDemo {
# ---- H. wave spawner: budgeted, throttled SpawnRequested + WaveCleared --
spawn Nest { Position { x: 300, y: 300 }, Spawner { rate: 3, budget: 4 } }
tick_n(20) # 4 spawns at 3-frame spacing, then cleared
print(bi(shooterdemo_st.spawn_reqs == 4)) # 1 — exactly budget requests
print(bi(shooterdemo_st.waves_cleared == 1)) # 1 — wave cleared once
print(bi(spawn_reqs == 4)) # 1 — exactly budget requests
print(bi(waves_cleared == 1)) # 1 — wave cleared once
quit()
}

View file

@ -24,19 +24,17 @@ program Snake {
const MODE_OVER: int = 1
# ---- the game's whole state, named ----------------------------------------
state SnakeState {
direction: int = DIR_RIGHT
length: int = 3
tick: int = 0
food_x: int = 14
food_y: int = 7
mode: int = MODE_PLAY
score: int = 0
ate: int = 0 # did the head reach the food this step?
tail_x: int = 0 # where the tail sat before it moved (grow here)
tail_y: int = 0
acked: int = 0 # game-over key must be released before restart
}
var direction: int = DIR_RIGHT
var length: int = 3
var tick: int = 0
var food_x: int = 14
var food_y: int = 7
var mode: int = MODE_PLAY
var score: int = 0
var ate: int = 0 # did the head reach the food this step?
var tail_x: int = 0 # where the tail sat before it moved (grow here)
var tail_y: int = 0
var acked: int = 0 # game-over key must be released before restart
# ---- custom shades this game uses (values the named palette doesn't carry) -
const SHADE_BG: int = 0x0d1020 # backdrop behind the board
@ -48,48 +46,48 @@ program Snake {
const SHADE_SCORE: int = 0xffe060
const SHADE_OVER: int = 0xff5555
function reset(snake_st: mut SnakeState, unused: int) -> int {
function reset(unused: int) -> int {
for (p, s) in query [Pos, Seg] { despawn self() }
spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } }
spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } }
spawn S2 { Seg { order: 2 }; Pos { x: 8, y: 7 } }
snake_st.direction = DIR_RIGHT
snake_st.length = 3
snake_st.tick = 0
snake_st.mode = MODE_PLAY
snake_st.score = 0
snake_st.food_x = 14
snake_st.food_y = 7
direction = DIR_RIGHT
length = 3
tick = 0
mode = MODE_PLAY
score = 0
food_x = 14
food_y = 7
return 0
}
handler Boot(snake_st: mut SnakeState) phase Start { reset(snake_st, 0) }
handler Boot phase Start { reset(0) }
handler Input(snake_st: mut SnakeState) phase Input {
handler Input phase Input {
let k = Input.key()
if snake_st.mode == MODE_PLAY {
if k == 'w' { if snake_st.direction != DIR_DOWN { snake_st.direction = DIR_UP } }
if k == 's' { if snake_st.direction != DIR_UP { snake_st.direction = DIR_DOWN } }
if k == 'a' { if snake_st.direction != DIR_RIGHT { snake_st.direction = DIR_LEFT } }
if k == 'd' { if snake_st.direction != DIR_LEFT { snake_st.direction = DIR_RIGHT } }
if mode == MODE_PLAY {
if k == 'w' { if direction != DIR_DOWN { direction = DIR_UP } }
if k == 's' { if direction != DIR_UP { direction = DIR_DOWN } }
if k == 'a' { if direction != DIR_RIGHT { direction = DIR_LEFT } }
if k == 'd' { if direction != DIR_LEFT { direction = DIR_RIGHT } }
}
if snake_st.mode == MODE_OVER {
if k == 0 { snake_st.acked = 1 }
if k != 0 { if snake_st.acked == 1 { reset(snake_st, 0) } }
if mode == MODE_OVER {
if k == 0 { acked = 1 }
if k != 0 { if acked == 1 { reset(0) } }
}
}
handler Move(snake_st: mut SnakeState) phase Update {
if snake_st.mode == MODE_PLAY {
snake_st.tick += 1
if snake_st.tick >= 6 {
snake_st.tick = 0
handler Move phase Update {
if mode == MODE_PLAY {
tick += 1
if tick >= 6 {
tick = 0
# remember the tail cell, then drag every segment onto the one ahead of it
for (pt, stg) in query [Pos, Seg] {
if stg.order == snake_st.length - 1 { snake_st.tail_x = pt.x; snake_st.tail_y = pt.y }
if stg.order == length - 1 { tail_x = pt.x; tail_y = pt.y }
}
for step in 0 .. snake_st.length {
let i = snake_st.length - 1 - step
for step in 0 .. length {
let i = length - 1 - step
if i >= 1 {
for (pa, sa) in query [Pos, Seg] {
if sa.order == i {
@ -101,18 +99,18 @@ program Snake {
}
}
# move the head, check the walls, check the food
snake_st.ate = 0
ate = 0
for (ph, sh) in query [Pos, Seg] {
if sh.order == 0 {
if snake_st.direction == DIR_UP { ph.y -= 1 }
if snake_st.direction == DIR_DOWN { ph.y += 1 }
if snake_st.direction == DIR_LEFT { ph.x -= 1 }
if snake_st.direction == DIR_RIGHT { ph.x += 1 }
if ph.x < 0 { snake_st.mode = MODE_OVER }
if ph.x >= GRID_W { snake_st.mode = MODE_OVER }
if ph.y < 0 { snake_st.mode = MODE_OVER }
if ph.y >= GRID_H { snake_st.mode = MODE_OVER }
if ph.x == snake_st.food_x { if ph.y == snake_st.food_y { snake_st.ate = 1 } }
if direction == DIR_UP { ph.y -= 1 }
if direction == DIR_DOWN { ph.y += 1 }
if direction == DIR_LEFT { ph.x -= 1 }
if direction == DIR_RIGHT { ph.x += 1 }
if ph.x < 0 { mode = MODE_OVER }
if ph.x >= GRID_W { mode = MODE_OVER }
if ph.y < 0 { mode = MODE_OVER }
if ph.y >= GRID_H { mode = MODE_OVER }
if ph.x == food_x { if ph.y == food_y { ate = 1 } }
}
}
# self-collision: head onto any body segment
@ -120,23 +118,23 @@ program Snake {
if sh.order == 0 {
for (pb, sb) in query [Pos, Seg] {
if sb.order > 0 {
if pb.x == ph.x { if pb.y == ph.y { snake_st.mode = MODE_OVER } }
if pb.x == ph.x { if pb.y == ph.y { mode = MODE_OVER } }
}
}
}
}
if snake_st.ate == 1 {
snake_st.score += 1
spawn Body { Seg { order: snake_st.length }; Pos { x: snake_st.tail_x, y: snake_st.tail_y } }
snake_st.length += 1
snake_st.food_x = Random.range(low: 0, high: GRID_W - 1)
snake_st.food_y = Random.range(low: 0, high: GRID_H - 1)
if ate == 1 {
score += 1
spawn Body { Seg { order: length }; Pos { x: tail_x, y: tail_y } }
length += 1
food_x = Random.range(low: 0, high: GRID_W - 1)
food_y = Random.range(low: 0, high: GRID_H - 1)
}
}
}
}
handler Draw(snake_st: SnakeState) phase Render {
handler Draw phase Render {
Screen.clear(SHADE_BG)
# checkerboard field
for gy in 0 .. GRID_H {
@ -147,7 +145,7 @@ program Snake {
}
}
# food, inset a few pixels inside its tile
Screen.fill_rectangle(x: snake_st.food_x * TILE + 3, y: snake_st.food_y * TILE + 3, width: TILE - 6, height: TILE - 6, color: SHADE_FOOD)
Screen.fill_rectangle(x: food_x * TILE + 3, y: food_y * TILE + 3, width: TILE - 6, height: TILE - 6, color: SHADE_FOOD)
# every snake segment, straight from the ECS
for (p, s) in query [Pos, Seg] {
var seg_color = SHADE_BODY
@ -155,8 +153,8 @@ program Snake {
Screen.fill_rectangle(x: p.x * TILE + 1, y: p.y * TILE + 1, width: TILE - 2, height: TILE - 2, color: seg_color)
}
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1)
Screen.draw_number(x: 52, y: 4, value: snake_st.score, color: SHADE_SCORE, scale: 1)
if snake_st.mode == MODE_OVER {
Screen.draw_number(x: 52, y: 4, value: score, color: SHADE_SCORE, scale: 1)
if mode == MODE_OVER {
Screen.draw_text(x: 112, y: 104, text: "GAME OVER", color: SHADE_OVER, scale: 2)
Screen.draw_text(x: 84, y: 140, text: "PRESS ANY KEY", color: Color.White, scale: 1)
}