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>
This commit is contained in:
parent
e9c2c51bc3
commit
ad548840c7
139 changed files with 58981 additions and 43671 deletions
|
|
@ -21,6 +21,8 @@ const WPN_MAX: int = 64
|
|||
# ---------------------------------------------------------------------------
|
||||
# Components
|
||||
# ---------------------------------------------------------------------------
|
||||
enum AimMode { Mouse, RightStick, MoveDirection, NearestEnemy, Manual, Auto } # TopDown.aim_mode; Auto = mouse, or the right stick while a pad is connected
|
||||
enum WeaponPattern { Single, Cone, Ring, Spiral } # Weapon.def pattern
|
||||
property TopDown {
|
||||
move_speed: int = 4, # px/frame
|
||||
accel: int = 80, # responsiveness %, gap closed / frame
|
||||
|
|
@ -32,7 +34,9 @@ property TopDown {
|
|||
want_x: int = 0, # -1/0/1 move intent (input OR ai writes)
|
||||
want_y: int = 0,
|
||||
want_fire: int = 0, # 1 = fire held (input OR ai writes)
|
||||
policy: int = 0
|
||||
policy: int = 0,
|
||||
reticle: int = 0, # a colour: the engine draws an aim line of reticle_length px (0 = none)
|
||||
reticle_length: int = 18 # and a cross at the mouse while aiming with it
|
||||
}
|
||||
|
||||
# a Weapon instance points at a registry entry (def_id). cooldown counts down to
|
||||
|
|
@ -40,6 +44,7 @@ property TopDown {
|
|||
property Weapon { def_id: int = 0, ammo: int = 0 - 1, cooldown: int = 0, phase: int = 0 }
|
||||
|
||||
property Projectile {
|
||||
color: int = 0xffffff, # drawn by the engine as a 3x3 square (Weapon.set_color)
|
||||
vx: fixed = 0.0, vy: fixed = 0.0, rx: fixed = 0.0, ry: fixed = 0.0,
|
||||
dir: int = 0, # heading in degrees (drives homing without re-deriving from velocity)
|
||||
speed: int = 8, damage: int = 1, ttl: int = 120,
|
||||
|
|
@ -71,6 +76,7 @@ var wpn_pattern: words = null
|
|||
var wpn_ttl: words = null
|
||||
var wpn_pierce: words = null
|
||||
var wpn_homing: words = null
|
||||
var wpn_color: words = null
|
||||
|
||||
function wpn_ensure() -> void {
|
||||
if wpn_reg == null {
|
||||
|
|
@ -78,6 +84,7 @@ function wpn_ensure() -> void {
|
|||
wpn_rate = words(WPN_MAX); wpn_dmg = words(WPN_MAX); wpn_speed = words(WPN_MAX)
|
||||
wpn_spread = words(WPN_MAX); wpn_pellets = words(WPN_MAX); wpn_pattern = words(WPN_MAX)
|
||||
wpn_ttl = words(WPN_MAX); wpn_pierce = words(WPN_MAX); wpn_homing = words(WPN_MAX)
|
||||
wpn_color = words(WPN_MAX); wpn_rate_base = words(WPN_MAX)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +97,8 @@ function wpn_ensure() -> void {
|
|||
wpn_rate[id] = fire_rate; wpn_dmg[id] = damage; wpn_speed[id] = speed
|
||||
wpn_spread[id] = spread; wpn_pellets[id] = pellets; wpn_pattern[id] = pattern
|
||||
wpn_ttl[id] = 120
|
||||
wpn_color[id] = 0xffffff
|
||||
wpn_rate_base[id] = fire_rate
|
||||
wpn_n = wpn_n + 1
|
||||
Dict.set(wpn_reg, name, id)
|
||||
return id
|
||||
|
|
@ -98,6 +107,23 @@ function wpn_ensure() -> void {
|
|||
wpn_ensure()
|
||||
return Dict.get_or(wpn_reg, name, 0 - 1)
|
||||
}
|
||||
# Weapon.set_rate(id, frames) — frames between shots (a fire-rate upgrade changes it in place).
|
||||
@Namespace(Weapon) function weapon_set_rate(id: int, frames: int) -> void {
|
||||
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_rate[id] = frames }
|
||||
}
|
||||
@Namespace(Weapon) function weapon_rate(id: int) -> int {
|
||||
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { return wpn_rate[id] }; return 0
|
||||
}
|
||||
# Weapon.reset(id) — back to the definition: no pierce, no homing, the defined fire rate.
|
||||
var wpn_rate_base: words = null
|
||||
@Namespace(Weapon) function weapon_reset(id: int) -> void {
|
||||
wpn_ensure(); if (id < 0) or (id >= WPN_MAX) { return }
|
||||
wpn_pierce[id] = 0; wpn_homing[id] = 0; wpn_rate[id] = wpn_rate_base[id]
|
||||
}
|
||||
# Weapon.set_color(id, color) — the colour the engine draws this weapon's shots in.
|
||||
@Namespace(Weapon) function weapon_set_color(id: int, color: int) -> void {
|
||||
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_color[id] = color }
|
||||
}
|
||||
# Weapon.set_ttl(id, frames) — projectile lifetime for a weapon (range control).
|
||||
@Namespace(Weapon) function weapon_set_ttl(id: int, frames: int) -> void {
|
||||
wpn_ensure(); if (id >= 0) and (id < WPN_MAX) { wpn_ttl[id] = frames }
|
||||
|
|
@ -163,8 +189,8 @@ function sh_nearest_enemy(self_e: int, fac: int, px: int, py: int) -> int {
|
|||
while e >= 0 {
|
||||
if (e != self_e) and (World.has(e, PP) != 0) {
|
||||
if Faction.hostile(fac, Faction.id_of(e)) == 1 {
|
||||
let dx = pos_get(e, "x") - px
|
||||
let dy = pos_get(e, "y") - py
|
||||
let dx = sh_cx(e) - px
|
||||
let dy = sh_cy(e) - py
|
||||
let d = dx * dx + dy * dy
|
||||
if (best < 0) or (d < bestd) { best = e; bestd = d }
|
||||
}
|
||||
|
|
@ -177,6 +203,169 @@ function sh_nearest_enemy(self_e: int, fac: int, px: int, py: int) -> int {
|
|||
# ===========================================================================
|
||||
# esys_topdown_move (FixedUpdate) — 2-axis acceleration toward the move intent.
|
||||
# ===========================================================================
|
||||
# ===========================================================================
|
||||
# Dash — the dodge roll: a burst of `speed` for `frames` frames in a fixed
|
||||
# direction, invulnerable while it lasts, then `cooldown_frames` before the next.
|
||||
# Dash.start(e, dx, dy) -> bool begin one (a zero direction dashes along the aim);
|
||||
# false while cooling down
|
||||
# Dash.active(e) -> bool mid-dash?
|
||||
# esys_dash (FixedUpdate, before the mover) overrides the movement intent and the
|
||||
# fire intent for the dash, restoring the walking speed when it ends.
|
||||
# ===========================================================================
|
||||
property Dash {
|
||||
frames: int = 10, speed: int = 11, cooldown_frames: int = 35,
|
||||
frames_left: countdown = 0, cooldown: countdown = 0, dx: int = 0, dy: int = 0, saved_speed: int = 0
|
||||
}
|
||||
# a component field by name (the reflected accessor the Dash / Melee helpers use)
|
||||
function sh_get(prop: pointer, e: int, name: pointer) -> int {
|
||||
let P = World.prop_id(prop); if P < 0 { return 0 }
|
||||
let f = World.field_id(P, name); if f < 0 { return 0 }
|
||||
return World.get(e, P, f)
|
||||
}
|
||||
function sh_set(prop: pointer, e: int, name: pointer, v: int) -> void {
|
||||
let P = World.prop_id(prop); if P < 0 { return }
|
||||
let f = World.field_id(P, name); if f >= 0 { World.set(e, P, f, v) }
|
||||
}
|
||||
function dash_get(e: int, name: pointer) -> int { return sh_get("Dash", e, name) }
|
||||
function dash_set(e: int, name: pointer, v: int) -> void { sh_set("Dash", e, name, v) }
|
||||
@Namespace(Dash) function dash_start(e: int, dx: int, dy: int) -> bool {
|
||||
if dash_get(e, "cooldown") > 0 { return false }
|
||||
var d = IVec2.make(dx, dy)
|
||||
if (dx == 0) and (dy == 0) { d = IVec2.step(td_get(e, "aim_angle")) }
|
||||
if (d.x == 0) and (d.y == 0) { d = IVec2.make(1, 0) }
|
||||
dash_set(e, "frames_left", dash_get(e, "frames"))
|
||||
dash_set(e, "cooldown", dash_get(e, "cooldown_frames"))
|
||||
dash_set(e, "dx", d.x)
|
||||
dash_set(e, "dy", d.y)
|
||||
dash_set(e, "saved_speed", td_get(e, "move_speed"))
|
||||
td_set(e, "move_speed", dash_get(e, "speed"))
|
||||
return true
|
||||
}
|
||||
@Namespace(Dash) function dash_active(e: int) -> bool { return dash_get(e, "frames_left") > 0 }
|
||||
@EngineSystem(Dash, FixedUpdate) function esys_dash() -> void {
|
||||
let P = World.prop_id("Dash")
|
||||
if P < 0 { return }
|
||||
var e = World.query_next(P, 0)
|
||||
while e >= 0 {
|
||||
if dash_get(e, "frames_left") > 0 {
|
||||
td_set(e, "want_x", dash_get(e, "dx"))
|
||||
td_set(e, "want_y", dash_get(e, "dy"))
|
||||
td_set(e, "want_fire", 0)
|
||||
} else { if dash_get(e, "saved_speed") > 0 {
|
||||
td_set(e, "move_speed", dash_get(e, "saved_speed"))
|
||||
dash_set(e, "saved_speed", 0)
|
||||
} }
|
||||
e = World.query_next(P, e + 1)
|
||||
}
|
||||
}
|
||||
@On(DamageAboutToApply) handler DashGuard { if dash_active(target) { cancel } } # i-frames while dashing
|
||||
|
||||
# ===========================================================================
|
||||
# Melee — a swing that hits every hostile inside an arc around the aim.
|
||||
# Melee.swing(e) -> int hits dealt (-1 while cooling down); each hit takes
|
||||
# `damage`, is pushed `knockback` px away (never into a
|
||||
# solid tile), and fires MeleeHit
|
||||
# Melee.ready(e) / Melee.active(e)
|
||||
# ===========================================================================
|
||||
property Melee {
|
||||
range: int = 24, half_arc: int = 50, damage: int = 10, knockback: int = 8,
|
||||
frames: int = 8, cooldown_frames: int = 20, frames_left: countdown = 0, cooldown: countdown = 0,
|
||||
arc: int = 0 # a colour: the engine draws the swing's arc while it lasts (0 = none)
|
||||
}
|
||||
# esys_melee_draw (Render): the swing as a circle at the reach, growing with its frames
|
||||
@EngineSystem(Melee, Render) function esys_melee_draw() -> void {
|
||||
let P = World.prop_id("Melee")
|
||||
if P < 0 { return }
|
||||
var e = World.query_next(P, 0)
|
||||
while e >= 0 {
|
||||
let color = melee_get(e, "arc")
|
||||
let left = melee_get(e, "frames_left")
|
||||
if (color != 0) and (left > 0) {
|
||||
let at = IVec2.along(IVec2.make(sh_cx(e), sh_cy(e)), td_get(e, "aim_angle"), melee_get(e, "range"))
|
||||
Screen.circle(x: at.x, y: at.y, radius: 6 + left, color: color)
|
||||
}
|
||||
e = World.query_next(P, e + 1)
|
||||
}
|
||||
}
|
||||
event MeleeHit { attacker: int = 0, target: int = 0 }
|
||||
function melee_get(e: int, name: pointer) -> int { return sh_get("Melee", e, name) }
|
||||
@Namespace(Melee) function melee_ready(e: int) -> bool { return melee_get(e, "cooldown") == 0 }
|
||||
@Namespace(Melee) function melee_active(e: int) -> bool { return melee_get(e, "frames_left") > 0 }
|
||||
@Namespace(Melee) function melee_swing(e: int) -> int {
|
||||
if melee_get(e, "cooldown") > 0 { return 0 - 1 }
|
||||
sh_set("Melee", e, "frames_left", melee_get(e, "frames"))
|
||||
sh_set("Melee", e, "cooldown", melee_get(e, "cooldown_frames"))
|
||||
let PF = World.prop_id("Faction")
|
||||
let PP = World.prop_id("Position")
|
||||
if (PF < 0) or (PP < 0) { return 0 }
|
||||
let me = IVec2.make(sh_cx(e), sh_cy(e))
|
||||
let aim = td_get(e, "aim_angle")
|
||||
let fac = Faction.id_of(e)
|
||||
var hits = 0
|
||||
var o = World.query_next(PF, 0)
|
||||
while o >= 0 {
|
||||
if (o != e) and (World.has(o, PP) != 0) and (Faction.hostile(fac, Faction.id_of(o)) == 1) {
|
||||
let other = IVec2.make(sh_cx(o), sh_cy(o))
|
||||
let reach = melee_get(e, "range") + sh_half_w(o)
|
||||
if IVec2.within(me, other, reach) {
|
||||
let heading = IVec2.heading(me, other)
|
||||
if abs(Angle.diff_degrees(a: aim, b: heading)) <= melee_get(e, "half_arc") {
|
||||
Combat.damage(o, e, melee_get(e, "damage"))
|
||||
let push = IVec2.scale(IVec2.step(heading), melee_get(e, "knockback"))
|
||||
let nx = pos_get(o, "x") + push.x
|
||||
let ny = pos_get(o, "y") + push.y
|
||||
if not Map.is_solid_at(nx, ny) { pos_set(o, "x", nx); pos_set(o, "y", ny) }
|
||||
emit MeleeHit(attacker: e, target: o)
|
||||
hits = hits + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
o = World.query_next(PF, o + 1)
|
||||
}
|
||||
return hits
|
||||
}
|
||||
function sh_half_w(e: int) -> int {
|
||||
let PC = World.prop_id("Collider")
|
||||
if (PC < 0) or (World.has(e, PC) == 0) { return 0 }
|
||||
return World.get(e, PC, World.field_id(PC, "w")) / 2
|
||||
}
|
||||
|
||||
# esys_topdown_draw (Render): the aim line, and a cross at the mouse while it is the aim
|
||||
@EngineSystem(TopDown, Render) function esys_topdown_draw() -> void {
|
||||
let P = World.prop_id("TopDown")
|
||||
if P < 0 { return }
|
||||
var e = World.query_next(P, 0)
|
||||
while e >= 0 {
|
||||
let color = td_get(e, "reticle")
|
||||
if color != 0 {
|
||||
let from = IVec2.make(sh_cx(e), sh_cy(e))
|
||||
let tip = IVec2.along(from, td_get(e, "aim_angle"), td_get(e, "reticle_length"))
|
||||
Screen.line(x1: from.x, y1: from.y, x2: tip.x, y2: tip.y, color: color)
|
||||
var mode = td_get(e, "aim_mode")
|
||||
if (mode == AimMode.Auto) and (not Input.pad_connected(0)) { mode = AimMode.Mouse }
|
||||
if mode == AimMode.Mouse {
|
||||
let mx = Input.mouse_x(); let my = Input.mouse_y()
|
||||
Screen.line(x1: mx - 3, y1: my, x2: mx + 3, y2: my, color: 0xffffff)
|
||||
Screen.line(x1: mx, y1: my - 3, x2: mx, y2: my + 3, color: 0xffffff)
|
||||
}
|
||||
}
|
||||
e = World.query_next(P, e + 1)
|
||||
}
|
||||
}
|
||||
|
||||
# esys_projectile_draw (Render, after the sprites): every shot as a 3x3 square in
|
||||
# its weapon's colour, through the camera, so a game draws no bullets by hand.
|
||||
@EngineSystem(Projectile, Render) function esys_projectile_draw() -> void {
|
||||
let P = World.prop_id("Projectile")
|
||||
if P < 0 { return }
|
||||
let fc = World.field_id(P, "color")
|
||||
var e = World.query_next(P, 0)
|
||||
while e >= 0 {
|
||||
if pr_get(e, "ttl") > 0 { Screen.fill_rectangle(x: pos_get(e, "x") - 1, y: pos_get(e, "y") - 1, width: 3, height: 3, color: World.get(e, P, fc)) }
|
||||
e = World.query_next(P, e + 1)
|
||||
}
|
||||
}
|
||||
|
||||
@EngineSystem(TopDown, FixedUpdate) function esys_topdown_move() -> void {
|
||||
let P = World.prop_id("TopDown")
|
||||
if P < 0 { return }
|
||||
|
|
@ -210,9 +399,10 @@ function sh_nearest_enemy(self_e: int, fac: int, px: int, py: int) -> int {
|
|||
if P < 0 { return }
|
||||
var e = World.query_next(P, 0)
|
||||
while e >= 0 {
|
||||
let mode = td_get(e, "aim_mode")
|
||||
let px = pos_get(e, "x")
|
||||
let py = pos_get(e, "y")
|
||||
var mode = td_get(e, "aim_mode")
|
||||
if mode == AimMode.Auto { mode = AimMode.Mouse; if Input.pad_connected(0) { mode = AimMode.RightStick } }
|
||||
let px = sh_cx(e)
|
||||
let py = sh_cy(e)
|
||||
var target = td_get(e, "aim_angle") # default: keep current
|
||||
var have = 0
|
||||
if mode == 0 { # mouse / point
|
||||
|
|
@ -234,7 +424,7 @@ function sh_nearest_enemy(self_e: int, fac: int, px: int, py: int) -> int {
|
|||
if mode == 3 { # nearest enemy (auto-aim)
|
||||
let fac = Faction.id_of(e)
|
||||
let t = sh_nearest_enemy(e, fac, px, py)
|
||||
if t >= 0 { target = sh_angle_to(px, py, pos_get(t, "x"), pos_get(t, "y")); have = 1 }
|
||||
if t >= 0 { target = sh_angle_to(px, py, sh_cx(t), sh_cy(t)); have = 1 }
|
||||
}
|
||||
if have == 1 {
|
||||
let tr = td_get(e, "turn_rate")
|
||||
|
|
@ -254,6 +444,23 @@ function sh_nearest_enemy(self_e: int, fac: int, px: int, py: int) -> int {
|
|||
# integer-degree angle helpers (built on Math trig, kept in degrees for the API)
|
||||
# both take small integer components (pixel deltas or -1/0/1 intent), so fixed()
|
||||
# stays well inside i32; results are wrapped into 0..359.
|
||||
# the centre of an entity's body: Position + Collider offset + half size when it has
|
||||
# a Collider (Position is a box's top-left anchor), else Position itself.
|
||||
function sh_cx(e: int) -> int {
|
||||
let PC = World.prop_id("Collider")
|
||||
if (PC >= 0) and (World.has(e, PC) != 0) {
|
||||
return pos_get(e, "x") + World.get(e, PC, World.field_id(PC, "offx")) + World.get(e, PC, World.field_id(PC, "w")) / 2
|
||||
}
|
||||
return pos_get(e, "x")
|
||||
}
|
||||
function sh_cy(e: int) -> int {
|
||||
let PC = World.prop_id("Collider")
|
||||
if (PC >= 0) and (World.has(e, PC) != 0) {
|
||||
return pos_get(e, "y") + World.get(e, PC, World.field_id(PC, "offy")) + World.get(e, PC, World.field_id(PC, "h")) / 2
|
||||
}
|
||||
return pos_get(e, "y")
|
||||
}
|
||||
|
||||
function sh_angle_to(ax: int, ay: int, bx: int, by: int) -> int {
|
||||
let r = Math.atan2(fixed(by - ay), fixed(bx - ax))
|
||||
return sh_wrap_deg(floor(Math.rad_to_deg(r)))
|
||||
|
|
@ -300,9 +507,11 @@ function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed
|
|||
World.set(b, PC, World.field_id(PC, "h"), 4)
|
||||
}
|
||||
# announce it: a listener may veto (cancel -> despawn) or tweak the Projectile.
|
||||
pr_set(b, "color", wpn_fire_color)
|
||||
if emit ProjectileSpawned(proj: b, owner: owner, damage: dmg) != 0 { despawn b; return 0 - 1 }
|
||||
return b
|
||||
}
|
||||
var wpn_fire_color: int = 0xffffff # the colour of the weapon being fired (sh_fire)
|
||||
|
||||
# ===========================================================================
|
||||
# esys_weapon (Update) — fire-rate gating + pattern emission. Reads the owner's
|
||||
|
|
@ -322,7 +531,7 @@ function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed
|
|||
let id = World.get(e, P, World.field_id(P, "def_id"))
|
||||
if (id >= 0) and (id < wpn_n) {
|
||||
sh_fire(e, id)
|
||||
World.set(e, P, World.field_id(P, "cooldown"), wpn_rate[id])
|
||||
World.set(e, P, World.field_id(P, "cooldown"), wpn_rate[id] * stats_field(e, "fire_rate_pct", 100) / 100)
|
||||
}
|
||||
}
|
||||
e = World.query_next(P, e + 1)
|
||||
|
|
@ -330,8 +539,8 @@ function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed
|
|||
}
|
||||
|
||||
function sh_fire(owner: int, id: int) -> void {
|
||||
let px = pos_get(owner, "x")
|
||||
let py = pos_get(owner, "y")
|
||||
let px = sh_cx(owner)
|
||||
let py = sh_cy(owner)
|
||||
let base = td_get(owner, "aim_angle")
|
||||
let fac = Faction.id_of(owner)
|
||||
let speed = wpn_speed[id]
|
||||
|
|
@ -342,6 +551,7 @@ function sh_fire(owner: int, id: int) -> void {
|
|||
let ttl = wpn_ttl[id]
|
||||
let pierce = wpn_pierce[id]
|
||||
let homing = wpn_homing[id]
|
||||
wpn_fire_color = wpn_color[id]
|
||||
var pellets = pellets0; if pellets < 1 { pellets = 1 }
|
||||
emit WeaponFired(owner: owner, def_id: id)
|
||||
if pattern == 2 { # ring: pellets around 360
|
||||
|
|
@ -387,6 +597,8 @@ function sh_fire(owner: int, id: int) -> void {
|
|||
let ttl = pr_get(e, "ttl") - 1
|
||||
if ttl <= 0 { emit ProjectileExpired(proj: e); despawn e; e = nxt; continue }
|
||||
pr_set(e, "ttl", ttl)
|
||||
# a solid tile (the Solids config) stops a shot; with no Solids nothing is solid
|
||||
if Map.is_solid_at(pos_get(e, "x"), pos_get(e, "y")) { emit ProjectileExpired(proj: e); despawn e; e = nxt; continue }
|
||||
# homing steer toward the nearest enemy of the projectile's faction
|
||||
let hom = pr_get(e, "homing")
|
||||
if hom > 0 {
|
||||
|
|
@ -394,7 +606,7 @@ function sh_fire(owner: int, id: int) -> void {
|
|||
let px0 = pos_get(e, "x"); let py0 = pos_get(e, "y")
|
||||
let t = sh_nearest_enemy(e, fac, px0, py0)
|
||||
if t >= 0 {
|
||||
let want = sh_angle_to(px0, py0, pos_get(t, "x"), pos_get(t, "y"))
|
||||
let want = sh_angle_to(px0, py0, sh_cx(t), sh_cy(t))
|
||||
let cur = pr_get(e, "dir")
|
||||
let d = sh_angle_diff(cur, want)
|
||||
var na = cur
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue