ludic/examples/library/combat_kit.ludic
Orkuncakilkaya ad548840c7
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 32s
ci / build-and-test (push) Successful in 2m49s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 30s
feat(engine): #90 atlas-aware Sprite component, #91 become from listeners, 0.3.x ergonomics batch
Closes the two open issues and lands the pending unreleased batch:

- #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex
  (scale/flip/tint), so cell / cell_span / strip ids of any size draw
  through the engine sprite-render system. examples/library/sprite_atlas
  is the pixel-readback regression.
- #91: `become` from an @On(Event) listener / global handler / plain
  function no longer segfaults the compiler; it emits @L_scene_leave()
  (a dispatch on the live scene id) so the leaving scene's on-exit runs.
  UI_* handles are readable from any code (widget table built on first
  use). examples/library/scene_menus covers it.
- fix: a windowed `ludicc -o` build that reaches the audio runtime only
  through the atlas/Assets preload import now links audio.ll +
  AVFoundation (the audio backend link was gated on a game-level
  Audio.* call, so any windowed game declaring Sprite failed to link).
- the hand-written "Unreleased" CHANGELOG section is converted to
  changesets under changes/ so `x release` generates it.
- plus the batch: engine-driven retained UI + UiClicked event, Overlay
  phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File
  namespaces, Sprite.strip, prefabs, managers, countdown fields,
  enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon
  packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3)
  fix, shooter centre-aim fix, reserved-word function diagnostic.

Verified: x test (124/124), x test-tools, check-impl, check-vocabulary,
check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 01:36:08 +03:00

46 lines
2.8 KiB
Text

# combat_kit.ludic — the shooter kit over ludic.gameplay: Dash (a dodge with i-frames),
# Melee (an arc swing with knockback and MeleeHit), build stats on Stats applied by
# Combat.damage (damage_pct, crit_pct, thorns; a Crit event), Stats.add, and the
# Dungeon package (arena, exits, entry points). Prints:
# 6846 153196 31 107110 6000 5002 1 80 97 11 10 700 110
program CombatKit {
import "ludic.core/components.ludic"
import "ludic.shooter/shooter.ludic"
import "ludic.dungeon/dungeon.ludic"
import "ludic.prefs/prefs.ludic"
property Foe { n: int = 0 }
model Hero { Position, Body, Collider, TopDown, Weapon, Faction, Stats, Dash, Melee }
model Creature { Position, Body, Collider, Faction, Stats, Foe }
model WorldRules { Solids }
var frames: int = 0
var hero: int = -1
@On(MeleeHit) handler H { print(5000 + target) }
@On(Crit) handler C { print(6000) }
handler Tick phase Update {
frames += 1
if frames == 1 {
Map.size(20, 15)
Dungeon.arena(Dungeon.random_style(), '#', '.')
Dungeon.set_exit(Side.North, 'D')
spawn WorldRules { Solids { tile: 16, wall: '#', solid2: 'D' } }
print(Map.get(x: 9, y: 0) * 100 + Map.get(x: 9, y: 7)) # 6846: the sealed door, the open lane
let e = Dungeon.entry_point(Side.South, 24, 14, 20)
print(e.x * 1000 + e.y) # 153196
print(Dungeon.opposite(Side.West) * 10 + Dungeon.at_edge(IVec2.make(0, 3))) # 31
Weapon.def(name: "p", fire_rate: 10, damage: 10, speed: 8, spread: 0, pellets: 1, pattern: 0)
hero = spawn Hero { Position { x: 100, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, TopDown { move_speed: 4, aim_mode: AimMode.Manual, aim_angle: 0 }, Faction { id: 1 },
Stats { hp: 100, max_hp: 100 }, Dash { frames: 3, speed: 9, cooldown_frames: 5 }, Melee { range: 30, damage: 7, knockback: 4 } }
Stats.add(hero, StatKind.CritPct, 100)
Stats.add(hero, StatKind.DamagePct, 50)
let c = Collider.center(hero); print(c.x * 1000 + c.y) # 107110
spawn Creature { Position { x: 120, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, Faction { id: 2 }, Stats { hp: 100, max_hp: 100, thorns: 3 }, Foe { n: 1 } }
Faction.set(1, 2, 2)
print(Melee.swing(hero)) # 1 hit (after 6000 and 5002)
for (s, f) in query [Stats, Foe] { print(s.hp) } # 80: 7 * 150% doubled by the crit
print(Stats.of(hero).hp) # 97: thorns
print(Dash.start(hero, 0, 0) * 10 + Dash.active(hero)) # 11
print(Prefs.max("best", 5) * 10 + Prefs.max("best", 3)) # 10
}
if frames == 7 { print(700 + Dash.active(hero)); print(Position.of(hero).x); quit() } # 700 110
}
}