feat(combat): ✨ Adjust combat balance formulas and update core exports for new parameters
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e0bb7cd902
commit
2eb86c0406
2 changed files with 124 additions and 1 deletions
|
|
@ -63,6 +63,78 @@ pub struct CombatBalance {
|
|||
/// city loss (`cities_lost ≥ 1`).
|
||||
#[serde(default)]
|
||||
pub solo_city_grace: SoloCityGrace,
|
||||
/// p2-57c — production-quality stat deltas. The **global default rule**
|
||||
/// applied to a unit's base combat stats when it spawns at a given
|
||||
/// `QualityTier` (derived from the producing city's stockpile depth of the
|
||||
/// gating resource, per PRODUCTION_CHAIN.md's well-stocked / low / empty
|
||||
/// band table). These are the falsifiable single-source magnitudes; an
|
||||
/// individual unit JSON may override them with a per-unit `quality_chain`
|
||||
/// block (p2-57b authoring), but in the absence of one this default rule
|
||||
/// governs — so quality magnitudes derive from one documented rule, not
|
||||
/// 158 invented per-unit numbers.
|
||||
#[serde(default)]
|
||||
pub quality_deltas: QualityDeltas,
|
||||
}
|
||||
|
||||
/// p2-57c — additive combat-stat deltas applied per production-quality band.
|
||||
///
|
||||
/// PRODUCTION_CHAIN.md §"Unit Quality from Processing" gives the *directions*
|
||||
/// (Forge → +attack, Tannery → +armor/defense, Sawmill → +HP) but no
|
||||
/// magnitudes; these defaults supply the canonical numeric rule. Each band
|
||||
/// carries an additive `(attack, defense, hp)` delta layered on the unit's
|
||||
/// base stats. `Levy` (empty stockpile) is the no-bonus baseline (all zero);
|
||||
/// `Regular` (low stockpile) a modest bonus; `Veteran` (well-stocked +
|
||||
/// master workers) the full bonus. Values are additive integers so a tier-1
|
||||
/// warrior (atk 14 / def 8 / hp 80) and a tier-5 unit scale the same flat
|
||||
/// amount — quality is an equipment bonus, not a percentage of base.
|
||||
///
|
||||
/// This block is the *contract shape* that per-unit `quality_chain` overrides
|
||||
/// must match (`validate-game-data.py::validate_unit_quality_chain`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct QualityDeltas {
|
||||
/// Well-stocked: full equipment + master-worker bonus.
|
||||
#[serde(default = "default_veteran_delta")]
|
||||
pub veteran: StatDelta,
|
||||
/// Low stockpile: reduced quality, partial bonus.
|
||||
#[serde(default = "default_regular_delta")]
|
||||
pub regular: StatDelta,
|
||||
/// Empty stockpile: base quality, no equipment bonus.
|
||||
#[serde(default = "default_levy_delta")]
|
||||
pub levy: StatDelta,
|
||||
}
|
||||
|
||||
/// Additive `(attack, defense, hp)` stat delta for one quality band.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct StatDelta {
|
||||
/// Additive attack bonus (Forge / master smiths — better weapons).
|
||||
#[serde(default)]
|
||||
pub attack: i32,
|
||||
/// Additive defense / armor bonus (Tannery / master tanners — better armor).
|
||||
#[serde(default)]
|
||||
pub defense: i32,
|
||||
/// Additive max-HP bonus (Sawmill / master carpenters — better materials).
|
||||
#[serde(default)]
|
||||
pub hp: i32,
|
||||
}
|
||||
|
||||
fn default_veteran_delta() -> StatDelta {
|
||||
StatDelta { attack: 3, defense: 3, hp: 10 }
|
||||
}
|
||||
fn default_regular_delta() -> StatDelta {
|
||||
StatDelta { attack: 1, defense: 1, hp: 4 }
|
||||
}
|
||||
fn default_levy_delta() -> StatDelta {
|
||||
StatDelta { attack: 0, defense: 0, hp: 0 }
|
||||
}
|
||||
|
||||
impl Default for QualityDeltas {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
veteran: default_veteran_delta(),
|
||||
regular: default_regular_delta(),
|
||||
levy: default_levy_delta(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// p1-29d solo-city grace tuning block.
|
||||
|
|
@ -131,6 +203,21 @@ impl Default for CombatBalance {
|
|||
base_xp_value: default_base_xp_value(),
|
||||
low_worker_pool_threshold: default_low_worker_pool_threshold(),
|
||||
solo_city_grace: SoloCityGrace::default(),
|
||||
quality_deltas: QualityDeltas::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QualityDeltas {
|
||||
/// Look up the additive stat delta for a named quality band. Callers map
|
||||
/// `mc_city::QualityTier` → `&'static str` (`"veteran"` / `"regular"` /
|
||||
/// `"levy"`) to keep this block free of an mc-city dependency edge.
|
||||
#[must_use]
|
||||
pub fn for_band(&self, band: &str) -> StatDelta {
|
||||
match band {
|
||||
"veteran" => self.veteran,
|
||||
"regular" => self.regular,
|
||||
_ => self.levy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -196,4 +283,38 @@ mod tests {
|
|||
let cb = parse_combat_balance(json).unwrap();
|
||||
assert_eq!(cb.ransom_offer_duration_turns, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_deltas_default_matches_canonical_json() {
|
||||
// p2-57c — the Rust default rule must match the canonical
|
||||
// combat_balance.json quality_deltas block exactly.
|
||||
let q = QualityDeltas::default();
|
||||
assert_eq!(q.veteran, StatDelta { attack: 3, defense: 3, hp: 10 });
|
||||
assert_eq!(q.regular, StatDelta { attack: 1, defense: 1, hp: 4 });
|
||||
assert_eq!(q.levy, StatDelta { attack: 0, defense: 0, hp: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_deltas_loads_from_json() {
|
||||
let json = r#"{
|
||||
"quality_deltas": {
|
||||
"veteran": { "attack": 5, "defense": 4, "hp": 15 },
|
||||
"regular": { "attack": 2, "defense": 2, "hp": 6 },
|
||||
"levy": { "attack": 0, "defense": 0, "hp": 0 }
|
||||
}
|
||||
}"#;
|
||||
let cb = parse_combat_balance(json).unwrap();
|
||||
assert_eq!(cb.quality_deltas.veteran, StatDelta { attack: 5, defense: 4, hp: 15 });
|
||||
assert_eq!(cb.quality_deltas.regular.hp, 6);
|
||||
// for_band maps tier names to deltas.
|
||||
assert_eq!(cb.quality_deltas.for_band("veteran").attack, 5);
|
||||
assert_eq!(cb.quality_deltas.for_band("levy"), StatDelta::default());
|
||||
assert_eq!(cb.quality_deltas.for_band("unknown"), cb.quality_deltas.levy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_deltas_absent_falls_through_to_default() {
|
||||
let cb = parse_combat_balance(r#"{"ransom_offer_duration_turns": 3}"#).unwrap();
|
||||
assert_eq!(cb.quality_deltas, QualityDeltas::default());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ pub mod worker;
|
|||
|
||||
pub use building::{BuildingEntity, Placement};
|
||||
pub use city_action::{CityAction, CityId};
|
||||
pub use combat_balance::{parse_combat_balance, CombatBalance, SoloCityGrace};
|
||||
pub use combat_balance::{
|
||||
parse_combat_balance, CombatBalance, QualityDeltas, SoloCityGrace, StatDelta,
|
||||
};
|
||||
pub use damage_channel::{ChannelDamageBundle, DamageChannel};
|
||||
pub use diplomacy::{AgreementType, MechanicKey};
|
||||
pub use civic::{AxisChoice, CivicAxis, CivicState, ANARCHY_DURATION, ANARCHY_SENTINEL};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue