- runtime: HEADLESS_FRAME_PATH, STICK_DEADZONE / STICK_LEFT_X/Y, key and byte codes as char literals throughout (`k == 'w'`, `fill(rt_map, ' ', …)`) - ludic.gameplay/stats: drop the duplicate `stat_field` (it answered "atk" for every build stat); Stats.base uses stats_field_name - ludic.shooter: compare aim modes and fire patterns with AimMode.* and WeaponPattern.* instead of raw ints; STICK_RIGHT_X/Y - ludic.npcai: DecisionMade / brain_set_state use AiState.* - examples/games/menu.ludic uses Font.load / Ui.* with FONT_PATH and BACKDROP named; strings.ludic header says what it prints - whole tree: `x = x + 1` → `x += 1` (single-term right-hand sides only), `0 - x` → `-x`, ASCII codes → char literals; every .ludic and every ```ludic fence reformatted with the fixed formatter (whitespace only) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
70 lines
2.7 KiB
Text
70 lines
2.7 KiB
Text
# faction.ludic — a friend/enemy/neutral relationship table (#57 foundation).
|
|
#
|
|
# Shared by shooter targeting and NPC AI: "who may hit / hunt / help whom". Each
|
|
# body carries a `Faction { id }`; a small square matrix records the relationship
|
|
# between two faction ids. Unset pairs fall back to the classic default — same id
|
|
# is friendly, different ids are hostile — so a game that never touches the table
|
|
# still gets sensible enemy/ally behaviour. The matrix is a plain integer buffer
|
|
# in a module var, so it is deterministic and captured by world_save.
|
|
|
|
property Faction { id: int = 0 }
|
|
|
|
const FAC_MAX: int = 32
|
|
const REL_FRIEND: int = 0
|
|
const REL_NEUTRAL: int = 1
|
|
const REL_HOSTILE: int = 2
|
|
|
|
# relationships stored as (rel + 1); a zero slot means "unset -> use the default".
|
|
# A global var initialiser must be a literal, so it is allocated lazily.
|
|
var fac_tbl: words = null
|
|
|
|
function fac_ensure() -> void {
|
|
if fac_tbl == null { fac_tbl = words(FAC_MAX * FAC_MAX) }
|
|
}
|
|
|
|
# Faction.set(a, b, rel) — record a symmetric relationship between two factions.
|
|
@Namespace(Faction) function faction_set(a: int, b: int, rel: int) -> void {
|
|
fac_ensure()
|
|
if (a < 0) or (a >= FAC_MAX) { return }
|
|
if (b < 0) or (b >= FAC_MAX) { return }
|
|
fac_tbl[a * FAC_MAX + b] = rel + 1
|
|
fac_tbl[b * FAC_MAX + a] = rel + 1
|
|
}
|
|
|
|
# Faction.rel(a, b) — the relationship code (0 friend, 1 neutral, 2 hostile).
|
|
@Namespace(Faction) function faction_rel(a: int, b: int) -> int {
|
|
fac_ensure()
|
|
if (a >= 0) and (a < FAC_MAX) and (b >= 0) and (b < FAC_MAX) {
|
|
let v = fac_tbl[a * FAC_MAX + b]
|
|
if v != 0 { return v - 1 }
|
|
}
|
|
if a == b { return REL_FRIEND }
|
|
return REL_HOSTILE
|
|
}
|
|
|
|
# Faction.hostile(a, b) / Faction.friendly(a, b) — the common yes/no queries.
|
|
@Namespace(Faction) function faction_hostile(a: int, b: int) -> int {
|
|
if faction_rel(a, b) == REL_HOSTILE { return 1 }
|
|
return 0
|
|
}
|
|
@Namespace(Faction) function faction_friendly(a: int, b: int) -> int {
|
|
if faction_rel(a, b) == REL_FRIEND { return 1 }
|
|
return 0
|
|
}
|
|
|
|
# Faction.id_of(e) — the faction id an entity carries (-1 if it has no Faction).
|
|
@Namespace(Faction) function faction_id_of(e: int) -> int {
|
|
let P = World.prop_id("Faction")
|
|
if P < 0 { return -1 }
|
|
if World.has(e, P) == 0 { return -1 }
|
|
let f = World.field_id(P, "id")
|
|
if f < 0 { return -1 }
|
|
return World.get(e, P, f)
|
|
}
|
|
|
|
# Faction.enemies(a, b) — true when the two entities' factions are hostile; the
|
|
# one-call test the shooter and AI reach for. Missing factions read as hostile
|
|
# (an unaligned hazard hits anyone) unless the ids happen to match.
|
|
@Namespace(Faction) function faction_enemies(ea: int, eb: int) -> int {
|
|
return faction_hostile(faction_id_of(ea), faction_id_of(eb))
|
|
}
|