feat(mc-magic): ✨ Add core magic subsystem and Ascension victory path implementation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f9a0e7b754
commit
c9d396b8a1
2 changed files with 197 additions and 1 deletions
184
src/simulator/crates/mc-magic/src/ascension.rs
Normal file
184
src/simulator/crates/mc-magic/src/ascension.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
//! Arcane Ascension victory path — second victory condition alongside domination.
|
||||
//!
|
||||
//! Scaffold only: defines the requirement config (parsed from `ascension.json`),
|
||||
//! the runtime state machine, and a pure detection function. Turn-processor
|
||||
//! integration and UI wiring come in follow-up tasks.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Ritual lifecycle. Transitions:
|
||||
/// `Inactive -> RequirementsMet -> Channeling -> (Complete | Failed)`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AscensionPhase {
|
||||
Inactive,
|
||||
RequirementsMet,
|
||||
Channeling,
|
||||
Complete,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Data-driven thresholds parsed from `ascension.json`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AscensionRequirements {
|
||||
pub school_wonders_built: u32,
|
||||
pub schools_maxed: u32,
|
||||
pub mana_accumulated: u32,
|
||||
pub high_archon_alive: bool,
|
||||
#[serde(default)]
|
||||
pub required_tech: String,
|
||||
}
|
||||
|
||||
impl Default for AscensionRequirements {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
school_wonders_built: 5,
|
||||
schools_maxed: 5,
|
||||
mana_accumulated: 500,
|
||||
high_archon_alive: true,
|
||||
required_tech: "arcane_ascension".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-player ascension tracking. Mutated each turn once the ritual is active.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AscensionState {
|
||||
pub phase: AscensionPhase,
|
||||
pub wonder_count: u32,
|
||||
pub schools_maxed: u32,
|
||||
pub mana_accumulated: u32,
|
||||
pub high_archon_alive: bool,
|
||||
pub has_required_tech: bool,
|
||||
pub channel_turns_remaining: u32,
|
||||
}
|
||||
|
||||
impl Default for AscensionState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
phase: AscensionPhase::Inactive,
|
||||
wonder_count: 0,
|
||||
schools_maxed: 0,
|
||||
mana_accumulated: 0,
|
||||
high_archon_alive: false,
|
||||
has_required_tech: false,
|
||||
channel_turns_remaining: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure check: do the current inputs satisfy the ritual requirements?
|
||||
pub fn requirements_met(state: &AscensionState, req: &AscensionRequirements) -> bool {
|
||||
state.wonder_count >= req.school_wonders_built
|
||||
&& state.schools_maxed >= req.schools_maxed
|
||||
&& state.mana_accumulated >= req.mana_accumulated
|
||||
&& (!req.high_archon_alive || state.high_archon_alive)
|
||||
&& (req.required_tech.is_empty() || state.has_required_tech)
|
||||
}
|
||||
|
||||
/// Advance the phase based on current inputs. Returns the new phase.
|
||||
/// Call once per turn during Phase 7 victory evaluation.
|
||||
pub fn evaluate(state: &mut AscensionState, req: &AscensionRequirements) -> AscensionPhase {
|
||||
match state.phase {
|
||||
AscensionPhase::Inactive => {
|
||||
if requirements_met(state, req) {
|
||||
state.phase = AscensionPhase::RequirementsMet;
|
||||
}
|
||||
}
|
||||
AscensionPhase::RequirementsMet => {
|
||||
if !state.high_archon_alive && req.high_archon_alive {
|
||||
state.phase = AscensionPhase::Failed;
|
||||
}
|
||||
}
|
||||
AscensionPhase::Channeling => {
|
||||
if !state.high_archon_alive && req.high_archon_alive {
|
||||
state.phase = AscensionPhase::Failed;
|
||||
} else if state.channel_turns_remaining == 0 {
|
||||
state.phase = AscensionPhase::Complete;
|
||||
} else {
|
||||
state.channel_turns_remaining -= 1;
|
||||
}
|
||||
}
|
||||
AscensionPhase::Complete | AscensionPhase::Failed => {}
|
||||
}
|
||||
state.phase
|
||||
}
|
||||
|
||||
/// Short stable tag for the victory-event payload.
|
||||
pub fn victory_type_str() -> &'static str {
|
||||
"arcane_ascension"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn maxed_state() -> AscensionState {
|
||||
AscensionState {
|
||||
phase: AscensionPhase::Inactive,
|
||||
wonder_count: 5,
|
||||
schools_maxed: 5,
|
||||
mana_accumulated: 500,
|
||||
high_archon_alive: true,
|
||||
has_required_tech: true,
|
||||
channel_turns_remaining: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_requirements_parse_from_canonical_json() {
|
||||
let raw = include_str!(
|
||||
"../../../public/games/age-of-dwarves/data/ascension.json"
|
||||
);
|
||||
let v: serde_json::Value = serde_json::from_str(raw).expect("ascension.json parses");
|
||||
let req: AscensionRequirements =
|
||||
serde_json::from_value(v["requirements"].clone()).expect("requirements parse");
|
||||
assert_eq!(req.school_wonders_built, 5);
|
||||
assert_eq!(req.mana_accumulated, 500);
|
||||
assert_eq!(req.required_tech, "arcane_ascension");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_not_met_when_any_input_short() {
|
||||
let req = AscensionRequirements::default();
|
||||
let mut state = maxed_state();
|
||||
assert!(requirements_met(&state, &req));
|
||||
|
||||
state.mana_accumulated = 499;
|
||||
assert!(!requirements_met(&state, &req));
|
||||
state.mana_accumulated = 500;
|
||||
|
||||
state.high_archon_alive = false;
|
||||
assert!(!requirements_met(&state, &req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_transitions_inactive_to_requirements_met() {
|
||||
let req = AscensionRequirements::default();
|
||||
let mut state = maxed_state();
|
||||
assert_eq!(evaluate(&mut state, &req), AscensionPhase::RequirementsMet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channeling_counts_down_to_complete() {
|
||||
let req = AscensionRequirements::default();
|
||||
let mut state = maxed_state();
|
||||
state.phase = AscensionPhase::Channeling;
|
||||
state.channel_turns_remaining = 2;
|
||||
assert_eq!(evaluate(&mut state, &req), AscensionPhase::Channeling);
|
||||
assert_eq!(state.channel_turns_remaining, 1);
|
||||
assert_eq!(evaluate(&mut state, &req), AscensionPhase::Channeling);
|
||||
assert_eq!(state.channel_turns_remaining, 0);
|
||||
assert_eq!(evaluate(&mut state, &req), AscensionPhase::Complete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_archon_death_during_channel_fails_ritual() {
|
||||
let req = AscensionRequirements::default();
|
||||
let mut state = maxed_state();
|
||||
state.phase = AscensionPhase::Channeling;
|
||||
state.channel_turns_remaining = 5;
|
||||
state.high_archon_alive = false;
|
||||
assert_eq!(evaluate(&mut state, &req), AscensionPhase::Failed);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,13 @@
|
|||
// TODO: mana economy, spells, Archons, enchantments, Ascension
|
||||
//! Magic subsystem: mana economy, spells, Archons, enchantments, Ascension.
|
||||
//!
|
||||
//! Scaffold stage — only the Ascension victory path is materialised here.
|
||||
//! Other modules (mana, spells, archons, enchantments) follow in subsequent
|
||||
//! tasks per CLAUDE.md's atomic-porting protocol.
|
||||
|
||||
pub mod ascension;
|
||||
|
||||
pub use ascension::{
|
||||
evaluate as evaluate_ascension, requirements_met as ascension_requirements_met,
|
||||
victory_type_str as ascension_victory_type_str, AscensionPhase, AscensionRequirements,
|
||||
AscensionState,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue