Implement real company-scoped event descriptors

This commit is contained in:
Jan Petykiewicz 2026-04-15 12:11:29 -07:00
commit 780e739daa
21 changed files with 1483 additions and 56 deletions

View file

@ -299,6 +299,41 @@ fn apply_runtime_effects(
mutated_company_ids.insert(company_id);
}
}
RuntimeEffect::DeactivateCompany { target } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!(
"missing company_id {company_id} while applying deactivate effect"
)
})?;
company.active = false;
mutated_company_ids.insert(company_id);
if state.selected_company_id == Some(company_id) {
state.selected_company_id = None;
}
}
}
RuntimeEffect::SetCompanyTrackLayingCapacity { target, value } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {
let company = state
.companies
.iter_mut()
.find(|company| company.company_id == company_id)
.ok_or_else(|| {
format!(
"missing company_id {company_id} while applying track capacity effect"
)
})?;
company.available_track_laying_capacity = *value;
mutated_company_ids.insert(company_id);
}
}
RuntimeEffect::AdjustCompanyCash { target, delta } => {
let company_ids = resolve_company_target_ids(state, target)?;
for company_id in company_ids {
@ -429,6 +464,7 @@ fn resolve_company_target_ids(
RuntimeCompanyTarget::AllActive => Ok(state
.companies
.iter()
.filter(|company| company.active)
.map(|company| company.company_id)
.collect()),
RuntimeCompanyTarget::Ids { ids } => {
@ -458,7 +494,10 @@ fn resolve_company_target_ids(
Ok(state
.companies
.iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Human)
.filter(|company| {
company.active
&& company.controller_kind == RuntimeCompanyControllerKind::Human
})
.map(|company| company.company_id)
.collect())
}
@ -476,14 +515,27 @@ fn resolve_company_target_ids(
Ok(state
.companies
.iter()
.filter(|company| company.controller_kind == RuntimeCompanyControllerKind::Ai)
.filter(|company| {
company.active && company.controller_kind == RuntimeCompanyControllerKind::Ai
})
.map(|company| company.company_id)
.collect())
}
RuntimeCompanyTarget::SelectedCompany => state
.selected_company_id
.map(|company_id| vec![company_id])
.ok_or_else(|| "target requires selected_company_id context".to_string()),
RuntimeCompanyTarget::SelectedCompany => {
let selected_company_id = state
.selected_company_id
.ok_or_else(|| "target requires selected_company_id context".to_string())?;
if state
.companies
.iter()
.any(|company| company.company_id == selected_company_id && company.active)
{
Ok(vec![selected_company_id])
} else {
Err("target requires selected_company_id to reference an active company"
.to_string())
}
}
RuntimeCompanyTarget::ConditionTrueCompany => {
Err("target requires condition-evaluation context".to_string())
}
@ -530,6 +582,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 0,
active: true,
available_track_laying_capacity: None,
}],
selected_company_id: None,
packed_event_collection: None,
@ -692,12 +746,16 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 5,
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 20,
debt: 8,
active: true,
available_track_laying_capacity: None,
},
],
event_runtime_records: vec![RuntimeEventRecord {
@ -744,12 +802,16 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 2,
active: true,
available_track_laying_capacity: None,
},
],
selected_company_id: Some(1),
@ -866,6 +928,179 @@ mod tests {
assert!(error.contains("controller_kind"));
}
#[test]
fn all_active_and_role_targets_exclude_inactive_companies() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 1,
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 20,
debt: 2,
active: false,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 3,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 30,
debt: 3,
active: true,
available_track_laying_capacity: None,
},
],
event_runtime_records: vec![
RuntimeEventRecord {
record_id: 16,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::AllActive,
delta: 5,
}],
},
RuntimeEventRecord {
record_id: 17,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::HumanCompanies,
delta: 4,
}],
},
RuntimeEventRecord {
record_id: 18,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::AdjustCompanyDebt {
target: RuntimeCompanyTarget::AiCompanies,
delta: 6,
}],
},
],
..state()
};
execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("active-company filtering should succeed");
assert_eq!(state.companies[0].current_cash, 15);
assert_eq!(state.companies[1].current_cash, 20);
assert_eq!(state.companies[2].current_cash, 35);
assert_eq!(state.companies[0].debt, 5);
assert_eq!(state.companies[1].debt, 2);
assert_eq!(state.companies[2].debt, 9);
}
#[test]
fn deactivating_selected_company_clears_selection() {
let mut state = RuntimeState {
companies: vec![RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
active: true,
available_track_laying_capacity: Some(8),
}],
selected_company_id: Some(1),
event_runtime_records: vec![RuntimeEventRecord {
record_id: 19,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::DeactivateCompany {
target: RuntimeCompanyTarget::SelectedCompany,
}],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("deactivate company effect should succeed");
assert!(!state.companies[0].active);
assert_eq!(state.selected_company_id, None);
assert_eq!(result.service_events[0].mutated_company_ids, vec![1]);
}
#[test]
fn sets_track_laying_capacity_for_resolved_targets() {
let mut state = RuntimeState {
companies: vec![
RuntimeCompany {
company_id: 1,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 10,
debt: 0,
active: true,
available_track_laying_capacity: None,
},
RuntimeCompany {
company_id: 2,
controller_kind: RuntimeCompanyControllerKind::Ai,
current_cash: 20,
debt: 0,
active: true,
available_track_laying_capacity: None,
},
],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 20,
trigger_kind: 7,
active: true,
service_count: 0,
marks_collection_dirty: false,
one_shot: false,
has_fired: false,
effects: vec![RuntimeEffect::SetCompanyTrackLayingCapacity {
target: RuntimeCompanyTarget::Ids { ids: vec![2] },
value: Some(14),
}],
}],
..state()
};
let result = execute_step_command(
&mut state,
&StepCommand::ServiceTriggerKind { trigger_kind: 7 },
)
.expect("track capacity effect should succeed");
assert_eq!(state.companies[0].available_track_laying_capacity, None);
assert_eq!(state.companies[1].available_track_laying_capacity, Some(14));
assert_eq!(result.service_events[0].mutated_company_ids, vec![2]);
}
#[test]
fn rejects_condition_true_company_target_without_condition_context() {
let mut state = RuntimeState {
@ -941,6 +1176,8 @@ mod tests {
controller_kind: RuntimeCompanyControllerKind::Unknown,
current_cash: 10,
debt: 2,
active: true,
available_track_laying_capacity: None,
}],
event_runtime_records: vec![RuntimeEventRecord {
record_id: 30,