feat(@projects/@magic-civilization): add json keyword-based capability system

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-19 17:49:28 -07:00
parent d6c0e76306
commit 509cb338c9
3 changed files with 59 additions and 23 deletions

View file

@ -0,0 +1,5 @@
[gd_scene format=3 uid="uid://a1c2t3i4o5n6b"]
[node name="ActionButton" type="Button"]
custom_minimum_size = Vector2(0, 28)
size_flags_horizontal = 3

View file

@ -32,9 +32,11 @@ var unit_type: String = ""
var domain: String = "land"
# ── Capability flags ─────────────────────────────────────────────────
## True if this unit can found cities (derived from "found_city" keyword).
## Keywords from unit JSON (replaces legacy flags/can_found_city/can_build_improvements).
var keywords: Array[String] = []
## True if this unit can found cities — derived from "founder" keyword.
var can_found_city: bool = false
## True if this unit can build tile improvements (derived from "build" keyword).
## True if this unit can build tile improvements — derived from "worker" keyword.
var can_build_improvements: bool = false
# ── Position & ownership ──────────────────────────────────────────────
@ -143,16 +145,12 @@ func _populate_from_data() -> void:
domain = String(data.get("domain", "land"))
if display_name.is_empty():
display_name = data.get("name", unit_id.capitalize())
var is_founder: bool = (
unit_id.contains("founder")
)
can_found_city = data.get("can_found_city", is_founder)
var is_builder: bool = (
unit_id.contains("worker") or unit_id.contains("engineer")
)
can_build_improvements = data.get(
"can_build_improvements", is_builder
)
var raw_keywords: Array = data.get("keywords", [])
keywords.clear()
for k: Variant in raw_keywords:
keywords.append(String(k))
can_found_city = "founder" in keywords
can_build_improvements = "worker" in keywords
# ── Per-turn refresh (called by turn_processor.gd) ────────────────────
@ -261,18 +259,47 @@ func _combat_type() -> String:
return data.get("combat_type", "")
## List of keyword strings for this unit, sourced from JSON unit data.
## Returns an empty array for units whose `unit_id` has no DataLoader entry.
func get_keywords() -> Array:
if unit_id.is_empty():
return []
var data: Dictionary = DataLoader.get_unit(unit_id)
return data.get("keywords", [])
## List of keyword strings for this unit (populated from JSON at init).
func get_keywords() -> Array[String]:
return keywords
## True if this unit has `keyword` in its JSON keyword list.
## True if this unit has `keyword` in its keyword list.
func has_keyword(keyword: String) -> bool:
return keyword in get_keywords()
return keyword in keywords
## Space-separated keywords string for the GdUnitActions bridge.
func get_keywords_str() -> String:
return " ".join(keywords)
## Return the legal actions for this unit's current state via GdUnitActions.
## Each entry is a Dictionary {kind: String, enabled: bool, disabled_reason: String}.
func get_legal_actions() -> Array[Dictionary]:
if not ClassDB.class_exists("GdUnitActions"):
return []
var bridge: RefCounted = ClassDB.instantiate("GdUnitActions") as RefCounted
return bridge.legal_actions_for(
unit_type,
get_keywords_str(),
movement_remaining > 0,
is_fortified
)
## Returns true if `kind` (e.g. "fortify") is a legal enabled action right now.
func can_invoke_action(kind: String) -> bool:
if not ClassDB.class_exists("GdUnitActions"):
return false
var bridge: RefCounted = ClassDB.instantiate("GdUnitActions") as RefCounted
return bridge.can_invoke(
unit_type,
get_keywords_str(),
movement_remaining > 0,
is_fortified,
kind
)
# ── Movement / damage / healing helpers ───────────────────────────────

View file

@ -876,9 +876,13 @@ mod tests {
vec![-1, 0],
);
let actions = decide_movement(&state(0, vec![me, enemy]), &weights(), &mut rng());
// Registry integration: sole garrison defender with no movement target
// now fortifies in place rather than idling (Action::Fortify sourced
// from legal_actions).
assert_eq!(actions.len(), 1, "expected one action (Fortify), got {actions:?}");
assert!(
actions.is_empty(),
"sole garrison defender should hold, got {actions:?}"
matches!(actions[0], Action::Fortify { .. }),
"sole garrison defender should fortify, got {actions:?}"
);
}