feat(api-gdext): ✨ Introduce physics engine integration traits and methods for GDExtension API simulations
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
57e798f7f8
commit
6e17c474d2
1 changed files with 200 additions and 0 deletions
|
|
@ -2046,3 +2046,203 @@ fn turn_result_to_dict(result: &mc_turn::TurnResult, post_turn: u32) -> Dictiona
|
|||
d.set("fauna_combat_log", log);
|
||||
d
|
||||
}
|
||||
|
||||
// ── GdCombatResolver ────────────────────────────────────────────────────
|
||||
//
|
||||
// Stateless wrapper around `mc_combat::CombatResolver`. Accepts attacker/
|
||||
// defender stat dicts + optional city context, returns a result dict. The
|
||||
// GDScript `combat_resolver.gd` wrapper converts Unit objects into these
|
||||
// dicts and writes the result back to the units.
|
||||
//
|
||||
// Input dict shape (attacker/defender):
|
||||
// { hp: int, max_hp: int, attack: int, defense: int,
|
||||
// ranged_attack: int, range: int, movement: int,
|
||||
// keywords: Array[String] }
|
||||
//
|
||||
// Params dict shape:
|
||||
// { combat_type: String ("melee"|"ranged"|"siege"),
|
||||
// attacker_flanking_allies: int, defender_flanking_allies: int,
|
||||
// defender_terrain_defense: float, defender_fortification: float,
|
||||
// city_hp: int (or -1 for no city), city_wall_tier: int,
|
||||
// city_has_garrison: bool, attacker_is_siege: bool }
|
||||
//
|
||||
// Result dict shape matches `mc_combat::CombatResult` field-by-field.
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdCombatResolver {
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdCombatResolver {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
}
|
||||
|
||||
fn dict_get_i64(d: &Dictionary, key: &str, default: i64) -> i64 {
|
||||
d.get(key)
|
||||
.and_then(|v| v.try_to::<i64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn dict_get_f64(d: &Dictionary, key: &str, default: f64) -> f64 {
|
||||
d.get(key)
|
||||
.and_then(|v| v.try_to::<f64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn dict_get_bool(d: &Dictionary, key: &str, default: bool) -> bool {
|
||||
d.get(key)
|
||||
.and_then(|v| v.try_to::<bool>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn dict_get_string(d: &Dictionary, key: &str, default: &str) -> String {
|
||||
d.get(key)
|
||||
.and_then(|v| v.try_to::<GString>().ok())
|
||||
.map(|g| g.to_string())
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
fn unit_stats_from_dict(d: &Dictionary) -> mc_combat::UnitStats {
|
||||
mc_combat::UnitStats {
|
||||
hp: dict_get_i64(d, "hp", 0) as i32,
|
||||
max_hp: dict_get_i64(d, "max_hp", 1) as i32,
|
||||
attack: dict_get_i64(d, "attack", 0) as i32,
|
||||
defense: dict_get_i64(d, "defense", 0) as i32,
|
||||
ranged_attack: dict_get_i64(d, "ranged_attack", 0) as i32,
|
||||
range: dict_get_i64(d, "range", 0) as i32,
|
||||
movement: dict_get_i64(d, "movement", 0) as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn keywords_from_dict(d: &Dictionary) -> Vec<mc_combat::Keyword> {
|
||||
let Some(v) = d.get("keywords") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(arr) = v.try_to::<Array<GString>>() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::with_capacity(arr.len());
|
||||
for i in 0..arr.len() {
|
||||
if let Some(s) = arr.get(i) {
|
||||
if let Some(kw) = mc_combat::Keyword::from_str(&s.to_string()) {
|
||||
out.push(kw);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn combat_type_from_str(s: &str) -> mc_combat::CombatType {
|
||||
match s {
|
||||
"ranged" => mc_combat::CombatType::Ranged,
|
||||
"siege" => mc_combat::CombatType::Siege,
|
||||
_ => mc_combat::CombatType::Melee,
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdCombatResolver {
|
||||
/// Resolve a combat engagement. See module docs for dict shapes.
|
||||
#[func]
|
||||
fn resolve(
|
||||
attacker: Dictionary,
|
||||
defender: Dictionary,
|
||||
params: Dictionary,
|
||||
) -> Dictionary {
|
||||
let attacker_stats = unit_stats_from_dict(&attacker);
|
||||
let defender_stats = unit_stats_from_dict(&defender);
|
||||
let attacker_keywords = keywords_from_dict(&attacker);
|
||||
let defender_keywords = keywords_from_dict(&defender);
|
||||
|
||||
let combat_type_s = dict_get_string(¶ms, "combat_type", "melee");
|
||||
let combat_type = combat_type_from_str(&combat_type_s);
|
||||
|
||||
let attacker_bonuses = mc_combat::CombatBonuses {
|
||||
flanking_allies: dict_get_i64(¶ms, "attacker_flanking_allies", 0) as i32,
|
||||
..Default::default()
|
||||
};
|
||||
let defender_bonuses = mc_combat::CombatBonuses {
|
||||
flanking_allies: dict_get_i64(¶ms, "defender_flanking_allies", 0) as i32,
|
||||
terrain_defense: dict_get_f64(¶ms, "defender_terrain_defense", 0.0) as f32,
|
||||
fortification: dict_get_f64(¶ms, "defender_fortification", 0.0) as f32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let city_hp_raw = dict_get_i64(¶ms, "city_hp", -1);
|
||||
let city_hp = if city_hp_raw >= 0 {
|
||||
Some(city_hp_raw as i32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let combat_params = mc_combat::CombatParams {
|
||||
attacker: attacker_stats,
|
||||
defender: defender_stats,
|
||||
combat_type,
|
||||
attacker_keywords,
|
||||
defender_keywords,
|
||||
attacker_bonuses,
|
||||
defender_bonuses,
|
||||
city_hp,
|
||||
city_wall_tier: dict_get_i64(¶ms, "city_wall_tier", 0) as i32,
|
||||
city_has_garrison: dict_get_bool(¶ms, "city_has_garrison", false),
|
||||
attacker_is_siege: dict_get_bool(¶ms, "attacker_is_siege", false),
|
||||
};
|
||||
|
||||
let result = mc_combat::CombatResolver::resolve(&combat_params);
|
||||
|
||||
let mut d = Dictionary::new();
|
||||
d.set("defender_damage", result.defender_damage as i64);
|
||||
d.set("attacker_damage", result.attacker_damage as i64);
|
||||
d.set(
|
||||
"attacker_killed",
|
||||
matches!(result.attacker_outcome, mc_combat::CombatOutcome::Killed),
|
||||
);
|
||||
d.set(
|
||||
"defender_killed",
|
||||
matches!(result.defender_outcome, mc_combat::CombatOutcome::Killed),
|
||||
);
|
||||
d.set("attacker_hp", result.attacker_hp as i64);
|
||||
d.set("defender_hp", result.defender_hp as i64);
|
||||
d.set("city_damage", result.city_damage as i64);
|
||||
d.set("city_hp_remaining", result.city_hp_remaining as i64);
|
||||
d.set("attacker_xp", result.attacker_xp as i64);
|
||||
d.set("defender_xp", result.defender_xp as i64);
|
||||
d.set("life_drain_heal", result.life_drain_heal as i64);
|
||||
d
|
||||
}
|
||||
|
||||
/// XP threshold for a given promotion level. Returns -1 if beyond max.
|
||||
#[func]
|
||||
fn xp_threshold(level: i64) -> i64 {
|
||||
mc_combat::xp_threshold(level as i32)
|
||||
.map(|x| x as i64)
|
||||
.unwrap_or(-1)
|
||||
}
|
||||
|
||||
/// HP healed when a unit promotes.
|
||||
#[func]
|
||||
fn heal_on_promote(max_hp: i64) -> i64 {
|
||||
mc_combat::heal_on_promote(max_hp as i32) as i64
|
||||
}
|
||||
|
||||
/// Derive wild creature stats (tier, size, diet) — same formula as the resolver test.
|
||||
#[func]
|
||||
fn wild_combat_stats(tier: i64, size: GString, diet: GString) -> Dictionary {
|
||||
let stats =
|
||||
mc_combat::wild_combat_stats(tier as i32, &size.to_string(), &diet.to_string());
|
||||
let mut d = Dictionary::new();
|
||||
d.set("hp", stats.hp as i64);
|
||||
d.set("max_hp", stats.max_hp as i64);
|
||||
d.set("attack", stats.attack as i64);
|
||||
d.set("defense", stats.defense as i64);
|
||||
d.set("ranged_attack", stats.ranged_attack as i64);
|
||||
d.set("range", stats.range as i64);
|
||||
d.set("movement", stats.movement as i64);
|
||||
d
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue