Modding System Campaign System (Branching, Persistent, Continuous)
Inspired by Operation Flashpoint: Cold War Crisis / Resistance. See D021.
OpenRA’s campaigns are disconnected: each mission is standalone, you exit to menu between them, there’s no flow. Our campaigns are continuous, branching, and stateful — a directed graph of missions with persistent state, multiple outcomes per mission, and no mandatory game-over screen.
That mission graph is the canonical D021 backbone. Some campaigns stop there. Others, especially first-party Enhanced Edition campaigns, organize that same graph into a phase-based strategic layer (War Table) with operations, enemy initiatives, Requisition, Command Authority, and an arms race / tech ledger between milestone missions.
Core Principles
- Campaign is a graph, not a list. Missions connect via named outcomes, forming branches, convergence points, and optional paths — not a linear sequence.
- Missions have multiple outcomes, not just win/lose. “Won with bridge intact” and “Won but bridge destroyed” are different outcomes that lead to different next missions.
- Failure doesn’t end the campaign by default. A “defeat” outcome is just another edge in the graph. The designer chooses: branch to a fallback mission, retry with fewer resources, or skip ahead with consequences. Truly campaign-failing missions are allowed only as explicit authored exceptions and must be labeled as critical missions before mission launch.
- State persists across missions. Surviving units, veterancy, captured equipment, story flags, resources — all carry forward based on designer-configured carryover rules.
- Continuous flow. Briefing → mission → debrief → next mission. No exit to menu between levels (unless the player explicitly quits).
Graph Backbone and Strategic-Layer Extension
D021 has two valid presentation tiers:
- Graph-only campaigns — the player moves directly from mission to mission through a branching outcome graph
- Phase-based strategic campaigns — the same graph is wrapped in a War Table that exposes optional operations, enemy initiatives, Command Authority, and Requisition between milestone missions
The key rule is that the graph remains authoritative in both cases. Strategic-layer campaigns do not replace the graph with an unrelated meta-system. They:
- group missions into authored phases
- expose some graph nodes as optional operations
- advance authored enemy initiatives between operations
- track a first-class tech / arms-race ledger that later missions consume
Classic campaigns can stay graph-only. Enhanced Edition campaigns can use the strategic layer. Both use the same CampaignState, mission outcomes, save/load model, and Lua Campaign API.
campaign:
id: allied_campaign_enhanced
start_mission: allied_01
campaign_phases:
phase_3:
main_mission: allied_06
granted_requisition: 2000
granted_intel: 50
operations:
authored:
- ic_behind_enemy_lines
- cs_crackdown
generated_profiles:
- allied_intel_raid_t1
enemy_initiatives:
- radar_network_expansion
- chemical_weapons_deployment
asset_ledger: allied_arms_race
Campaign Definition (YAML)
# campaigns/allied/campaign.yaml
campaign:
id: allied_campaign
title: "Allied Campaign"
description: "Drive back the Soviet invasion across Europe"
start_mission: allied_01
# Campaign-authored default settings (D033/D019/D032/D043/D045/D048)
# These are the campaign's baked-in configuration — what the author
# intends the campaign to play like. Applied as defaults when the
# player starts a new playthrough; player can review and tweak
# individual switches before launching.
default_settings:
difficulty: normal # campaign's intended starting difficulty
balance: classic # D019 balance preset
theme: classic # D032 UI theme
behavior: vanilla # D033 QoL behavior preset
ai_behavior: classic_ra # D043 AI preset
pathfinding: classic_ra # D045 pathfinding feel
render_mode: classic # D048 render mode
# Individual toggle overrides — fine-grained switches on top of
# the behavior preset above (same keys as D033 YAML structure)
toggle_overrides:
fog_of_war: on # campaign requires fog
shroud_regrow: false # but no shroud regrowth
health_bars: on_selection # author preference for this campaign
# What persists between missions (campaign-wide defaults)
persistent_state:
unit_roster: true # surviving units carry forward
veterancy: true # unit experience persists
resources: false # credits reset per mission
equipment: true # captured vehicles/crates persist
hero_progression: false # optional built-in hero toolkit (XP/levels/skills)
custom_flags: {} # arbitrary Lua-writable key-value state
missions:
allied_01:
map: missions/allied-01
briefing: briefings/allied-01.yaml
video: videos/allied-01-briefing.vqa
critical_failure: false # default: defeat branches, does not end the campaign
carryover:
from_previous: none # first mission — nothing carries
outcomes:
victory_bridge_intact:
description: "Bridge secured intact"
next: allied_02a
debrief: briefings/allied-01-debrief-bridge.yaml
state_effects:
set_flag:
bridge_status: intact
victory_bridge_destroyed:
description: "Won but bridge was destroyed"
next: allied_02b
state_effects:
set_flag:
bridge_status: destroyed
defeat:
description: "Base overrun"
next: allied_01_fallback
state_effects:
set_flag:
retreat_count: +1
allied_02a:
map: missions/allied-02a # different map — bridge crossing
briefing: briefings/allied-02a.yaml
carryover:
units: surviving # units from mission 01 appear
veterancy: keep # their experience carries
equipment: keep # captured Soviet tanks too
conditions: # optional entry conditions
require_flag:
bridge_status: intact
outcomes:
victory:
next: allied_03
defeat:
next: allied_02_fallback
allied_02b:
map: missions/allied-02b # different map — river crossing without bridge
briefing: briefings/allied-02b.yaml
carryover:
units: surviving
veterancy: keep
outcomes:
victory:
next: allied_03 # branches converge at mission 03
defeat:
next: allied_02_fallback
allied_01_fallback:
map: missions/allied-01-retreat
briefing: briefings/allied-01-retreat.yaml
carryover:
units: surviving # fewer units since you lost
veterancy: keep
outcomes:
victory:
next: allied_02b # after retreating, you take the harder path
state_effects:
set_flag:
morale: low
allied_03:
map: missions/allied-03
# ...branches converge here regardless of path taken
Campaign Graph Visualization
┌─────────────┐
│ allied_01 │
└──┬───┬───┬──┘
bridge ok ╱ │ ╲ defeat
╱ │ ╲
┌────────────┐ bridge ┌─────────────────┐
│ allied_02a │ destroyed│ allied_01_ │
└─────┬──────┘ │ │ fallback │
│ ┌─────┴───┐└────────┬────────┘
│ │allied_02b│ │
│ └────┬─────┘ │
│ │ joins 02b
└─────┬──────┘
│ converge
┌─────┴──────┐
│ allied_03 │
└─────────────┘
This is a directed acyclic graph (with optional cycles for retry loops). The engine validates campaign graphs at load time: no orphan nodes, all outcome targets exist, start mission is defined.
Mission Criticality and Failure Disclosure
The canonical expectation for IC campaigns is:
- Main missions usually fail forward. Defeat branches to a fallback node, harder variant, or reduced-state continuation.
- SpecOps usually fail with consequences. You may lose intel, lose a hero, open a rescue branch, or strengthen the enemy — but the campaign should usually continue.
- Critical missions are rare exceptions. These are the missions where defeat truly ends the campaign, a theater, or a mini-campaign run.
If a mission is critical, that must be authored and surfaced explicitly:
missions:
allied_14_final_assault:
critical_failure: true
critical_failure_text: "If Moscow holds, the Allied campaign ends in defeat."
revealed_operations:
- allied_cleanup_01
unlocked_operations:
- allied_epilogue_aftermath
briefing_risk:
success_reward: "Campaign victory"
failure_consequence: "Campaign ends"
UI rule: A critical mission must show a visible CRITICAL badge on the campaign map and in the mission briefing. “Lose and retry” or “campaign ends on failure” must never be ambiguous.
Default rule: If critical_failure is absent, it is treated as false. Campaign authors must opt in to hard failure.
Save/Load and Choice Commitment
By default, D021 campaign choices are normal save/load state. IC does not try to prevent reload-based reconsideration in standard campaign play.
Recommended first-party policy:
- Normal mode: free saving/reloading before or after decisions
- Ironman / commit modes: autosave immediately after a tactical dilemma selection or other branch-committing decision, and treat that branch as locked
A campaign may also mark a small number of optional spotlight operations as explicit commit missions. These inherit the same autosave-and-lock behavior even when the wider campaign is not running in Ironman.
The “world moves without you” rule is about authored consequences and opportunity cost, not an anti-save-scum guarantee by itself.
Campaign Validation and Coverage
Branching campaigns create a large state space. D021 content therefore needs more than a graph-shape check.
ic campaign validate should cover three layers:
- Graph validation — no orphan nodes, no missing outcome targets, no impossible joins
- State-coverage validation — traverse authored outcomes, expiring opportunity branches, pending-rescue states, and fallback edges to confirm every consumer mission still has a legal playable state
- Presentation validation — snapshot world-screen cards and briefings so
On Success,On Failure,If Skipped,Time Window, andCRITICALtext do not silently disappear under specific flag combinations
Large campaigns should validate endgame consumers by asset bundle rather than brute-forcing every raw flag permutation. For example:
- air-support bundle
- partisan / theater bundle
- prototype-tech bundle
- unrest / sabotage bundle
- rescue / compromise bundle
The goal is to prove that every legal combination reaching a mission like M14 still:
- spawns correctly
- exposes a coherent briefing
- preserves the authored reward / penalty promises
Generated SpecOps add one more layer:
- sample every legal generated profile and run the same route / objective / duration validation used at runtime
This is design-level policy, not a hard requirement on one exact CLI shape, but first-party campaigns should not ship without automated traversal and snapshot coverage.
Campaign Graph Extensions (Optional Operations)
Campaign graphs are extensible — external mods can inject new mission nodes into an existing campaign’s graph without modifying the original campaign files. This enables:
- First-party expansion content — IC ships optional “Enhanced Campaign” operations that branch off the original RA1/TD campaign graph, using IC’s new mechanics (hero progression, branching, dynamic weather, asymmetric co-op) while the classic missions remain untouched
- Community optional operations — Workshop authors publish SpecOps packs or theater-branch packs that attach to specific points in official or community campaigns
- Campaign DLC pattern — new story arcs that branch off after specific missions and rejoin (or don’t) later in the original graph
How it works: Campaign extensions use the same YAML graph format as primary campaigns but declare an extends field pointing to the parent campaign. The engine merges the extension graph into the parent at load time.
# Extension campaign — adds optional operations to the Allied campaign
campaign:
id: allied_campaign_enhanced
extends: allied_campaign # parent campaign this extends
# Extension metadata
title: "Allied Campaign — Enhanced Edition"
description: "Optional missions exploring new IC mechanics alongside the classic Allied campaign"
optional: true # player can enable/disable in campaign settings
badge: "IC Enhanced" # shown in mission select to distinguish extended content
# New missions that attach to the parent graph
missions:
# Optional SpecOps branch: branches off after allied_03
allied_03_tanya_ops:
extends_from:
parent_mission: allied_03 # attach point in the parent campaign
parent_outcome: victory # which outcome edge to branch from
insertion: optional_branch # 'optional_branch' = new choice, not replacement
map: missions/enhanced/tanya_ops_01
briefing: briefings/enhanced/tanya_ops_01.yaml
badge: "IC Enhanced" # UI badge distinguishing this from original missions
type: embedded_task_force # uses new IC mission archetype
hero: tanya
outcomes:
victory:
next: allied_04 # rejoin the original campaign graph
state_effects:
set_flag:
tanya_ops_complete: true
tanya_reputation: hero
defeat:
next: allied_04 # still continues — optional operation failure isn't campaign-ending
state_effects:
set_flag:
tanya_ops_failed: true
# Optional SpecOps follow-up: available only if the player chose a specific path
allied_06_spy_network:
extends_from:
parent_mission: allied_06
parent_outcome: victory
insertion: optional_branch
condition: # only available if a specific flag is set
require_flag:
tanya_ops_complete: true
map: missions/enhanced/spy_network
type: spy_infiltration
outcomes:
victory:
next: allied_07
state_effects:
set_flag:
spy_network_active: true
# Replacement mission: same graph position but uses new IC mechanics
allied_05_remastered:
extends_from:
parent_mission: allied_05
insertion: alternative # 'alternative' = offered alongside the original
label: "Enhanced Version" # shown in mission select
map: missions/enhanced/allied_05_remastered
type: embedded_task_force # new IC archetype instead of classic scripting
outcomes:
victory:
next: allied_06 # same destination as original
defeat:
next: allied_05_fallback # same fallback as original
Insertion modes:
| Mode | Behavior | UX |
|---|---|---|
optional_branch | Adds a new choice alongside existing outcome edges. Player can take the optional operation or continue the original path. If the branch is skipped, the original graph is unchanged unless authored flags say otherwise | Mission select shows: “Continue to Mission 4” / “SpecOps: Tanya Ops (IC Enhanced)” |
alternative | Offers an alternative version of an existing mission. Player chooses between original and enhanced. Both lead to the same next-mission targets | Mission select shows: “Mission 5 (Classic)” / “Mission 5 (Enhanced Version)” |
insert_before | Adds a new mission that must be completed before the original mission. The extension mission’s victory outcome leads to the original mission | The new mission appears in the graph between the parent and its original next mission |
post_campaign | Attaches after the campaign’s ending node(s). Extends the story beyond the original ending | Available after campaign completion. Shows as “Epilogue: …” |
Some first-party plans may intentionally keep a branch pending across a bounded future window, such as a rescue mission that remains available for the next one or two nodes while penalties escalate. That is an explicit campaign-layer extension, not implicit behavior of every optional_branch.
When this happens, the campaign map/intermission UI may show multiple available missions at once. Continue Campaign should reopen that campaign map/intermission state rather than auto-launching an arbitrary node.
Graph merge at load time:
- Engine loads the parent campaign graph (e.g.,
allied_campaign) - Engine scans for installed campaign extensions that declare
extends: allied_campaign - For each enabled extension, the engine validates:
parent_missionexists in the parent graphparent_outcomeis a valid outcome for that mission- No conflicting insertions (two extensions trying to
insert_beforethe same mission) - Extension missions don’t create graph cycles
- Extension missions are merged into the combined graph with their
badgemetadata - The combined graph is what the player sees in mission select
Player control:
- Extensions are opt-in. The campaign settings screen shows installed extensions with toggle switches:
Allied Campaign ✅ Classic missions (always on) 🔲 IC Enhanced Edition — optional operations using new mechanics 🔲 Community: "Tanya Chronicles" by MapMaster — 5 additional commando operations - Disabling an extension removes its missions from the graph. The original campaign is always playable without any extensions
- Campaign progress (D021
CampaignState) tracks which extension missions were completed, so enabling/disabling extensions mid-campaign is safe — completed extension missions remain in history, and the player continues from wherever they are in the original graph
Workshop support: Campaign extensions are Workshop packages (D030/D049) with type = "campaign_extension" in their mod.toml. The Workshop listing shows which parent campaign they extend. Installing an extension automatically associates it with the parent campaign in the mission select UI.
# mod.toml for a campaign extension
[mod]
id = "allied-enhanced"
title = "Allied Campaign — Enhanced Edition"
type = "campaign_extension"
extends_campaign = "allied_campaign" # which campaign this extends
[dependencies]
"official/ra1" = "^1.0" # requires the base RA1 module
First-Party Enhanced Campaigns (IC Enhanced Edition)
IC ships the classic RA1 Allied and Soviet campaigns faithfully reproduced (Phase 4). Alongside them, IC offers an optional “Enhanced Edition” — a campaign extension that weaves Counterstrike/Aftermath expansion missions into the main campaign graph, adds IC-original missions showcasing new mechanics, and introduces branching decisions. Like XCOM: Enemy Within to Enemy Unknown, or Baldur’s Gate Enhanced Edition — the same campaign, but richer.
Guiding principle: The classic campaign is always available untouched. Enhanced Edition is opt-in. A player who wants the 1996 experience gets it. A player who wants to see what a modern engine adds enables the extension. Both are first-class.
What the Enhanced Edition Adds
1. Counterstrike & Aftermath missions integrated into the campaign graph
The original expansion packs (Counterstrike: 16 missions, Aftermath: 18 missions) shipped as standalone, play-in-any-order mission sets with no campaign integration. The Enhanced Edition places them into the main campaign graph at chronologically appropriate points, as optional operations:
Classic Allied Campaign (linear):
A01 → A02 → A03 → A04 → ... → A14
Enhanced Edition (branching, with expansion missions woven in):
A01 → A02 → A03 ─┬─ A04 → A05 ─┬─ A06 → ...
│ │
└─ CS-A03 └─ AM-A02
(Counterstrike (Aftermath
optional op, optional op,
optional) optional)
Some expansion missions become alternative versions of main missions (offering the enhanced IC version alongside the classic). Some become SpecOps branches or Theater Branches that the player can take or skip. A few become mandatory in the Enhanced Edition flow where they fill narrative gaps.
Enhanced Edition optional-content taxonomy:
- Main Operation — the campaign backbone. Full base building, full economy, full faction expression, decisive war outcomes.
- SpecOps / Commando Operation — hero-led precision mission. Used for intel theft, sabotage, tech gain or denial, faction favor, rescue, and other high-stakes interventions. Failure or skipping can worsen the campaign state.
- Commander-Supported SpecOps — still a SpecOps mission, but the commander can field a limited support footprint: a small forward base, restricted tech, support powers, artillery, or extraction cover. It is not a full macro mission.
- Theater Branch — optional secondary-front chain such as Siberia, Poland, Italy, Spain, or France. These justify themselves by opening a whole regional front, allied faction, or tech package. They should grant concrete assets; skipping them usually means no extra advantage, not baseline punishment.
Generic “side mission” is not a sufficient authored category by itself. Every optional node should be written either as a SpecOps / Commando operation or as a Theater Branch with a named downstream asset.
First-party policy for SpecOps content: Hand-authored official missions should be used once, where they fit the story best. After that, additional SpecOps content should default to generated unique operations rather than reusing the same authored map as a second or third branch. The authored mission is the showcase beat; the follow-up operations are generated from campaign-aware templates.
Generated SpecOps Missions (XCOM 2-style, Deterministic)
Campaigns should support generated SpecOps missions built from authored map kits, objective modules, and deterministic seeds. This is the preferred way to supply optional commando content beyond the one best-fit use of an official handcrafted mission.
For prior-art analysis and an end-to-end proof-of-concept schema, see:
research/generated-specops-missions-study.md- Generated SpecOps Prototype
Design intent:
- Hand-authored missions are for landmark story beats, famous set pieces, final assaults, and one-time narrative reveals
- Generated SpecOps missions are for repeatable or branch-variable operations: prison breaks, tech theft, radar sabotage, scientist extraction, convoy ambush, safe-house defense, counter-intelligence sweep
- The player should feel that each available SpecOps opportunity is a fresh operation in the same war theater, not the exact same map being recycled
Authoring model:
- Site kits — authored parcel libraries for mission locations such as prison compounds, radar stations, ports, rail yards, research labs, villas, command bunkers, village safe houses
- Objective modules — rescue cell block, comms terminal, reactor room, prototype crate vault, scientist office, AA control room, extraction helipad
- Ingress / egress modules — sewer entry, cliff rope point, truck gate, beach landing, rooftop insertion, tunnel exit
- Security modules — patrol graphs, alarm towers, dog pens, spotlight yards, reserve barracks, timed QRF spawn rooms
- Complication modules — power outage, wounded VIP, weather front, ticking data purge, prisoner transfer countdown, fuel fire, moving convoy
The generator assembles a map from these authored pieces under strict validation rather than freeform noise-based terrain synthesis.
Modder authoring contract: Generated SpecOps is a normal modding surface, not a first-party-only feature. Modders register authored building blocks through merged YAML registries:
generated_site_kits:
soviet_coastal_radar_compound:
theater: greece
legal_families:
- intel_raid
- tech_denial
generated_objective_modules:
steal_radar_codes:
family: intel_raid
required_sockets:
- command_bunker
generated_complication_modules:
data_purge_timer:
families:
- intel_raid
- counter_intel
The builtin generator owns the base assembly pipeline. Mods extend it in three layers:
- YAML — register site kits, objectives, ingress/egress modules, security profiles, and complication modules
- Lua — filter or reweight candidate pools from campaign flags and run deterministic post-generation acceptance hooks
- WASM (optional, Tier 3) — replace candidate scoring or validation for total conversions through a pure deterministic generation interface; no network/filesystem capabilities and no sim-tick coupling
First-party content should be achievable with YAML + Lua. WASM is the escape hatch, not the default path.
Example hook surface:
generated_specops_profiles:
allied_intel_raid_t1:
family: intel_raid
lua_hooks:
candidate_filter: "SpecOpsGen.filter_candidates"
post_validate: "SpecOpsGen.post_validate"
function SpecOpsGen.filter_candidates(ctx, pools)
if Campaign.get_flag("siberian_window_closed") then
pools.site_kits["snow_signal_outpost"] = nil
end
return pools
end
function SpecOpsGen.post_validate(ctx, instance)
return instance.validation_report.estimated_duration_minutes <= 15
end
The editor / CLI validator should execute the same hooks used by runtime generation so mod authors can test the exact authored pools they ship.
Concrete generation pipeline:
generated_specops:
operation_id: allied_specops_07
mission_family: tech_theft
theater: greece
tileset: mediterranean_industrial
site_kit: soviet_research_compound
objective_module: prototype_vault
ingress_module: sewer_entry
egress_module: cliff_exfil
complication_module: data_purge_timer
security_tier: 3
seed: 1844674407370955161
Generation order:
- Pick mission family from campaign state (
intel_raid,tech_theft,tech_denial,rescue,faction_favor,counter_intel) - Pick a site kit that matches the theater and story state
- Place required objective modules
- Place at least two valid ingress paths and one valid exfil path
- Lay down security modules and patrol graphs
- Add one authored complication module
- Run validation:
- hero can physically reach the main objective
- stealth route exists if the mission advertises stealth
- loud route exists if the mission advertises a commander-supported assault
- evac path remains reachable after alarms trigger
- objective spacing and detection coverage meet authored bounds
- Persist the resulting seed + chosen modules into
CampaignStateso save/load, replay, and takeover all refer to the same generated mission
Runtime generation failure policy: Generated missions must never dead-end the campaign. If the candidate pool is exhausted:
- Development / editor validation: hard error, because the authored pool is insufficient
- Runtime for shipped content: use the authored fallback policy attached to the operation
Recommended fallback modes:
authored_backup— switch to a known handcrafted backup missionresolve_as_skipped— consume the operation as missed and apply its skip effects
Low-stakes optional missions may use resolve_as_skipped. High-stakes spotlight ops should prefer authored_backup.
Determinism rule: The generated mission must be reproducible from campaign state. Same campaign seed + same operation state = same generated map. The chosen generation seed and module picks are persisted when the operation appears, not rerolled every time the player opens the briefing.
Recommended mission grammar for generated SpecOps:
| Mission Family | Primary Objective | Optional Objective | Common Failure State |
|---|---|---|---|
intel_raid | photograph / hack / steal plans | stay undetected | enemy gains alertness, later mission loses intel route |
tech_theft | recover prototype / scientist / crate | extract secondary cache | prototype destroyed or incomplete on failure |
tech_denial | sabotage reactor / radar / lab / ammo dump | plant false intel | enemy asset survives if ignored or failed |
rescue | free hero / VIP / prisoners | recover records / gear | captive transferred, interrogation escalates |
faction_favor | save allied contact / resistance cell | secure local cache | no faction support package gained |
counter_intel | find mole / seize documents / silence informants | identify second network node | enemy keeps scouting advantage |
Map-design rules for generated SpecOps:
- Readable objective triangle. The player should be able to understand ingress → target → exfil at a glance once scouting begins.
- Two playstyles minimum. Every generated SpecOps map should support a stealth-first route and a loud contingency route, even if one is clearly riskier.
- One dominant complication, not five. A timer, storm, prisoner transfer, or fuel fire is enough. Generated missions should not become procedural soup.
- Short and sharp. Generated SpecOps should usually land in a 10-15 minute target band.
20 minutesis the hard cap, not the default expectation. - Theater-consistent visuals. Greece does not generate the same compounds as Poland; Spain does not look like Siberia. Site kits are theater-bound.
- Rewards must stay campaign-specific. The map is generated, but the reward and downstream consumer come from the campaign node that spawned it.
Commander-supported generated SpecOps: When the operation advertises commander support, generation must reserve a bounded support zone with:
- one limited landing zone or forward camp
- an explicit support-only package, not an open-ended build tree
- no full economy escalation
- clear relationship to the commando route (artillery cover, extraction, diversion, repair)
That keeps the generated SpecOps mission from collapsing into a normal main-operation base map.
Canonical support-only package: Unless a profile overrides it more tightly, commander-supported SpecOps should be bounded to:
- Structures: field HQ, power node, repair bay, medic tent, sensor post, artillery uplink, helipad / extraction pad
- Support powers: recon sweep, smoke, off-map artillery, extraction beacon, emergency repair
- Units: engineers, medics, transports, light APCs, and at most a few light escort vehicles
- Explicitly disallowed: refinery / ore economy, heavy factory, tech center, superweapons, unlimited turret creep, heavy armor production
The commander is providing support, not playing a second full macro match.
First-party Enhanced Edition usage rule: Use the classic official mission once where it is the strongest narrative fit. If the campaign later needs “another prison break”, “another spy raid”, or “another sabotage op”, it should spawn a generated SpecOps node in the same mission family rather than reusing the same canonical mission again.
2. IC-original missions showcasing platform capabilities
New missions designed specifically to demonstrate what IC’s engine can do that the original couldn’t:
| IC Feature | Enhanced Edition Mission | What It Demonstrates |
|---|---|---|
| Hero progression (D021) | “Tanya: First Blood” — a prologue mission before A01 where Tanya earns her first skill points | Hero XP, skill tree, persistent progression across the entire campaign |
| Embedded task force (§ Special Ops Archetypes) | “Evidence: Enhanced” — Tanya + squad operate inside a live battle around the facility approach | Hero-in-battle gameplay: live AI-vs-AI combat around the player’s special ops objectives |
| Branching decisions | After M10A, choose: classic interior raid, embedded task-force variant, or commander-led siege | Multiple paths with different rewards and persistent consequences |
| Air campaign | “Operation Skyfall” — coordinate air strikes and recon flights supporting a ground resistance attack | Air commander gameplay: sortie management, target designation, no base building |
| Spy infiltration | “Behind Enemy Lines” — Tanya infiltrates a Soviet facility for Iron Curtain intel | Detection/alert system, sabotage effects weakening later Soviet operations |
| Dynamic weather (D022) | “Protect the Chronosphere” — a storm rolls in during the defense, reducing visibility and slowing vehicles | Weather as gameplay: the battlefield changes mid-mission, not just visually |
| Asymmetric co-op (D070, Phase 6b add-on) | “Joint Operations” — optional co-op variant where Player 1 is Commander (base) and Player 2 is SpecOps (Tanya’s squad) | The full Commander & SpecOps experience within the campaign once D070 ships |
| Campaign menu scenes | Each act changes the main menu background — Act 1: naval convoy, Act 2: night air patrol, Act 3: ground assault, Victory: sunrise over liberated city | Evolving title screen tied to campaign progress |
| Unit roster carryover (D021) | Surviving units from “Tanya: First Blood” appear in A01 as veteran reinforcements | Persistent roster: lose a unit and they’re gone for the rest of the campaign |
| Failure as branching | If “Behind Enemy Lines” fails, Tanya is captured and the rescue branch can stay pending for a bounded window while penalties escalate; ignoring it leaves permanent consequences | No game-over screen: failure changes the next branch and mission conditions instead of forcing a reload |
3. Decision points that create campaign variety
At key moments, the Enhanced Edition offers the player a choice:
# Example: after completing mission A05, the player chooses
missions:
allied_05:
outcomes:
victory:
# Enhanced Edition adds a decision point here
decision:
prompt: "Command has two plans for the next phase."
choices:
- label: "Frontal Assault (Classic)"
description: "Attack the Soviet base directly with full armor support."
next: allied_06 # original mission
badge: "Classic"
- label: "Sewer Infiltration (Enhanced)"
description: "Send Tanya through the sewers to sabotage the base from within before the assault."
next: allied_05b_infiltration # new IC mission
badge: "IC Enhanced"
requires_hero: tanya # only available if Tanya is alive
- label: "Air Strike First (Counterstrike)"
description: "Soften the target with a bombing campaign before committing ground forces."
next: cs_allied_04 # Counterstrike expansion mission
badge: "Counterstrike"
unchosen_effects:
set_flag:
air_strike_window_missed: true
Choices may optionally define unchosen_effects on individual entries. When the player picks one branch, the engine applies the unchosen_effects from every branch not taken. This lets authors build mutually-exclusive decision points using the same D021 decision primitive without inventing a new node type (though expiring open-world opportunities prefer the mission node’s expires_in_phases timer).
4. Expansion mission enhancements
When Counterstrike/Aftermath missions are played within the Enhanced Edition, they gain IC features they didn’t originally have:
- Briefing/debrief flow — continuous narrative instead of standalone mission select
- Roster carryover — units from the previous mission carry over (the originals had no persistence)
- Weather effects — maps gain dynamic weather appropriate to their setting (Italian missions get Mediterranean sun; Polish missions get winter conditions)
- Veterancy — units earn experience and carry it forward
- Multiple outcomes — original missions had win/lose; Enhanced Edition adds alternative victory conditions (“won with low casualties” → bonus reinforcements next mission)
5. Ideas drawn from other successful enhanced editions
| Source | What They Did | IC Application |
|---|---|---|
| XCOM: Enemy Within | Added new resources (Meld), new soldier class (MEC), new mission types woven into the same campaign timeline. Toggleable mission chains (Operation Progeny) | IC adds new resource mechanics (hero XP), new mission types (task force, air campaign, spy infiltration) woven into RA1 timeline. Each chain is toggleable |
| Baldur’s Gate Enhanced Edition | Added 3 new companion characters with unique quest lines that interleave with the main story. New arena mode. QoL improvements | IC adds Tanya hero progression as a “companion” quest line threading through the campaign. Highlight reel POTG after each mission |
| StarCraft: Brood War | “Final Fantasy type events” during missions — scripted narrative beats within gameplay. Tactical decisions over which objectives to pursue | IC’s embedded task force missions do exactly this — live battle with narrative events and objective choices |
| Warcraft III: Frozen Throne | Each campaign had a distinct tone/gameplay style (RPG-heavy Rexxar campaign vs. base-building Scourge campaign) | Enhanced Edition missions vary: some are base-building (classic), some are hero-focused (task force), some are air-only (air campaign) |
| C&C Remastered Collection | Bonus gallery content — behind-the-scenes art, remastered music, developer commentary | IC Enhanced Edition could include mission designer commentary (text/audio notes the player can toggle during briefings) |
| Halo: Reach | Menu background and music change per campaign chapter | Campaign menu scenes: each act of the Enhanced Edition changes the main menu |
| Fire Emblem | Permadeath creates emotional investment in individual units; branching paths based on who survives | IC’s roster carryover does this — lose Tanya early and the entire campaign arc changes. Some missions only exist if specific heroes survived |
Campaign Structure Overview
┌──────────────────────────────────────────────┐
│ ALLIED ENHANCED EDITION CAMPAIGN GRAPH │
│ │
Prologue (IC) │ [Tanya: First Blood] │
│ │ │
Act 1: Coastal War │ A01 ──┤── CS-A01 (optional Counterstrike) │
(Classic + Expansion) │ A02 ──┤── CS-A02 (optional) │
│ A03 ──┼── IC: Dead End / SpecOps branch │
│ │ │
Decision Point ───────│────────┼── Choose: A04 (classic) OR │
│ │ IC: Behind Enemy Lines (SpecOps) │
│ │ │
Act 2: Interior Push │ A05 ──┤── AM-A01 (optional Aftermath) │
(Classic + Enhanced) │ A06 ──┤── IC: Protect the Chronosphere (weather) │
│ A07 ──┤── IC: Operation Skyfall (theater air) │
│ │── CS-A03 (optional) │
│ │ │
Decision Point ───────│────────┼── Choose: A08 (classic) OR │
│ │ IC: Evidence hybrid / siege │
│ │ │
Act 3: Final Push │ A09 ──┤── AM-A02 (optional) │
(Classic + Add-ons) │ A10 ──┤── IC: Joint Ops (co-op, D070, Phase 6b add-on) │
│ ... │ │
│ A14 ──┤── IC: Epilogue (post-campaign) │
│ │ │
└──────────────────────────────────────────────┘
Player Settings
Campaign Settings — Allied Campaign
✅ Classic Missions (14 missions — always available)
🔲 IC Enhanced Edition
✅ Counterstrike Missions (woven into campaign timeline)
✅ Aftermath Missions (woven into campaign timeline)
✅ IC Original Missions (task force, air campaign, spy ops)
✅ Hero Progression (Tanya XP/skills across all missions)
✅ Decision Points (choose your path at key moments)
✅ Dynamic Weather
✅ Campaign Menu Scenes
🔲 Co-op Missions (requires second player)
Each sub-feature is independently toggleable. A player can enable Counterstrike integration but disable IC original missions. The campaign graph adjusts — disabled branches are hidden, and the graph reconnects through the classic path.
Campaign World Screen as Strategic Layer
For first-party narrative campaigns, the campaign map / intermission screen should do more than list the next mission. It should act as the strategic layer of the campaign: the place where the player understands what fronts are active, what operations are available, what is urgent, and what assets the war has already produced.
This is a first-class D021 mode, not just UI dressing. The mission graph still owns legal progression, but the War Table owns the campaign-facing presentation of:
- current phase
- requisition and intel
- command authority
- active operations
- enemy initiatives
- tech / arms-race ledger
The model is closer to the XCOM globe than to a flat mission picker:
- Main operations anchor the campaign backbone
- SpecOps operations appear as urgent, high-leverage interventions
- Theater branches appear as secondary fronts with long-arc value
- Enemy projects and captured-hero crises remain visible between missions until resolved or expired
- Campaign assets (intel, prototypes, resistance favor, air packages, denied enemy tech) remain visible so the player can reason about future choices
This strategic layer can be presented in multiple authoring styles:
- Node-and-edge graph — the default for community campaigns and compact narrative campaigns
- Authored world screen / front map — the preferred presentation for first-party Enhanced Edition campaigns, where fronts such as Greece, Siberia, Poland, Italy, Spain, or England are shown as active theaters
- Full territory simulation — D016-style world-domination campaigns that persist explicit region ownership and garrisons
Regardless of presentation style, an available operation should surface the same information:
- Role tag:
MAIN,SPECOPS,THEATER, or another authored role - Criticality: recoverable, critical, rescue, or timed
- Urgency: normal, expiring, critical, rescue, enemy project nearing completion
- Reward preview: the concrete asset gained on success
- Operation reveal / unlock preview: any mission cards that appear or become selectable because of this operation
- If ignored / if failed: the concrete state change if the player does not act
- Downstream consumer: which next mission, act, or final assault will use that asset
Example operation cards:
campaign_world_screen:
fronts:
- id: greece
status: "Sarin sites active"
urgency: critical
- id: siberia
status: "Window open for second front"
urgency: expiring
operations:
- mission: ic_behind_enemy_lines
role: specops
criticality: recoverable
reward_preview: "M6 access codes; better Iron Curtain intel"
reveal_preview: "Reveals Spy Network if the raid succeeds cleanly"
effect_detail: "Reveal the east service entrance in M6 and delay the first alarm by 90 seconds"
failure_consequence: "Tanya captured or M6 infiltration runs blind"
if_ignored: "M6 runs blind; the spy-network follow-up closes"
if_ignored_detail: "Tanya stays safe, but the Soviet site hardens before Act 2"
time_window: "Expires in 2 operation phases"
reveals_operations:
- ic_spy_network
consumed_by:
- allied_06
- ic_spy_network
- mission: cs_sarin_gas_1
role: specops
criticality: timed
reward_preview: "Chemical attacks denied in M8"
effect_detail: "No gas shelling or chemical infantry waves during the Chronosphere defense"
failure_consequence: "Facility not neutralized in time; M8 uses chemical attacks"
if_ignored: "Sarin active in Chronosphere defense"
if_ignored_detail: "M8 gains two gas-shell barrages and one contaminated approach lane"
time_window: "Expires in 2 operation phases"
consumed_by:
- allied_08
- mission: am_poland_1
role: theater
criticality: recoverable
reward_preview: "Super Tanks + partisan chain"
reveal_preview: "Reveals Poland follow-up operations if the first liberation succeeds"
effect_detail: "Unlock 2 Super Tanks for Act 3 and the Poland resistance follow-up chain"
failure_consequence: "No Poland chain rewards, but campaign continues normally"
if_ignored: "Poland branch closes"
if_ignored_detail: "No Super Tanks, no partisan reinforcements, no Poland follow-up nodes"
reveals_operations:
- am_poland_2
- am_poland_3
consumed_by:
- allied_12
- allied_14
The player should be able to answer, from the world screen alone: What is happening? What can I do? What do I gain? What do I lose by waiting?
reward_preview and if_ignored are the short card headlines. First-party authored campaigns should also provide an exact-effect sentence (effect_detail / if_ignored_detail) so the player sees the real mechanical consequence, not just a slogan.
For SpecOps, the operation card / mission briefing should show four fields together whenever possible:
- Success reward — what concrete asset you gain
- Failure consequence — what happens if you attempt it and fail
- Skip / ignore consequence — what happens if you do not take it at all
- Time window / urgency — whether it must be taken now, can be delayed, or can remain open indefinitely
- Operation reveal / unlock — whether success exposes a new SpecOps card, Theater Branch, or commander operation on the strategic map
Optional Operations — Concrete Assets, Not Abstract Bonuses
Optional content must feed back into the main campaign in tangible, visible ways. If an optional operation does not produce a concrete downstream asset, denial, or rescue state, it feels disconnected and should be cut.
In practice, first-party Enhanced Edition content should default to:
- SpecOps / Commando Operations for intel, tech capture, tech denial, faction favor, rescue, counter-intelligence, and commander-supported infiltration
- Theater Branches only when the branch represents a whole secondary front or campaign-scale support package
Every optional operation should answer five concrete questions:
- What class is it?
specops,commander_supported_specops, ortheater_branch - What asset or state does it produce? Intel, tech unlock, enemy tech denial, faction favor, route access, roster unit, support package, rescue state
- What exact effect does that asset have? Not “better position” but “reveal 25% of map”, “unlock 2 Super Tanks”, “delay reinforcements by 180s”, “open Poland chain”
- Which later missions consume that asset? Name the next mission(s), act, or branch
- What happens if skipped or failed? SpecOps can create negative state; Theater Branches normally only withhold upside
- Is it time-critical or critical to campaign survival? The player must know whether they can postpone it safely, and whether failure is recoverable
- Does it reveal or unlock a follow-up operation? If yes, the world screen should tell the player what new commander or SpecOps card appears
Concrete operation-output categories:
- Intel Ops — reveal routes, access codes, shroud, patrol schedules, composition, branch availability
- Operation-Reveal Ops — expose hidden sites, convoys, labs, safe houses, assault windows, or regional follow-up branches that become new selectable mission cards
- Tech Ops — unlock prototypes, support powers, expansion-pack units, or equipment pools
- Denial Ops — prevent enemy deployment, delay strategic capabilities, disable defenses, disrupt production lines, or close enemy branches
- Program-Interference Ops — hit one stage of an enemy capability program (materials, prototype, training, doctrine rollout, test, deployment, sustainment) to deny, delay, degrade, corrupt, capture, or expose it
- Faction Favor Ops — gain resistance cells, defectors, scientists, naval contacts, partisans, or local guides
- Third-Party Influence Ops — win, protect, arm, evacuate, or keep hold of local actors so they provide staging rights, FOBs, support packages, route access, or local knowledge instead of helping the enemy
- Prevention Ops — stop a collaborator network, coerced militia, captured depot, or compromised safehouse from turning into a later enemy advantage
- Endurance-Shaping Ops — attack or preserve supply corridors, depots, bridges, power, and relief routes so later missions change how long each side can sustain pressure
- Rescue / Recovery Ops — retrieve captured heroes, reduce compromise level, recover stolen prototypes, save wounded rosters
Reward design principles:
- Specific, not generic. “Enemy has no air support next mission” is meaningful. “Better position” is too vague.
- Visible consequence. The next main mission briefing should reference the operation: “Thanks to your sabotage, Soviet air defenses are offline.”
- SpecOps can create negative state; Theater Branches usually should not. A failed rescue or sabotage can hurt. Skipping Poland or Italy should usually just deny the extra asset.
- Cumulative assets. Spy network + radar sabotage = full battlefield intel. Resistance favor + harbor secured = naval insertion route plus reinforcements.
- Exclusive content. Some branches, units, and final approaches should exist only if specific operations were completed.
- Quantify anything that changes difficulty. Prefer “first reinforcement wave delayed 180 seconds,” “2 Super Tanks added to M14,” or “40% of the map revealed at mission start” over “better intel” or “harder defense.”
- Differentiate attempt-failure from expiration. A failed SpecOps raid and an ignored operation that expired are not always the same state; the authored card and briefing should say which consequence belongs to which.
- Let SpecOps reveal commander work. An intel raid, sabotage, or defector extraction can reveal a new commander operation card such as an interception, assault window, convoy ambush, or theater branch.
- Treat third-party actors as persistent theater assets. A won militia should grant a real start location, FOB, safe route, reinforcements, or support package; a lost militia should create a concrete enemy advantage, not just remove a possible buff.
- Let prevention matter. Some optional operations exist primarily to stop a bad future state. “No rear-area uprising in M14” or “enemy does not get urban guides next mission” is a valid high-value reward.
- Endurance missions should culminate, not merely count down. When logistics and time pressure are the fantasy, use operations to lengthen or shorten each side’s sustainment window; when a clock hits zero, production stall, reinforcement failure, withdrawal, or morale break is often a better consequence than total annihilation.
- Capability timing must be authored, not emergent chaos. If optional operations can make MiGs, satellite recon, heavy armor, or advanced naval assets arrive earlier or later, the campaign must define the baseline timing, the allowed shift window, and the exact early/on-time/late mission variants that consume that timing.
- Reward intel chains, not just isolated raids. When one operation reveals a follow-up operation (e.g., an intel raid exposes a prototype lab, which becomes a new Tech Theft card), completing the chain in sequence should grant a compound chain bonus — better reward quality, an exclusive branch, or a unique asset that neither operation alone would produce. This rewards investigative depth over cherry-picking the easiest ops and makes “follow the thread” a satisfying strategic pattern.
Commander Alternatives Must Quantify Their Trade-Offs
When a commando-heavy mission offers a commander-compatible path, the choice must describe the exact downstream difference between the precise approach and the loud one. “Less intel” or “cruder result” is not enough.
mission_variants:
allied_06_iron_curtain:
operative:
reward_preview: "Steal access codes and shipping manifests"
effect_detail: "M7 reveals 25% of the harbor and delays the first submarine wave by 120 seconds"
commander:
reward_preview: "Destroy the Tech Center by assault"
effect_detail: "M7 loses the harbor reveal and delayed sub wave, but one shore battery starts already destroyed"
Good commander-alternative descriptions answer four things:
- What story result is preserved? Rescue, assassination, capture, sabotage, or destruction still happens.
- What exact asset is weaker or missing? Fewer access codes, no safe tunnel, one missed scientist, no patrol route, shorter setup time.
- What exact military upside does the commander path get instead, if any? A destroyed battery, rescued hero, intact bridgehead, or pre-cleared escort lane.
- Which later mission consumes that trade-off? Name the consumer mission and the exact effect there.
Reward categories:
# Optional operation outcome → main campaign effect
# All implemented via existing D021 state_effects + flags + asset-ledger entries + Lua
# ── SpecOps / direct tactical advantage ────────────────────────────
optional_operation_rewards:
destroy_radar_station:
flags:
soviet_radar_destroyed: true
main_mission_effect: "Next mission: no enemy air strikes (MiGs grounded)"
briefing_line: "Thanks to your raid on the radar station, Soviet air command is blind."
sabotage_ammo_dump:
flags:
ammo_supply_disrupted: true
main_mission_effect: "Next mission: enemy units start at Rookie veterancy (supplies lost)"
briefing_line: "Without ammunition reserves, their veterans are fighting with scraps."
destroy_power_grid:
flags:
power_grid_down: true
main_mission_effect: "Next mission: enemy Tesla Coils and SAM sites are offline for 3 minutes"
briefing_line: "The power grid is still down. Their base defenses are dark — move fast."
# ── Theater branch / roster additions ──────────────────────────────
rescue_engineer:
roster_add:
- engineer
main_mission_effect: "Engineer joins your roster permanently — can repair and capture"
briefing_line: "The engineer you rescued has volunteered to join your task force."
capture_prototype_tank:
roster_add:
- mammoth_tank_prototype
- mammoth_tank_prototype
main_mission_effect: "2 prototype Mammoth Tanks available for the final assault"
briefing_line: "Soviet R&D won't be needing these anymore."
liberate_resistance_fighters:
roster_add:
- resistance_squad
- resistance_squad
- resistance_squad
main_mission_effect: "3 Resistance squads join as reinforcements in Act 3"
# ── Intel, favor, and denial assets ────────────────────────────────
establish_spy_network:
flags:
spy_network_active: true
main_mission_effect: "Future missions: start with partial shroud revealed (intel from spy network)"
briefing_line: "Our agents have mapped their positions. You'll know where they are."
intercept_communications:
flags:
comms_intercepted: true
main_mission_effect: "Enemy AI coordination disrupted — units respond slower to threats"
gain_partisan_favor:
flags:
polish_resistance_favor: true
main_mission_effect: "Polish partisans sabotage one reinforcement rail line and send 3 infantry squads in M14"
briefing_line: "The resistance has committed to the final push. Their rail sabotage buys us time."
deny_super_tank_program:
flags:
soviet_super_tank_denied: true
main_mission_effect: "Enemy cannot field Super Tanks in the final act"
briefing_line: "The prototype line is in ruins. Moscow will not be deploying Super Tanks."
falsify_nuclear_test:
flags:
soviet_nuke_program_unstable: true
main_mission_effect: "Enemy reaches deployment later, but the first strike window is unreliable or aborts outright"
briefing_line: "Their test data is poisoned. They may think the device is ready. It isn't."
sabotage_iron_curtain_calibration:
flags:
iron_curtain_calibration_corrupted: true
main_mission_effect: "Enemy can still trigger the system, but the first pulse may miss, underperform, or backfire"
briefing_line: "The emitters are out of sync. If they fire that machine, it may hurt them more than us."
sabotage_heavy_tank_tooling:
flags:
enemy_heavy_armor_quality_reduced: true
main_mission_effect: "Enemy still fields heavy armor, but it arrives later, in lower numbers, or with weaker veterancy and repair rate"
briefing_line: "Their heavy-tank line is still running, but the workmanship is poor and the output is behind schedule."
raid_winterization_depot:
flags:
enemy_winter_package_denied: true
main_mission_effect: "Enemy vehicles lose their cold-weather edge in the next snow mission and reinforcement timing slips"
briefing_line: "Their winter gear never reached the front. The cold will hurt them as much as us."
capture_radar_cipher_team:
flags:
enemy_air_coordination_degraded: true
main_mission_effect: "Enemy air and artillery coordination becomes slower, less accurate, or partially blind in the next act"
briefing_line: "Their cipher team is gone. Expect confusion between their spotters and strike wings."
accelerate_mig_readiness:
flags:
soviet_mig_timing: early
main_mission_effect: "Enemy MiGs appear 1 phase earlier than the baseline campaign schedule in authored consumer missions"
briefing_line: "Their air arm is ahead of schedule. Expect MiGs sooner than command predicted."
delay_advanced_naval_group:
flags:
soviet_naval_group_timing: delayed
main_mission_effect: "Enemy cruiser / submarine package arrives 1 phase late or understrength in authored naval consumer missions"
briefing_line: "The drydocks are damaged and the fuel trains are late. Their fleet won't be ready on time."
secure_satellite_uplink:
flags:
allied_satellite_recon_timing: early
main_mission_effect: "Player reconnaissance package comes online 1 phase early in authored consumer missions"
briefing_line: "The uplink is live ahead of schedule. Strategic reconnaissance is now in play."
unlock_aftermath_prototype:
flags:
chrono_tank_salvaged: true
main_mission_effect: "Chrono Tank prototype added to equipment pool for Act 3"
briefing_line: "Our engineers restored one functional Chrono Tank from the Italian salvage yard."
# ── Third-party support and blowback ───────────────────────────────
arm_the_resistance:
flags:
greece_resistance_active: true
greece_forward_lz_unlocked: true
main_mission_effect: "Next theater mission starts from a forward village with 2 militia squads and a live repair depot"
briefing_line: "The resistance has opened a landing zone inland. You'll hit the next target from much closer."
rescue_partisan_engineers:
flags:
partisan_engineers_active: true
main_mission_effect: "A friendly engineer team repairs one bridge and prebuilds one power plant in the next act"
briefing_line: "The engineers you pulled out are already at work on our route east."
ignore_city_plea:
flags:
belgrade_militia_coerced: true
main_mission_effect: "Enemy gains urban guides, one ambush marker, and a rear-area FOB in the next city mission"
briefing_line: "We left the city to its fate. The occupiers now have local hands helping them."
# ── Endurance shaping / operational sustainment ────────────────────
secure_supply_corridor:
flags:
allied_endurance_bonus_seconds: 180
main_mission_effect: "Your next bridgehead hold lasts 180 seconds longer before supply exhaustion sets in"
briefing_line: "The corridor is open. Your forces can hold the line longer than expected."
destroy_fuel_reserve:
flags:
enemy_endurance_penalty_seconds: 240
main_mission_effect: "Enemy offensive culminates 240 seconds earlier in the next endurance mission"
briefing_line: "Their fuel reserve went up in flames. They cannot keep this tempo for long."
# Training cadre acquisition — the asset required by support-operative promotion (see §Support Operatives)
establish_specops_cadre:
flags:
allied_specops_training_cadre: true
main_mission_effect: "Unlocks elite black-ops team promotion pipeline; surviving veteran infantry can now be promoted to SpecOps detachments between missions"
briefing_line: "The commando school is running. Our best soldiers can now train for special operations."
# ── Soft-timer endurance mission example ───────────────────────────
# A bridgehead hold where supply depletion is the clock.
# Earlier optional ops shape how long each side can sustain pressure.
#
# mission:
# id: allied_11_bridgehead
# type: endurance
#
# endurance_system:
# player_sustainment:
# base_ticks: 12000 # 10 minutes before supply exhaustion
# bonus_from_flags:
# allied_endurance_bonus_seconds: ticks # add seconds from §secure_supply_corridor
# on_exhaustion:
# effect: production_stall # refineries stop, repair halts, no new units
# ui_warning_at_remaining: 2400 # 2-minute warning
# enemy_sustainment:
# base_ticks: 14400 # enemy lasts 12 minutes baseline
# penalty_from_flags:
# enemy_endurance_penalty_seconds: ticks # subtract from §destroy_fuel_reserve
# on_exhaustion:
# effect: offensive_culmination # enemy stops attacking, retreats to defensive positions
# briefing_hint: "Their fuel ran out. Expect their advance to stall."
#
# objectives:
# primary:
# - id: hold_bridgehead
# description: "Hold the crossing until the enemy offensive culminates"
# completion: enemy_exhausted
# secondary:
# - id: capture_forward_depot
# description: "Seize the enemy depot to extend your own sustainment"
# reward:
# player_sustainment_bonus_ticks: 3600 # +3 minutes
# - id: destroy_fuel_train
# description: "Destroy the incoming fuel convoy to shorten the enemy window"
# reward:
# enemy_sustainment_penalty_ticks: 2400 # -2 minutes
#
# outcomes:
# victory_held:
# condition: "bridgehead_intact AND enemy_exhausted"
# description: "The enemy ran out of steam. The bridgehead is secure."
# defeat_overrun:
# condition: "bridgehead_destroyed"
# description: "Our supply lines collapsed first."
# ── Compound bonuses (multiple successful operations stack) ────────
# If both spy network AND radar destroyed:
compound_full_intel:
requires:
spy_network_active: true
soviet_radar_destroyed: true
main_mission_effect: "Full battlefield intel — all enemy units visible at mission start"
briefing_line: "Between our spies and the radar blackout, we see everything. They see nothing."
# ── Exclusive content unlocks ──────────────────────────────────────
all_act1_specops_complete:
requires:
act1_specops_complete: 3 # completed 3+ SpecOps operations in Act 1
unlocks_mission: allied_06b_secret_path # a mission that only exists for thorough players
briefing_line: "Command is impressed. They're authorizing a classified operation."
tanya_max_level:
requires_hero:
tanya:
level_gte: 4
unlocks_mission: tanya_solo_finale # Tanya-only final mission variant
briefing_line: "Tanya, you've proven you can handle this alone."
Lua implementation (reads flags set by optional operations):
-- Main mission setup: check what optional operations the player completed
Events.on("mission_start", function()
-- Direct tactical advantage
if Campaign.get_flag("soviet_radar_destroyed") then
Ai.modify("soviet_ai", { disable_ability = "air_strike" })
end
if Campaign.get_flag("ammo_supply_disrupted") then
Ai.modify("soviet_ai", { max_veterancy = "rookie" })
end
if Campaign.get_flag("power_grid_down") then
Trigger.schedule(0, function()
Trigger.disable_structures_by_type("tesla_coil", { duration_ticks = 3600 })
Trigger.disable_structures_by_type("sam_site", { duration_ticks = 3600 })
end)
end
-- Intelligence advantage
if Campaign.get_flag("spy_network_active") then
Player.reveal_shroud_percentage(0.4) -- reveal 40% of the map
end
-- Compound bonus: full intel
if Campaign.get_flag("spy_network_active") and Campaign.get_flag("soviet_radar_destroyed") then
Player.reveal_all_enemies() -- show all enemy units at mission start
UI.show_notification("Full battlefield intel active. All enemy positions known.")
end
-- Roster additions from optional operations appear as reinforcements
local rescued = Campaign.get_roster_by_flag("rescued_in_optional_operation")
if #rescued > 0 then
Trigger.spawn_reinforcements_from_roster(rescued, "allied_reinforcement_zone")
UI.show_notification(#rescued .. " units from your previous operations have joined the fight.")
end
end)
How the briefing acknowledges optional operation results:
# Main mission briefing with conditional lines
mission:
id: allied_06
briefing:
base_text: "Commander, the assault on the Soviet forward base begins at dawn."
conditional_lines:
- flag: soviet_radar_destroyed
text: "Good news — the radar station you destroyed last week has left their air command blind. No MiG support expected."
- flag: ammo_supply_disrupted
text: "Intelligence reports the ammo dump sabotage has taken effect. Enemy units are under-supplied."
- flag: spy_network_active
text: "Our spy network has provided detailed positions of their defenses. You'll have partial intel."
- flag_absent: soviet_radar_destroyed
text: "Be warned — Soviet air support is fully operational. Expect MiG patrols."
- compound:
spy_network_active: true
soviet_radar_destroyed: true
text: "Between our spies and the radar blackout, we have full battlefield awareness. Press the advantage."
The briefing dynamically assembles based on which optional operations the player completed. A player who did everything hears good news. A player who skipped everything hears warnings. The same mission, different context — the world reacts to what you did (or didn’t do).
Failure as Consequence, Not Game Over (Spectrum Outcomes)
Classic C&C missions have two outcomes: win or lose. Lose means retry. The Enhanced Edition uses spectrum outcomes — the same mission can have 3-4 named results, each leading to a different next mission with different consequences. Failure doesn’t end the campaign; it makes the campaign harder and different.
Planned example — “Destroy the Bridges” (Tanya mission):
This is the simple direct-branch pattern: capture routes immediately into a rescue mission or harder follow-up. Campaigns that want “rescue now, later, or never” escalation across later nodes should use the bounded pending-branch extension described above, not overload this simpler pattern.
mission:
id: enhanced_bridge_assault
type: embedded_task_force
hero: tanya
description: "Tanya must destroy three bridges to cut off Soviet reinforcements before the main assault."
outcomes:
# Best case: all bridges destroyed
total_success:
condition: "bridges_destroyed == 3"
next: allied_08a_easy
state_effects:
set_flag:
bridges_destroyed: 3
reinforcement_route: severed
briefing_next: "All three bridges are down. The Soviets are completely cut off. This will be a clean sweep."
# Partial success: some bridges destroyed
partial_success:
condition: "bridges_destroyed >= 1 AND bridges_destroyed < 3"
next: allied_08a_medium
state_effects:
set_flag:
bridges_destroyed: $bridges_destroyed
reinforcement_route: reduced
briefing_next: "We got some of the bridges, but enemy reinforcements are still trickling through. Expect heavier resistance."
# Failed but Tanya escaped
failed_escaped:
condition: "bridges_destroyed == 0 AND hero_alive(tanya)"
next: allied_08b_hard
state_effects:
set_flag:
bridges_intact: true
reinforcement_route: full
tanya_reputation: setback
briefing_next: "The bridges are intact. The full Soviet armored division is rolling across. Commander, prepare for a hard fight."
# Failed and Tanya captured
tanya_captured:
condition: "bridges_destroyed == 0 AND NOT hero_alive(tanya)"
next: allied_08c_rescue
state_effects:
set_flag:
bridges_intact: true
tanya_captured: true
hero_status:
tanya: captured
briefing_next: "We've lost contact with Tanya. Intel suggests she's been taken to the Soviet detention facility. New priority: rescue operation."
# Failed but Tanya killed (hero death_policy: wounded)
tanya_wounded:
condition: "hero_wounded(tanya)"
next: allied_08b_hard
state_effects:
set_flag:
bridges_intact: true
tanya_wounded: true
hero_status:
tanya: wounded # Tanya unavailable for 2 missions (recovery)
briefing_next: "Tanya is wounded and being evacuated. She'll be out of action for a while. We proceed without her."
How the next main mission reacts (Lua):
-- allied_08 setup: adapts based on bridge mission outcome
Events.on("mission_start", function()
local bridges = Campaign.get_flag("bridges_destroyed") or 0
if bridges == 3 then
-- Easy variant: enemy has no reinforcements
Ai.modify("soviet_ai", { disable_reinforcements = true })
Ai.modify("soviet_ai", { starting_units_multiplier = 0.5 })
elseif bridges >= 1 then
-- Medium: reduced reinforcements
Ai.modify("soviet_ai", { reinforcement_delay_ticks = 3600 }) -- delayed by 3 min
Ai.modify("soviet_ai", { starting_units_multiplier = 0.75 })
else
-- Hard: full enemy force, bridges intact
Ai.modify("soviet_ai", { reinforcement_interval_ticks = 1200 }) -- every 60 seconds
Ai.modify("soviet_ai", { starting_units_multiplier = 1.0 })
end
-- Tanya unavailable if captured or wounded
if Campaign.get_flag("tanya_captured") then
-- This mission becomes harder without Tanya
UI.show_notification("Tanya is being held at the detention facility. You're on your own.")
elseif Campaign.get_flag("tanya_wounded") then
UI.show_notification("Tanya is recovering from her injuries. She'll rejoin in two missions.")
end
end)
The spectrum: The player never sees a “Mission Failed” screen. Every outcome — total success, partial success, failure with escape, capture, wounding — leads to a different next mission or a different version of the same mission. The campaign continues, but the player’s performance shapes what comes next. A player who destroyed all three bridges gets a victory lap. A player who got Tanya captured gets a rescue mission they wouldn’t have otherwise seen. Both paths are content. Both are valid. Neither is “wrong.” In more advanced campaign designs, that capture branch can instead become a bounded pending rescue opportunity with escalating penalties.
This pattern applies to any Enhanced Edition mission — not just the bridge example. Every mission should have at least 2-3 meaningfully different outcomes that branch the campaign or modify the next mission’s conditions.
Unit Roster & Persistence
Inspired by Operation Flashpoint: Resistance — surviving units are precious resources that carry forward, creating emotional investment and strategic consequences.
Unit Roster:
#![allow(unused)]
fn main() {
/// Persistent unit state that carries between campaign missions.
#[derive(Serialize, Deserialize, Clone)]
pub struct RosterUnit {
pub unit_type: UnitTypeId, // e.g., "medium_tank", "tanya"
pub name: Option<String>, // optional custom name
pub veterancy: VeterancyLevel, // rookie → veteran → elite → heroic
pub kills: u32, // lifetime kill count
pub missions_survived: u32, // how many missions this unit has lived through
pub equipment: Vec<EquipmentId>, // OFP:R-style captured/found equipment
pub custom_state: HashMap<String, Value>, // mod-extensible per-unit state
}
}
Carryover modes (per campaign transition):
| Mode | Behavior |
|---|---|
none | Clean slate — the next mission provides its own units |
surviving | All player units alive at mission end join the roster |
extracted | Only units inside a designated extraction zone carry over (OFP-style “get to the evac”) |
selected | Lua script explicitly picks which units carry over |
custom | Full Lua control — script reads unit list, decides what persists |
Veterancy across missions:
- Units gain experience from kills and surviving missions
- A veteran tank from mission 1 is still veteran in mission 5
- Losing a veteran unit hurts — they’re irreplaceable until you earn new ones
- Veterancy Dilution: If a campaign allows replenishing depleted veteran vehicle squads or infantry platoons between missions using Requisition, the influx of green recruits proportionally dilutes the unit’s overall veterancy level. Pure preservation is rewarded; brute-force replacement degrades elite status.
- Veterancy grants stat bonuses (configurable in YAML rules, per balance preset)
Equipment persistence (OFP: Resistance model):
- Captured enemy vehicles at mission end go into the equipment pool
- Found supply crates add to available equipment
- Next mission’s starting loadout can draw from the equipment pool
- Modders can define custom persistent items
Campaign State
#![allow(unused)]
fn main() {
/// Full campaign progress — serializable for save games.
#[derive(Serialize, Deserialize, Clone)]
pub struct CampaignState {
pub campaign_id: CampaignId,
pub active_mission: Option<MissionId>, // mission currently being briefed / played / debriefed; None while resting on the War Table
pub current_focus: CampaignFocusState,
pub completed_missions: Vec<CompletedMission>,
pub unit_roster: Vec<RosterUnit>,
pub equipment_pool: Vec<EquipmentId>,
pub hero_profiles: HashMap<String, HeroProfileState>, // optional built-in hero progression state (keyed by character_id)
pub resources: i64, // persistent credits (if enabled)
pub flags: HashMap<String, Value>, // story flags set by Lua
pub stats: CampaignStats, // cumulative performance
pub path_taken: Vec<MissionId>, // breadcrumb trail for replay/debrief
pub strategic_layer: Option<StrategicLayerState>, // first-class War Table state for campaigns that use phases / operations / initiatives
pub world_map: Option<WorldMapState>, // explicit strategic front / territory presentation state when a campaign persists one
}
#[derive(Serialize, Deserialize, Clone)]
pub enum CampaignFocusState {
StrategicLayer,
Intermission,
Briefing,
Mission,
Debrief,
}
/// Accepted D021 extension for phase-based campaigns.
/// Graph-only campaigns leave this `None`.
#[derive(Serialize, Deserialize, Clone)]
pub struct StrategicLayerState {
pub current_phase: Option<CampaignPhaseState>,
pub completed_phases: Vec<String>,
pub war_momentum: i32, // signed campaign pressure / advantage meter
pub doomsday_clock: Option<u16>, // minutes to midnight (optional urgency mechanic)
pub command_authority: u8, // 0-100 gauge gating operation slots
pub requisition: u32, // War funds for operations/base upgrades
pub intel: u32, // Information currency for reveals/bonuses
pub operations: Vec<CampaignOperationState>,
pub active_enemy_initiatives: Vec<EnemyInitiativeState>,
pub asset_ledger: CampaignAssetLedgerState,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CampaignPhaseState {
pub phase_id: String,
pub main_mission_urgent: bool,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CampaignOperationState {
pub mission_id: MissionId,
pub source: OperationSource,
pub status: OperationStatus,
pub expires_after_phase: Option<String>,
pub generated_instance: Option<GeneratedOperationState>,
pub generation_fallback: Option<GenerationFallbackMode>,
}
pub enum OperationSource { Authored, Generated }
pub enum OperationStatus {
Revealed,
Available,
Completed,
Failed,
Skipped,
Expired,
}
/// Persisted payload for generated operations so save/load, replay, and
/// takeover all point at the exact same authored assembly result.
#[derive(Serialize, Deserialize, Clone)]
pub struct GeneratedOperationState {
pub profile_id: String,
pub seed: u64,
pub site_kit: String,
pub security_tier: u8,
pub resolved_modules: Vec<ResolvedModulePick>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ResolvedModulePick {
pub slot: String, // objective / ingress / egress / complication / extra authored socket
pub module_id: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum GenerationFallbackMode {
AuthoredBackup { mission_id: MissionId },
ResolveAsSkipped,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct EnemyInitiativeState {
pub initiative_id: String,
pub status: EnemyInitiativeStatus,
pub ticks_remaining: u32,
pub counter_operation: Option<MissionId>,
}
pub enum EnemyInitiativeStatus {
Revealed,
Countered,
Activated,
Expired,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct CampaignAssetLedgerState {
pub entries: Vec<CampaignAssetLedgerEntry>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CampaignAssetLedgerEntry {
pub asset_id: String,
pub owner: AssetOwner,
pub state: AssetState,
pub quantity: u32,
pub quality: Option<String>, // campaign-authored quality tag; see §Recommended Quality Vocabulary below
pub unlocked_in_phase: Option<String>,
pub consumed_by: Vec<MissionId>,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum AssetOwner {
Player,
Enemy,
Neutral,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum AssetState {
Acquired,
Partial,
Denied,
}
### Strategic-Layer Pattern: Programs, Support Actors, and Endurance Anchors
The D021 asset ledger is not limited to prototype units. First-party campaigns should also use it for:
- **enemy or player capability programs** — nuclear tracks, Chronosphere progress, Iron Curtain deployment, heavy-armor lines, radar networks, elite training cadres, chemical stockpiles, weather projects
- **committed third-party support actors** — resistance cells, militias, defectors, naval contacts, exile air wings, black-market engineers, rear-area collaborators
- **endurance anchors** — held-open supply corridors, damaged depots, secured FOBs, sabotaged rail lines, threatened evacuation windows
The key question is not "is this a unit?" but "is this a persistent campaign-scale capability or liability that later missions consume?"
**Recommended representation pattern:**
- Use **`owner`** for who currently benefits: `Player`, `Enemy`, or `Neutral`
- Use **`state`** for broad status: `Acquired`, `Partial`, or `Denied`
- Use **`quality`** for the exact authored flavor: `unstable`, `delayed`, `coerced`, `staging_rights`, `urban_harassment`, `held_open`, `exhausted`, `early`, `on_schedule`
- Use **`consumed_by`** to name the missions or acts that should react to it explicitly
```yaml
strategic_layer:
asset_ledger:
entries:
- asset_id: soviet_nuclear_program
owner: Enemy
state: Partial
quantity: 1
quality: unstable
consumed_by: [allied_12, allied_14]
- asset_id: soviet_heavy_armor_line
owner: Enemy
state: Partial
quantity: 1
quality: reduced_output
consumed_by: [allied_09, allied_12]
- asset_id: soviet_mig_wing
owner: Enemy
state: Acquired
quantity: 1
quality: early
unlocked_in_phase: phase_4
consumed_by: [allied_08, allied_11]
- asset_id: allied_satellite_recon
owner: Player
state: Partial
quantity: 1
quality: delayed
unlocked_in_phase: phase_6
consumed_by: [allied_11, allied_14]
- asset_id: greek_resistance
owner: Player
state: Acquired
quantity: 1
quality: staging_rights
consumed_by: [allied_08, allied_11]
- asset_id: belgrade_militia
owner: Enemy
state: Partial
quantity: 1
quality: coerced_guides
consumed_by: [allied_10]
- asset_id: danube_supply_corridor
owner: Player
state: Partial
quantity: 1
quality: held_open
consumed_by: [allied_11]
}
Third-party actor rule: use Neutral while an actor is uncommitted or wavering, then flip the owner when the actor becomes a concrete support or hostility source. If a campaign wants more narrative nuance before commitment, CampaignState.flags or WorldMapState.narrative_state can track intermediate values such as wavering, under_threat, or abandoned.
Endurance rule: endurance is usually authored as a combination of mission-local timers plus campaign-carried logistics state. A “held-open corridor” asset can add setup time, reinforce a base, or delay culmination; a sabotaged fuel reserve can shorten an enemy offensive window or suppress reinforcement waves.
Capability-war rule: the same ledger should also carry mid-tier capability deltas, not just grand projects. A reduced-output tank line, a denied winterization package, a disrupted radar net, or an unfinished elite-training cadre may matter more across a campaign than a single flashy superweapon branch.
Timing-window rule: capability timing should be handled as an authored window, not a freeform simulation. The classic path defines the baseline arrival. Optional operations and enemy initiatives may move selected capabilities to early, on_schedule, or delayed, typically by one phase or 1-2 missions. Consumer missions should then select authored early/on-time/late variants rather than trying to improvise balance at runtime.
Recommended Quality Vocabulary
The quality field on CampaignAssetLedgerEntry is a free-form authored string. First-party campaigns should draw from these categories:
| Category | Tokens | Typical Use |
|---|---|---|
| Condition | full, damaged, unstable, reduced_output, reduced_range, unreliable | Physical state of a prototype, program, or facility |
| Timing | early, on_schedule, delayed, accelerated | Capability arrival window relative to baseline |
| Actor relationship | staging_rights, coerced, coerced_guides, allied_guides, sanctuary, defected, wavering | Third-party actor alignment / contribution |
| Logistics | held_open, exhausted, interdicted, resupplied, critical | Supply corridor, depot, or sustainment state |
| Intelligence | exposed, compromised, intact, decrypted, corrupted | Status of intel, cover, or counter-intelligence |
This is not an enum — campaigns and mods may define any string. The table exists so first-party content uses consistent vocabulary and consumer missions can pattern-match reliably.
Enemy Counter-Intelligence Escalation
Successful SpecOps should not be risk-free. If the player runs multiple covert operations successfully, the enemy should adapt — not through emergent AI, but through authored campaign-state escalation.
Recommended escalation ladder:
- Baseline — standard patrol density, normal detection thresholds, no counter-ops
- Heightened security — tighter patrols, shorter stealth windows, additional detection triggers in authored mission variants
- Active counter-ops — enemy launches their own initiative: mole hunts, bait operations, compromised safehouses, trap intel
- Hardened posture — enemy restructures deployments, moves high-value targets, or accelerates programs to reduce exposure windows
Implementation: track escalation as a campaign flag or asset-ledger entry (e.g., enemy_opsec_level: heightened). Consumer missions read this state and select authored variants: tighter patrol routes, shorter extraction windows, an extra detection phase, or a counter-operative ambush event. The escalation resets partially when the player fails or skips SpecOps, or when a specific counter-intelligence operation neutralizes the enemy’s awareness.
Design intent: this prevents SpecOps from being pure upside. The player should weigh “run another raid now for the asset” against “the enemy is getting wise — save my team for the really valuable op.” This pairs naturally with the operational tempo mechanic (see below) and the hero availability model.
# Counter-intelligence escalation in the asset ledger
strategic_layer:
asset_ledger:
entries:
- asset_id: enemy_opsec_posture
owner: Enemy
state: Acquired
quantity: 1
quality: heightened # baseline → heightened → active_counterops → hardened
consumed_by: [allied_08, allied_10, allied_12]
-- Mission reads enemy OPSEC level and adjusts detection parameters
local opsec = Campaign.get_asset_state("enemy_opsec_posture")
if opsec and opsec.quality == "active_counterops" then
-- Tighter patrol routes, shorter stealth windows
Ai.modify("enemy_patrols", { detection_radius_pct = 130, patrol_density = "high" })
-- Counter-op ambush event at extraction point
Trigger.enable("counterop_ambush_at_extraction")
UI.show_notification("Intel: Enemy counter-intelligence is active. Expect resistance at the extraction point.")
end
/// Explicit strategic front / territory state for campaigns that persist one.
/// This is presentation / territory state, not a replacement for
/// StrategicLayerState.
#[derive(Serialize, Deserialize, Clone)]
pub struct WorldMapState {
pub map_id: String, // which world map asset is active
pub mission_count: u32, // how many missions played so far
pub regions: HashMap<String, RegionState>,
pub narrative_state: HashMap<String, Value>, // LLM narrative flags (alliances, story arcs, etc.)
}
#[derive(Serialize, Deserialize, Clone)]
pub struct RegionState {
pub controlling_faction: String, // faction id or “contested”/“neutral”
pub stability: i32, // 0-100; low = vulnerable to revolt/counter-attack
pub garrison_strength: i32, // abstract force level
pub garrison_units: Vec
pub struct CompletedMission { pub mission_id: MissionId, pub outcome: String, // the named outcome key pub time_taken: Duration, pub units_lost: u32, pub units_gained: u32, pub score: i64, }
/// Cumulative campaign performance counters (local, save-authoritative). #[derive(Serialize, Deserialize, Clone, Default)] pub struct CampaignStats { pub missions_started: u32, pub missions_completed: u32, pub mission_retries: u32, pub mission_failures: u32, pub total_time_s: u64, pub units_lost_total: u32, pub units_gained_total: u32, pub credits_earned_total: i64, // optional; 0 when module/campaign does not track this pub credits_spent_total: i64, // optional; 0 when module/campaign does not track this }
/// Derived UI-facing progress summary for branching campaigns.
/// This is computed from the campaign graph + save state, not authored directly.
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct CampaignProgressSummary {
pub total_missions_in_graph: u32,
pub unique_missions_completed: u32,
pub discovered_missions: u32, // nodes revealed/encountered by this player/run history
pub current_path_depth: u32, // current run breadcrumb depth
pub best_path_depth: u32, // farthest mission depth reached across local history
pub endings_unlocked: u32,
pub total_endings_in_graph: Option
/// Scope key for community comparisons (optional, opt-in, D052/D053). /// Campaign progress comparisons must normalize on these fields. #[derive(Serialize, Deserialize, Clone)] pub struct CampaignComparisonScope { pub campaign_id: CampaignId, pub campaign_content_version: String, // manifest/version/hash-derived label pub game_module: String, pub difficulty: String, pub balance_preset: String, pub used_campaign_defaults: bool, // true if player kept the campaign’s default_settings pub settings_fingerprint: [u8; 32], // SHA-256 of resolved settings (for exact comparison grouping) }
/// Persistent progression state for a named hero character (optional toolkit).
#[derive(Serialize, Deserialize, Clone)]
pub struct HeroProfileState {
pub character_id: String, // links to D038 Named Character id
pub level: u16,
pub xp: u32,
pub unspent_skill_points: u16,
pub unlocked_skills: Vec
`CampaignState.flags` remains the general authoring escape hatch for narrative and mission-specific state. Campaigns that adopt the War Table should not hide canonical focus, phase, generated-operation, initiative, or asset-ledger data inside arbitrary flag keys. Those live in structured campaign state so save/load, UI, validation, and replay metadata can reason about them directly.
### Campaign Progress Metadata & GUI Semantics (Branching-Safe, Spoiler-Safe)
The campaign UI should display **progress metadata** (mission counts, completion %, farthest progress, time played), but D021 campaigns are branching graphs — not a simple linear list. To avoid confusing or misleading numbers, D021 defines these metrics explicitly:
- **`unique_missions_completed`**: count of distinct mission nodes completed across local history (best "completion %" metric for branching campaigns)
- **`current_path_depth`**: depth of the active run's current path (useful for "where am I now?")
- **`best_path_depth`**: farthest path depth the player has reached in local history (all-time "farthest reached" metric)
- **`endings_unlocked`**: ending/outcome coverage for replayability (optional if the author marks endings hidden)
**UI guidance (campaign browser / graph / profile):**
- Show **raw counts + percentage** together (example: `5 / 14 missions`, `36%`) — percentages alone hide too much.
- Label branching-aware metrics explicitly (`Best Path Depth`, not just `Farthest Mission`) to avoid ambiguity.
- For classic linear campaigns, `best_path_depth` and `unique completion` are numerically similar; UI may simplify wording.
**Spoiler safety (default):**
- Campaign browser cards should avoid revealing locked mission names.
- Community branch statistics should not reveal branch names or outcome labels until the player reaches that branch point.
- Use generic labels for locked content in comparisons (e.g., `Alternate Branch`, `Hidden Ending`) unless the campaign author opts into full reveal.
**Community comparisons (optional, D052/D053):**
- Local campaign progress is always available offline from `CampaignState` and local SQLite history.
- Community comparisons (percentiles, average completion, popular branch rates) are **opt-in** and must be scoped by `CampaignComparisonScope` (campaign version, module, difficulty, balance preset).
- Community comparison data is informational and social-facing, not competitive/ranked authority.
Campaign state is fully serializable (D010 — snapshottable sim state). Save games capture the entire campaign progress. Replays can replay an entire campaign run, not just individual missions.
### Named Character Presentation Overrides (Optional Convenience Layer)
To make a unit clearly read as a **unique character** (hero/operative/VIP) without forcing a full gameplay-unit fork for every case, D021 supports an optional **presentation override layer** for named characters. This is a **creator convenience** that composes with D038 Named Characters + the Hero Toolkit.
**Intended use cases:**
- unique voice set for a named commando while keeping the same base infantry gameplay role
- alternate portrait/icon/marker for a story-critical engineer/spy
- mission-scoped disguise/winter-gear variants for the same `character_id`
- subtle palette/tint/selection badge differences so a unique actor is readable in battle
**Scope boundary (important):**
- **Presentation overrides are not gameplay rules.** Weapons, armor, speed, abilities, and other gameplay-changing differences still belong in the unit definition and/or hero toolkit progression.
- If the campaign intentionally changes the character's gameplay profile, it should do so explicitly via the unit type binding / hero loadout, not by hiding it inside presentation metadata.
- Presentation overrides are local/content metadata and should not be treated as multiplayer/ranked compatibility changes by themselves (asset pack requirements still apply through normal package/resource dependency rules).
**Canonical schema (shared by D021 runtime data and D038 authoring UI):**
```rust
/// Optional presentation-only overrides for a named character.
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct CharacterPresentationOverrides {
pub portrait_override: Option<String>, // dialogue / hero sheet portrait asset id
pub unit_icon_override: Option<String>, // roster/sidebar/build icon when shown
pub voice_set_override: Option<String>, // select/move/attack/deny voice set id
pub sprite_variant: Option<String>, // alternate sprite/sequences mapping id
pub sprite_sequence_override: Option<String>,// sequence remap/alias (module-defined)
pub palette_variant: Option<String>, // palette/tint preset id
pub selection_badge: Option<String>, // world-space selection marker/badge id
pub minimap_marker_variant: Option<String>, // minimap glyph/marker variant id
}
/// Campaign-authored defaults + named variants for one character.
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct NamedCharacterPresentationConfig {
pub default_overrides: CharacterPresentationOverrides,
pub variants: HashMap<String, CharacterPresentationOverrides>, // e.g. disguise, winter_ops
}
YAML shape (conceptual, exact field names may mirror D038 UI labels):
named_characters:
- id: tanya
name: "Tanya"
unit_type: tanya_commando
portrait: portraits/tanya_default
presentation:
default:
voice_set: voices/tanya_black_ops
unit_icon: icons/tanya_black_ops
palette_variant: hero_red_trim
selection_badge: hero_star
minimap_marker_variant: specops_hero
variants:
disguise:
sprite_variant: tanya_officer_disguise
unit_icon: icons/tanya_officer_disguise
voice_set: voices/tanya_whisper
selection_badge: covert_marker
winter_ops:
sprite_variant: tanya_winter_gear
palette_variant: winter_white_trim
Layering model:
- campaign-level named character definition may provide
presentation.defaultandpresentation.variants - scenario bindings choose which variant to apply when spawning that character (for example
default,disguise,winter_ops) - D038 exposes this as a previewable authoring panel and a mission-level
Apply Character Presentation Variantconvenience action
Hero Campaign Toolkit (Optional, Built-In)
Warcraft III-style hero campaigns (for example, Tanya gaining XP, levels, unlockable abilities, and persistent equipment) fit D021 directly and should be possible without engine modding (no WASM module required). This is an optional campaign authoring layer on top of the existing D021 persistent state model and D038’s Named Characters / Inventory / Intermission tooling.
Design intent:
- No engine modding for common hero campaigns. Designers should build hero campaigns through YAML + the SDK Campaign Editor.
- Optional, not global. Classic RA-style campaigns remain simple; hero progression is enabled per campaign.
- Lua is the escape hatch. Use Lua for bespoke talent effects, unusual status systems, or custom UI logic beyond the built-in toolkit.
Built-in hero toolkit capabilities (recommended baseline):
- Persistent hero XP, level, and skill points across missions
- Skill unlocks and mission rewards via debrief/intermission flow
- Hero death/injury policies per character (
must survive,wounded,campaign_continue) - Hero availability states (
ready,fatigued,wounded,captured,lost) with authored recovery windows - Hero-specific flags/stats for branching dialogue and mission conditions
- Hero loadout/equipment assignment using the standard campaign inventory system
- Optional support-operative roster definitions for non-hero SpecOps teams
- Optional mission risk tiers (
routine,high_risk,commit) for spotlight operations
Example YAML (campaign-level hero progression config):
campaign:
id: tanya_black_ops
title: "Tanya: Black Ops"
persistent_state:
unit_roster: true
equipment: true
hero_progression: true
hero_toolkit:
enabled: true
xp_curve:
levels:
- level: 1
total_xp: 0
skill_points: 0
- level: 2
total_xp: 120
skill_points: 1
- level: 3
total_xp: 300
skill_points: 1
- level: 4
total_xp: 600
skill_points: 1
heroes:
- character_id: tanya
start_level: 1
skill_tree: tanya_commando
death_policy: wounded # must_survive | wounded | campaign_continue
stat_defaults:
agility: 3
stealth: 2
demolitions: 4
mission_rewards:
default_objective_xp: 50
bonus_objective_xp: 100
Concrete example: Tanya commando skill tree (campaign-authored, no engine modding):
campaign:
id: tanya_black_ops
hero_toolkit:
enabled: true
skill_trees:
tanya_commando:
display_name: "Tanya - Black Ops Progression"
branches:
- id: commando
display_name: "Commando"
color: "#C84A3A"
- id: stealth
display_name: "Stealth"
color: "#3E7C6D"
- id: demolitions
display_name: "Demolitions"
color: "#B88A2E"
skills:
- id: dual_pistols_drill
branch: commando
tier: 1
cost: 1
display_name: "Dual Pistols Drill"
description: "+10% infantry damage; faster target reacquire"
unlock_effects:
stat_modifiers:
infantry_damage_pct: 10
target_reacquire_ticks: -4
- id: raid_momentum
branch: commando
tier: 2
cost: 1
requires:
- dual_pistols_drill
display_name: "Raid Momentum"
description: "Gain temporary move speed after destroying a structure"
unlock_effects:
grants_ability: raid_momentum_buff
- id: silent_step
branch: stealth
tier: 1
cost: 1
display_name: "Silent Step"
description: "Reduced enemy detection radius while not firing"
unlock_effects:
stat_modifiers:
enemy_detection_radius_pct: -20
- id: infiltrator_clearance
branch: stealth
tier: 2
cost: 1
requires:
- silent_step
display_name: "Infiltrator Clearance"
description: "Unlocks additional infiltration dialogue/mission branches"
unlock_effects:
set_hero_flag:
key: tanya_infiltration_clearance
value: true
- id: satchel_charge_mk2
branch: demolitions
tier: 1
cost: 1
display_name: "Satchel Charge Mk II"
description: "Stronger satchel charge with larger structure damage radius"
unlock_effects:
upgrades_ability:
ability_id: satchel_charge
variant: mk2
- id: chain_detonation
branch: demolitions
tier: 3
cost: 2
requires:
- satchel_charge_mk2
- raid_momentum
display_name: "Chain Detonation"
description: "Destroyed explosive objectives can trigger nearby explosives"
unlock_effects:
grants_ability: chain_detonation
heroes:
- character_id: tanya
skill_tree: tanya_commando
start_level: 1
start_skills:
- dual_pistols_drill
death_policy: wounded
loadout_slots:
ability: 3
gear: 2
# Status-ladder transitions: how hero state changes trigger campaign branches
status_transitions:
wounded:
unavailable_missions: 2 # sits out 2 missions, then recovers to ready
recovery_operation: tanya_field_hospital # optional: a side op can speed recovery
captured:
triggers_branch: tanya_rescue # rescue mission becomes available
compromise_per_mission: 1 # each mission delay leaks intel (see hero capture escalation)
max_compromise_before_lost: 4 # after 4 missions, hero is permanently lost
lost:
terminal: true # hero removed from roster for this campaign run
narrative_flag: tanya_lost # set for briefing/ending variant selection
mission_rewards:
by_mission:
black_ops_03_aa_sabotage:
objective_xp:
destroy_aa_sites: 150
rescue_spy: 100
completion_choices:
- id: field_upgrade
label: "Field Upgrade"
grant_skill_choice_from:
- silent_step
- satchel_charge_mk2
- id: requisition_cache
label: "Requisition Cache"
grant_items:
- id: remote_detonator_pack
qty: 1
- id: intel_keycard
qty: 1
Recommended Hero Availability Model (No Arcade Lives)
IC should not treat heroes as having abstract extra lives in the Mario sense. The better fit for D021 is a status ladder that preserves stakes while keeping the campaign playable:
ready -> fatigued -> wounded -> captured -> lost
Recommended interpretation:
- Ready — hero can be deployed normally
- Fatigued — hero is overextended; still available for low-stakes use if the author allows it, but should usually sit out spotlight SpecOps
- Wounded — hero survives but is unavailable for a bounded number of missions or until a recovery operation completes
- Captured — hero is removed from the roster and becomes the center of a rescue / exploitation branch
- Lost — terminal state for this campaign run (death, irreversible disappearance, or political removal)
Default first-party stance: flagship heroes such as Tanya and Volkov should usually hit wounded or captured before reaching lost on Standard difficulty. Permanent loss should be rare, visible, and either authored as a high-stakes consequence or enabled by Ironman / harsher modes.
This is the intended thematic split for first-party content: the marquee heroes are the people who are “not supposed to get killed” in ordinary optional operations. Elite SpecOps teams are not afforded the same protection.
Diegetic resilience, not extra lives: if authors want to cushion risk, do it through campaign fiction:
- medevac coverage
- resistance safehouses
- extraction APCs
- field surgery teams
- armored retrieval teams
- bribed prison contacts
These can convert a fatal outcome into wounded or captured once, which serves the same balancing purpose as a “life” without feeling gamey.
Support Operatives: Hero-Adjacent, Not Hero-Equivalent
Campaigns that lean on SpecOps should not depend on a single superstar always being available. D021 should support elite non-hero operatives: Allied black-ops assault teams, Spetsnaz raiders, combat engineers, recon detachments, spy pairs, or partisan specialists.
These units sit between a normal roster squad and a marquee hero:
- weaker than Tanya / Volkov individually
- expected to operate in teams rather than solo
- can handle routine and some high-risk optional operations
- usually have veterancy and equipment persistence, but not a full hero skill tree
- gain better success odds, stealth windows, or objective quality when accompanied by a flagship hero
- can be killed permanently in normal campaign play without triggering a hero-style capture/rescue branch
- should be replaceable after losses, but replacement should cost time, resources, and often some veterancy/equipment quality
- should usually read visually as ordinary infantry with only subtle elite markers such as a special rank pip, badge, portrait frame, or selection marker
- should not normally be available as mass-producible barracks units in the first-party campaign
Recommended dispatch tiers for optional operations:
hero_required— only for rare signature missions or rescue branches built around that characterhero_preferred— elite team can attempt it, but dispatching the hero improves odds, reward quality, timing window, or extraction safetyteam_viable— an elite special-operations detachment is sufficient; hero dispatch is optional bonus valuecommander_variant— hero unavailable? The mission can still be tackled as a loud commander-supported or commander-only alternative
hero_toolkit:
heroes:
- character_id: tanya
role: flagship_hero
death_policy: wounded
availability:
fatigue_after_optional_specops: 1
wounded_unavailable_missions: 2
support_operatives:
- unit_type: allied_special_ops_team
display_name: "Allied Black Ops Team"
role: elite_specops
death_policy: normal
replacement_delay_missions: 1
veterancy_loss_on_rebuild: 1
not_buildable_in_standard_production: true
preferred_team_size: 4 # Allies favor smaller, faster insertion teams
presentation_profile:
base_silhouette: rifle_infantry
elite_marker: veteran_rank_badge
ui_reveal_strength: subtle
acquisition:
source: veteran_promotion
minimum_veterancy: elite
consumes_line_roster_unit: true
requires_asset: allied_specops_training_cadre
training_delay_missions: 1
can_lead_routine_specops: true
can_attempt_high_risk_specops: true
insertion_profiles:
- foot_infiltration
- paradrop
- helicopter_lift
- truck_cover
- civilian_cover_if_authored
signature_capabilities:
- demolition_charges
- timed_bombs
- covert_breach_tools
hero_bonus_if_attached:
reward_quality: +1
extraction_safety: +1
- unit_type: soviet_spetsnaz_team
display_name: "Soviet Spetsnaz Team"
role: elite_specops
death_policy: normal
replacement_delay_missions: 1
veterancy_loss_on_rebuild: 1
not_buildable_in_standard_production: true
preferred_team_size: 5 # Soviets favor heavier assault squads — intentional faction asymmetry
presentation_profile:
base_silhouette: rifle_infantry
elite_marker: spetsnaz_rank_flash
ui_reveal_strength: subtle
acquisition:
source: veteran_promotion
minimum_veterancy: elite
consumes_line_roster_unit: true
requires_asset: soviet_specops_training_cadre
training_delay_missions: 1
can_lead_routine_specops: true
can_attempt_high_risk_specops: true
insertion_profiles:
- foot_infiltration
- helicopter_lift
- paradrop
- truck_cover
- civilian_cover_if_authored
signature_capabilities:
- demolition_charges
- sabotage_kit
- covert_breach_tools
hero_bonus_if_attached:
stealth_window_seconds: 45
objective_speed_pct: 15
missions:
steel_archive_raid:
role: SPECOPS
dispatch_tier: hero_preferred
if_hero_absent:
fallback_variant: team_viable
Recommended first-party pattern: keep a small bench of 2-4 elite detachments per faction/theater so the player can keep doing SpecOps even when Tanya or Volkov is recovering, captured, or being saved for a more valuable operation. Those detachments may suffer normal fatalities and even be wiped out; the campaign replaces them through training/recruitment delays rather than hero-style rescue logic.
Recommended acquisition / promotion rule:
- elite SpecOps teams should usually enter the roster through promotion, rescue, allied grant, or theater transfer, not through ordinary unit production
- a surviving infantry squad that reaches the required veterancy can become a promotion candidate between missions
- promoting that squad should consume it from the general line roster and convert it into a smaller, scarcer elite detachment with better insertion/loadout options
- if the promoted team dies, the player loses the accumulated investment and must wait for a new candidate or replacement pipeline
- first-party content should present this as “we selected the best survivors for special duty”, not as buying commandos from a barracks queue
Recommended presentation / loadout rule:
- elite SpecOps should mostly look like slightly better-equipped line infantry rather than comic-book superheroes
- their special status should be communicated with subtle markers: rank insignia, colored shoulder flash, command pip, portrait frame, or small UI badge
- their distinction should come more from mission profile than silhouette: charges, covert entry, paradrop, helicopter lift, safehouse spawn, or civilian/truck-cover insertion where authored
- disguise or cover identities should remain scenario-authored capabilities, not universal always-on toggles for every match
Risk Tiers for Optional Operations
Optional missions do not all need the same commitment level. Some should be routine opportunistic raids; others should be explicitly risky, high-value spotlights.
Recommended risk tiers:
routine— standard optional mission; normal save/load expectationshigh_risk— stronger penalties or better rewards, but still follows normal campaign save policycommit— a high-risk, high-reward optional operation that autosaves on launch and locks the outcome even outside full-campaign Ironman
Use commit sparingly. It is not a replacement for global Ironman; it is a spotlight flag for the handful of optional operations where the whole fantasy is “you get one shot at this.”
Good candidates for commit:
- rescue windows that will permanently close
- one-chance prototype thefts
- deep-cover exfiltrations where discovery changes the whole theater
- deniable political operations with no do-over
- top-tier tech-denial raids that can permanently reshape the act
Frequency rule: first-party campaigns should usually cap commit missions at 1-2 per act and badge them clearly in the War Table UI before launch.
missions:
behind_enemy_lines:
role: SPECOPS
dispatch_tier: hero_required
risk_tier: commit
required_hero: tanya
spy_network:
role: SPECOPS
dispatch_tier: hero_preferred
risk_tier: high_risk
preferred_hero: tanya
fallback_variant: team_viable
nuclear_escalation:
role: SPECOPS
dispatch_tier: hero_preferred
risk_tier: commit
preferred_operatives:
- soviet_spetsnaz_team
- volkov
Interaction with expiring opportunities: A commit mission that is also an expiring opportunity stacks both constraints: the player has a limited window to launch it, and once launched, the outcome is locked. This combination is valid and dramatically powerful — but should be used at most once per campaign and displayed with both the time window and the COMMIT badge on the War Table card. The briefing must clearly state: “This operation will not wait, and there are no second chances.”
Why this fits the design: The engine core stays game-agnostic (hero progression is campaign/game-module content, not an engine-core assumption), and the feature composes cleanly with D021 branches, D038 intermissions, and D065 tutorial/onboarding flows.
Operational Tempo and Bench Fatigue
Running SpecOps back-to-back should have a cost beyond individual hero fatigue. The bench itself should wear down if the player over-relies on covert operations without spacing them out.
Recommended tempo model:
- Each SpecOps mission (regardless of success) adds a
tempo_pressureincrement to campaign state - Tempo pressure decays by 1 for each main operation completed without launching a SpecOps mission
- At low tempo (0-1): normal stealth windows, normal team availability, normal reward quality
- At moderate tempo (2-3): elite teams need longer rotation between ops; stealth windows in authored missions tighten slightly; replacement delays increase by 1 mission
- At high tempo (4+): enemy counter-intelligence escalates (see §Enemy Counter-Intelligence Escalation); extraction risk increases; team rebuild delays double
Implementation: tempo is a simple integer campaign flag. Consumer missions read it and select authored difficulty variants:
# Campaign-level tempo tracking
persistent_state:
custom_flags:
specops_tempo_pressure: 0
# Mission-level tempo reaction
missions:
chrono_convoy_intercept:
role: SPECOPS
risk_tier: high_risk
tempo_variants:
low: # tempo 0-1
stealth_window_ticks: 1800 # 90 seconds
extraction_difficulty: normal
moderate: # tempo 2-3
stealth_window_ticks: 1200 # 60 seconds
extraction_difficulty: contested
high: # tempo 4+
stealth_window_ticks: 600 # 30 seconds
extraction_difficulty: hot_pursuit
counterop_event: true
Design intent: this creates a natural pacing rhythm. The player who spaces out SpecOps between main operations keeps their bench fresh and the enemy unaware. The player who runs three raids in a row may succeed in all of them — but will find the fourth much harder, with worn-down teams facing an alert enemy. This makes the “save Tanya for later” decision real and strategic.
Post-Mission Debrief as Strategic Feedback
The campaign should close the feedback loop after every mission. The debrief screen is not just a score tally — it is the primary tool for teaching the player how the strategic layer works.
Recommended debrief structure:
- Mission outcome — what happened, which outcome ID was triggered
- Assets earned — what the player gained from this mission (roster survivors, captured equipment, flags set, operations revealed)
- Assets lost — what the player spent or failed to protect (hero injuries, team casualties, expired opportunities that closed during this mission)
- Strategic impact — how this mission’s outcome changes the War Table: new operations available, enemy initiatives advanced, capability timing shifted, asset-ledger entries updated
- Comparison hint (optional) — a single line indicating what the player could have gained from an alternative outcome, without spoiling specific branch content: “An operative approach to this mission would have yielded additional intel.”
Implementation: the debrief reads CampaignState diffs (state before mission vs. state after) and presents them as categorized line items. This is campaign-UI content (ic-ui), not sim logic.
-- Debrief data provided by the campaign system after mission completion
Events.on("mission_debrief", function(ctx)
-- ctx.earned contains new assets, flags, operations revealed
-- ctx.lost contains casualties, expired ops, compromise increments
-- ctx.strategic contains War Table deltas (new cards, shifted timings, ledger changes)
-- ctx.comparison_hint is an optional authored string from the mission YAML
UI.show_debrief(ctx)
end)
Design intent: players often do not understand how optional operations connect to later missions. The debrief screen makes those connections explicit: “Your sabotage of the radar calibration facility means enemy air coordination will be degraded in the next act.” This turns every mission into a teaching moment for the strategic layer.
Special Operations Mission Archetypes (Hero-in-Battle)
The hero toolkit above handles hero progression (XP, skills, loadouts). This section defines mission design patterns where the hero operates inside a live, dynamic battle — not on a separate scripted commando map.
The key difference from classic C&C commando missions: in traditional commando missions, Tanya infiltrates a static, scripted map alone. In these archetypes, the player controls a hero + squad embedded inside an ongoing AI-vs-AI battle. The larger battle is dynamic, affects the player, and the player affects it. Think Dota’s hero-in-a-war model applied to RTS campaigns.
Archetype: Embedded Task Force
The player controls a hero (Tanya) + a small squad (5-15 units) inside a live battle between AI factions. Tanks, aircraft, and infantry fight around the player. The player’s objectives are special-ops tasks (sabotage, rescue, assassination) that take place within and alongside the larger engagement.
mission:
id: allied_07_bridge_assault
type: embedded_task_force
# The live battle — two AI factions fight each other on the same map
battle_context:
allied_ai:
faction: allies
preset: aggressive # AI preset (D043)
relationship_to_player: allied # same team as player's squad
base: true # has a base, produces units
objective: "Push across the river and establish a beachhead"
soviet_ai:
faction: soviet
preset: defensive
relationship_to_player: enemy
base: true
objective: "Hold the river crossing at all costs"
# The player's task force — hero + squad, embedded in the battle
player:
hero: tanya # hero character (D021 hero toolkit)
squad_roster: extracted # squad comes from campaign roster (D021 carryover)
max_squad_size: 12
starting_position: behind_allied_lines
can_build: false # player has no base — task force only
can_call_reinforcements: true # request reinforcements from allied AI (see below)
# Special-ops objectives — the player's job within the battle
objectives:
primary:
- id: destroy_bridge_charges
description: "Reach the bridge and disarm Soviet demolition charges before they detonate"
location: river_bridge
time_limit_ticks: 6000 # 5 minutes — the Soviets will blow the bridge
secondary:
- id: rescue_engineer
description: "Free the captured Allied engineer from the Soviet camp"
location: soviet_prison_camp
reward:
roster_add: engineer # engineer joins your squad for future missions
- id: destroy_radar
description: "Take out the Soviet radar to blind their air defenses"
reward:
battle_effect: allied_air_support_unlocked # changes the live battle
# How the player interacts with the larger battle
reinforcement_system:
enabled: true
request_cooldown_ticks: 1200 # can request every 60 seconds
options:
- type: infantry_squad
description: "Request a rifle squad from the Allied front line"
cost: none # free but cooldown-limited
- type: apc_extraction
description: "Call an APC to extract wounded squad members"
cost: none
- type: artillery_strike
description: "Request a 10-second artillery barrage on a target area"
cost: secondary_objective # unlocked by completing 'destroy_radar'
requires_flag: allied_air_support_unlocked
# How the player's actions affect the larger battle
battle_effects:
- trigger: objective_complete("destroy_bridge_charges")
effect: "Allied AI gains a crossing point — sends armor across the bridge"
- trigger: objective_complete("destroy_radar")
effect: "Soviet AI loses radar — no longer calls in air strikes"
- trigger: hero_dies("tanya")
effect: "Allied AI morale drops — retreats to defensive positions"
- trigger: squad_losses_exceed(75%)
effect: "Allied AI sends reinforcements to player's position"
outcomes:
victory_bridge_saved:
condition: "bridge_charges_disarmed AND bridge_intact"
next: allied_08a
state_effects:
set_flag:
bridge_status: intact
tanya_reputation: hero
victory_bridge_lost:
condition: "bridge_charges_not_disarmed OR bridge_destroyed"
next: allied_08b
state_effects:
set_flag:
bridge_status: destroyed
tanya_reputation: survivor
defeat:
condition: "hero_dead AND no_reinforcements"
next: allied_07_fallback
Lua mission script for the live battle interaction:
-- allied_07_bridge_assault.lua
Events.on("mission_start", function()
-- Start the AI battle — both sides begin fighting immediately
Ai.activate("allied_ai")
Ai.activate("soviet_ai")
-- Player's squad spawns behind allied lines
local squad = Campaign.get_roster()
local tanya = Campaign.hero_get("tanya")
Trigger.spawn_roster_at("player_spawn_zone", squad)
-- The bridge has a 5-minute timer
Trigger.start_timer("bridge_demolition", 6000, function()
if not Var.get("bridge_charges_disarmed") then
Trigger.destroy_bridge("river_bridge")
UI.show_notification("The Soviets have destroyed the bridge!")
end
end)
end)
-- Player completes a secondary objective → changes the live battle
Events.on("objective_complete", function(ctx)
if ctx.id == "destroy_radar" then
-- Soviet AI loses air support capability
Ai.modify("soviet_ai", { disable_ability = "air_strike" })
-- Allied AI gains air support
Ai.modify("allied_ai", { enable_ability = "air_support" })
Campaign.set_flag("allied_air_support_unlocked", true)
UI.show_timeline_effect("Soviet radar destroyed. Allied air support now available!")
end
end)
-- Player is in trouble → allied AI reacts
Events.on("squad_losses", function(ctx)
if ctx.losses_percent > 50 then
-- Allied AI sends a relief force to the player's position
Ai.send_reinforcements("allied_ai", {
to = Player.get_hero_position(),
units = { "rifle_squad", "medic" },
priority = "urgent"
})
UI.show_notification("Command: 'Hold tight, reinforcements inbound!'")
end
end)
-- Player calls for reinforcements manually (via request wheel / D059 beacons)
Events.on("reinforcement_request", function(ctx)
if ctx.type == "artillery_strike" then
if Campaign.get_flag("allied_air_support_unlocked") then
Trigger.artillery_barrage(ctx.target_position, {
duration_ticks = 200,
radius = 5,
damage = 500
})
else
UI.show_notification("Command: 'Negative — enemy radar still active. Take it out first.'")
end
end
end)
-- Hero death affects the larger battle
Events.on("hero_killed", function(ctx)
if ctx.character_id == "tanya" then
Ai.modify("allied_ai", { morale = "low", posture = "defensive" })
UI.show_notification("Allied forces fall back after losing their commando.")
end
end)
Archetype: Air Campaign (Ground Support)
The player commands air assets and coordinates with ground forces (AI-controlled allied army + local resistance). The player doesn’t build — they manage sortie assignments, target designation, and extraction timing.
mission:
id: allied_12_air_superiority
type: air_campaign
player:
role: air_commander
assets:
- type: attack_helicopter
count: 4
- type: spy_plane
count: 1
- type: transport_chinook
count: 2
can_build: false
resupply_point: allied_airfield # units return here to rearm/repair
# Ground forces the player coordinates with but doesn't directly control
ground_allies:
- id: resistance_cells
faction: allies
ai_preset: guerrilla # hit-and-run, avoids direct engagement
controllable: false # player can't give them direct orders
requestable: true # player can designate targets for them
- id: allied_armor
faction: allies
ai_preset: cautious
controllable: false
requestable: true # player can call in armor support to a location
# Spy mechanics — player uses spy plane for recon
recon:
spy_plane_reveals: 30 # cells revealed per flyover
intel_persists: true # revealed areas stay revealed (shroud removal)
mark_targets: true # player can mark spotted targets for ground forces
objectives:
primary:
- id: destroy_sam_sites
description: "Neutralize all 4 SAM sites to secure air corridors"
- id: extract_spy
description: "Land transport at extraction point, load the spy, return to airfield"
secondary:
- id: support_resistance_raid
description: "Provide air cover for the resistance attack on the supply depot"
-- Air campaign Lua — player designates targets for AI ground forces
-- Player marks a target with spy plane recon
Events.on("target_designated", function(ctx)
-- Resistance cells receive the target and plan an attack
Ai.assign_target("resistance_cells", ctx.target_position, {
attack_type = "raid",
wait_for_air_cover = true -- they won't attack until player provides air support
})
UI.show_notification("Resistance: 'Target received. Awaiting your air cover.'")
end)
-- Player provides air cover → resistance attacks
Events.on("air_units_in_area", function(ctx)
if ctx.area == Ai.get_pending_target("resistance_cells") then
Ai.execute_attack("resistance_cells")
UI.show_notification("Resistance: 'Air cover confirmed. Moving in!'")
end
end)
Archetype: Spy Infiltration (Behind Enemy Lines)
The player controls a spy + small cell operating behind enemy lines. The larger battle happens on the “front” — the player operates in the enemy rear. The player’s sabotage actions weaken the enemy’s front-line forces.
mission:
id: soviet_05_behind_the_lines
type: spy_infiltration
player:
hero: spy
squad:
- spy
- saboteur
- sniper
can_build: false
detection_system:
enabled: true
alert_levels:
- undetected
- suspicious
- alerted
- hunted
# Player actions affect alert level:
# - killing guards quietly: no change
# - explosions: +1 level
# - spotted by patrol: +1 level
# - destroying radar: resets to 'undetected'
# The front-line battle (player can see its effects but operates separately)
front_line:
soviet_ai:
faction: soviet
objective: "Hold the front line against Allied assault"
allied_ai:
faction: allies
objective: "Break through Soviet defenses"
# Player's sabotage actions weaken the Soviet front:
sabotage_effects:
- action: "destroy_ammo_dump"
front_effect: "Soviet AI loses artillery support for 3 minutes"
- action: "cut_supply_line"
front_effect: "Soviet AI unit production slowed by 50% for 2 minutes"
- action: "hack_comms"
front_effect: "Soviet AI coordination disrupted — units fight independently"
outcomes:
victory_front_collapses:
condition: "3+ sabotage_effects_active AND allied_ai_breaks_through"
description: "Your sabotage broke the Soviet lines. The Allies pour through."
victory_extraction:
condition: "all_primary_objectives_complete AND squad_extracted"
description: "Mission complete. The intel will change the course of the war."
How These Archetypes Use Existing Systems
| System | Role in Hero-in-Battle Missions |
|---|---|
| D021 Campaign Graph | Missions are nodes in the branching graph. Outcomes (bridge saved/lost, hero alive/dead) create branches |
| D021 Hero Toolkit | Tanya gains XP from mission objectives. Skills unlock across the campaign arc |
| D021 Roster Carryover | Squad members survive between missions. Lost units stay lost |
| D043 AI Presets | Allied and enemy AI factions use preset behaviors (aggressive, defensive, guerrilla) |
| D070 Commander & SpecOps | In co-op, one player can be the Commander (controlling the allied AI’s base/production) while another is the SpecOps player (controlling Tanya’s squad). The request economy (reinforcements, air support) maps directly to D070’s coordination surfaces |
| D059 Beacons & Coordination | The reinforcement request wheel uses D059’s beacon/ping system. “Need CAS”, “Need Extraction” are D059 request types |
| Lua Scripting (Tier 2) | All battle interactions (AI modification, reinforcement triggers, objective effects) are Lua scripts. No WASM needed |
| D022 Dynamic Weather | Weather affects the battle (fog reduces air effectiveness, rain slows armor) |
No new engine systems required. These mission archetypes compose existing systems: AI presets run the battle, Lua scripts connect player actions to battle effects, the hero toolkit handles progression, and the campaign graph handles branching. The only new content is the mission YAML and Lua scripts — all authored by campaign designers, not engine developers.
Campaign Menu Scenes (Evolving Main Menu Background)
Campaign authors can define menu scenes that change the main menu background to reflect the player’s current campaign progress. When the player returns to the main menu during a campaign playthrough, the background shows a scene tied to where they are in the story — an evolving title screen pattern used by Half-Life 2, Halo: Reach, Warcraft III, and others (see player-flow/main-menu.md § Campaign-Progress Menu Background for prior art).
This is an authoring feature, not an engine system. The engine reads the menu_scenes table from the campaign YAML and matches the player’s CampaignState to select the active scene. No new engine code — just YAML configuration + scene assets.
# Campaign YAML — menu scene definitions
campaign:
id: allied_campaign
title: "Allied Campaign"
# Menu scenes — matched in order, first match wins
menu_scenes:
# Act 1: Early war — daylight naval scene
- match:
missions_completed_lt: 3 # before mission 3
scene:
type: video_loop # pre-rendered video
video: "media/menu/act1_naval_fleet.webm"
audio: "media/menu/act1_radio_chatter.opus" # ambient radio, plays under menu music
audio_volume: 0.3
# Act 2: Night operations — air campaign
- match:
missions_completed_gte: 3
missions_completed_lt: 7
flag:
current_theater: "air_campaign" # optional flag condition
scene:
type: video_loop
video: "media/menu/act2_night_flight.webm" # cockpit view, aircraft in formation
audio: "media/menu/act2_pilot_radio.opus" # radio chatter + engine hum
# Act 3: Ground assault — live shellmap scenario
- match:
missions_completed_gte: 7
missions_completed_lt: 11
scene:
type: shellmap # live in-game scene
map: "maps/menu_scenes/act3_beachhead.yaml"
script: "scripts/menu_scenes/act3_battle.lua" # lightweight AI battle script
duration_ticks: 6000 # loops after 5 minutes
# Act 3 variant: if the bridge was destroyed
- match:
missions_completed_gte: 7
flag:
bridge_status: destroyed
scene:
type: shellmap
map: "maps/menu_scenes/act3_ruins.yaml" # different scene — bombed-out bridge
script: "scripts/menu_scenes/act3_ruins_battle.lua"
# Final act: victory aftermath
- match:
flag:
campaign_complete: true
scene:
type: static_image
image: "media/menu/victory_sunrise.png"
audio: "media/menu/victory_theme.opus"
audio_volume: 0.5
# Default fallback (no match — e.g., campaign just started)
- match: {} # matches everything
scene:
type: shellmap
map: "maps/menu_scenes/default_skirmish.yaml"
Scene types:
| Type | What It Is | Best For |
|---|---|---|
shellmap | Live in-game scene — a small map with a Lua script running AI units. Uses the existing shellmap infrastructure. Renders behind the menu at reduced priority | Dynamic scenes: a base under siege, a patrol formation, a naval convoy. The player sees a living piece of the campaign world |
video_loop | A .webm video playing in a seamless loop. Optional ambient audio track (radio chatter, engine noise, wind) plays behind menu music at configurable volume | Cinematic scenes: cockpit view of night flight, war room briefing, satellite surveillance footage. Pre-rendered = consistent quality, no sim overhead |
static_image | A single image (artwork, screenshot, concept art) | Simple scenes: victory aftermath, chapter title cards, faction banners |
Match rules:
| Condition | Type | Description |
|---|---|---|
campaign_focus | string | CampaignState.current_focus matches strategic_layer, intermission, briefing, mission, or debrief |
missions_completed_lt | integer | Current CampaignState.completed_missions.len() < value |
missions_completed_gte | integer | Current CampaignState.completed_missions.len() >= value |
active_mission | string | CampaignState.active_mission matches this mission ID |
current_phase | string | CampaignState.strategic_layer.current_phase.phase_id matches this phase ID |
operation_status | map | A named operation matches a status such as available, completed, failed, skipped, or expired |
initiative_status | map | A named enemy initiative matches revealed, countered, activated, or expired |
asset_state | map | Asset-ledger entry matches a requested state / owner / quality / quantity tuple (for example chrono_tank: { owner: player, state: partial, quality: damaged, quantity_gte: 1 }) |
flag | map | All specified flags match their values in CampaignState.flags |
hero_level_gte | map | Hero character’s level >= value (e.g., { tanya: 3 }) |
{} (empty) | — | Matches everything — use as fallback |
Conditions are evaluated in order; the first matching entry wins. This allows both classic flag-based branching and structured strategic-layer branching: a bridge-destroyed variant can still key off flag: { bridge_status: destroyed }, while a late-war scene can key off current_phase, initiative_status, or a specific asset_state.
Workshop support: Community campaigns published via Workshop include their menu_scenes table and all referenced assets (videos, images, shellmap maps/scripts). The Workshop packaging system (D049) bundles scene assets as part of the campaign package. The SDK Campaign Editor (D038) provides a “Menu Scenes” panel for authoring and previewing scenes per campaign stage.
Player override: The player can always override the campaign scene by selecting a different background style in Settings → Video (static image, shellmap AI, personal highlights, community highlights). Campaign scenes are the default when authored by the campaign and when the player hasn’t chosen something else.
Lua Campaign API
Mission scripts interact with campaign state through a sandboxed API:
-- === Reading campaign state ===
-- Get the unit roster (surviving units from previous missions)
local roster = Campaign.get_roster()
for _, unit in ipairs(roster) do
-- Spawn each surviving unit at a designated entry point
local spawned = SpawnUnit(unit.type, entry_point)
spawned:set_veterancy(unit.veterancy)
spawned:set_name(unit.name)
end
-- Read story flags set by previous missions
if Campaign.get_flag("bridge_status") == "intact" then
-- Bridge exists on this map — open the crossing
bridge_actor:set_state("intact")
else
-- Bridge was destroyed — it's rubble
bridge_actor:set_state("destroyed")
end
-- Check cumulative stats
if Campaign.get_stat("total_units_lost") > 50 then
-- Player has been losing lots of units — offer reinforcements
trigger_reinforcements()
end
-- Read structured strategic-layer state
local phase = Campaign.get_phase()
local spy_network = Campaign.get_operation_state("ic_spy_network")
local lazarus = Campaign.get_initiative_state("project_lazarus")
local chrono_tank = Campaign.get_asset_state("chrono_tank")
local greek_resistance = Campaign.get_asset_state("greek_resistance")
if phase and phase.phase_id == "phase_6" then
UI.show_notification("Final-act operations are now live.")
end
if spy_network and spy_network.status == "available" then
UI.show_notification("Spy Network has been revealed on the War Table.")
end
if lazarus and lazarus.status == "revealed" then
UI.show_notification("Project Lazarus is advancing. Counter it soon.")
end
if chrono_tank and chrono_tank.state == "partial" then
UI.show_notification("Chrono Tank restored in damaged condition. Temporal shift unavailable.")
end
if greek_resistance and greek_resistance.owner == "player" and greek_resistance.quality == "staging_rights" then
UI.show_notification("Greek resistance has opened a forward landing zone for this operation.")
end
-- === Writing campaign state ===
-- Signal mission completion with a named outcome
function OnObjectiveComplete()
if bridge:is_alive() then
Campaign.complete("victory_bridge_intact")
else
Campaign.complete("victory_bridge_destroyed")
end
end
-- Set custom flags for future missions to read
Campaign.set_flag("captured_radar", true)
Campaign.set_flag("enemy_morale", "broken")
-- Reveal or unlock follow-up operations on the War Table
Campaign.reveal_operation("ic_spy_network")
Campaign.unlock_operation("chrono_convoy_intercept")
-- Update structured strategic-layer state without burying it in generic flags
Campaign.mark_initiative_countered("chemical_weapons_deployment")
Campaign.set_operation_status("ic_behind_enemy_lines", "completed")
Campaign.set_asset_state("chrono_tank", {
owner = "player",
state = "partial",
quantity = 1,
quality = "damaged",
consumed_by = { "allied_12", "allied_14" },
})
Campaign.set_asset_state("super_tanks", {
owner = "enemy",
state = "denied",
quantity = 0,
consumed_by = { "allied_14" },
})
Campaign.set_asset_state("soviet_nuclear_program", {
owner = "enemy",
state = "partial",
quantity = 1,
quality = "unstable",
consumed_by = { "allied_12", "allied_14" },
})
Campaign.set_asset_state("greek_resistance", {
owner = "player",
state = "acquired",
quantity = 1,
quality = "staging_rights",
consumed_by = { "allied_08", "allied_11" },
})
Campaign.set_asset_state("belgrade_militia", {
owner = "enemy",
state = "partial",
quantity = 1,
quality = "coerced_guides",
consumed_by = { "allied_10" },
})
Campaign.set_flag("enemy_endurance_penalty_seconds", 240)
-- Update roster: mark which units survived
-- (automatic if carryover mode is "surviving" — manual if "selected")
function OnMissionEnd()
local survivors = GetPlayerUnits():alive()
for _, unit in ipairs(survivors) do
Campaign.roster_add(unit)
end
end
-- Add captured equipment to persistent pool
function OnEnemyVehicleCaptured(vehicle)
Campaign.equipment_add(vehicle.type)
end
-- Failure doesn't mean game over — it's just another outcome
function OnPlayerBaseDestroyed()
Campaign.complete("defeat") -- campaign graph decides what happens next
end
Structured-state rule: Campaign.set_flag() remains correct for bespoke narrative markers (bridge_status, commander_recovered, scientist_betrayed). Use the strategic helpers for canonical War Table state (phase, operation status, initiative status, asset ledger) so authors do not have to reverse-engineer first-party systems from arbitrary flag names.
Hero progression helpers (optional built-in toolkit)
When hero_toolkit.enabled is true, the campaign API exposes built-in helpers for common hero-campaign flows. These are convenience functions over D021 campaign state; they do not require WASM or custom engine code.
-- Award XP to Tanya after destroying anti-air positions
Campaign.hero_add_xp("tanya", 150, { reason = "aa_sabotage" })
-- Check level gate before enabling a side objective/dialogue option
if Campaign.hero_get_level("tanya") >= 3 then
Campaign.set_flag("tanya_can_infiltrate_lab", true)
end
-- Grant a skill as a mission reward or intermission choice outcome
Campaign.hero_unlock_skill("tanya", "satchel_charge_mk2")
-- Modify hero-specific stats/flags for branching missions/dialogue
Campaign.hero_set_stat("tanya", "stealth", 4)
Campaign.hero_set_flag("tanya", "injured_last_mission", false)
-- Query persistent hero state (for UI or mission logic)
local tanya = Campaign.hero_get("tanya")
print(tanya.level, tanya.xp, tanya.unspent_skill_points)
Scope boundary: These helpers cover common hero-RPG campaign patterns (XP, levels, skills, hero flags, progression rewards). Bespoke systems (random loot affixes, complex proc trees, fully custom hero UIs) remain the domain of Lua (and optionally WASM for extreme cases).
Adaptive Difficulty via Campaign State
Campaign state enables dynamic difficulty without an explicit slider:
# In a mission's carryover config:
adaptive:
# If player lost the previous mission, give them extra resources
on_previous_defeat:
bonus_resources: 2000
bonus_units:
- medium_tank
- medium_tank
- rifle_infantry
- rifle_infantry
# If player blitzed the previous mission, make this one harder
on_previous_fast_victory: # completed in < 50% of par time
extra_enemy_waves: 1
enemy_veterancy_boost: 1
# Scale to cumulative performance
scaling:
low_roster: # < 5 surviving units
reinforcement_schedule: accelerated
high_roster: # > 20 surviving units
enemy_count_multiplier: 1.3
This is not AI-adaptive difficulty (that’s D016/ic-llm). This is designer-authored conditional logic expressed in YAML — the campaign reacts to the player’s cumulative performance without any LLM involvement.
Dynamic Mission Flow: Individual missions within a campaign can use map layers (dynamic expansion), sub-map transitions (building interiors), and phase briefings (mid-mission cutscenes) to create multi-phase missions with progressive reveals and infiltration sequences. Flags set during sub-map transitions (e.g.,
radar_destroyed,radar_captured) are written toCampaign.set_flag()and persist across missions — a spy’s infiltration outcome in mission 3 can affect the enemy’s capabilities in mission 5. See04-MODDING.md§ Dynamic Mission Flow for the full system design, Lua API, and worked examples.
D070 extension path (future “Ops Campaigns”): D070’s
Commander & Field Opsasymmetric co-op mode is v1 match-based by default (session-local field progression), but it composes with D021 later. A campaign can wrap D070-style missions and persist squad/hero state, requisition unlocks, and role-specific flags across missions using the sameCampaignStateandCampaign.set_flag()model defined here. This includes optional hero-style SpecOps leaders (e.g., Tanya-like or custom commandos) using the built-in hero toolkit for XP/skills/loadouts between matches/missions. This is an optional campaign layer, not a requirement for the base D070 mode.
Commander rescue bootstrap pattern (D021 + D070-adjacent Commander Avatar modes): A mini-campaign can intentionally start with command/building systems disabled because the commander is captured/missing. Mission 1 is a SpecOps rescue/infiltration scenario; on success, Lua sets a campaign flag such as
commander_recovered = true. Subsequent missions check this flag to enable commander-avatar presence mechanics, base construction/production menus, support powers, or broader unit command surfaces. This is a recommended way to teach layered mechanics while making the commander narratively and mechanically important.
D070 proving mini-campaign pattern (“Ops Prologue”): A short 3-4 mission mini-campaign is the preferred vertical slice for validating
Commander & SpecOps(D070) before promoting it as a polished built-in mode/template. Recommended structure:
- Rescue the Commander (SpecOps-only, infiltration/extraction, command/building restricted)
- Establish Forward Command (commander recovered, limited support/building unlocked)
- Joint Operation (full Commander + SpecOps strategic/field/joint objectives)
- (Optional) Counterstrike / Defense (enemy counter-ops pressure, commander-avatar survivability/readability test)
This pattern is valuable both as a player-facing mini-campaign and as an internal implementation/playtest harness because it validates D021 flags, D070 role flow, D059 request UX, and D065 onboarding in one narrative arc.
D070 pacing extension pattern (“Operational Momentum” / “one more phase”): An
Ops Campaigncan preserve D070’s optional Operational Momentum pacing across missions by storing lane progress and war-effort outcomes as campaign state/flags (for exampleintel_chain_progress,command_network_tier,superweapon_delays_applied,forward_lz_unlocked). The next mission can then react with support availability changes, route options, enemy readiness, or objective variants. UI should present these as branching-safe, spoiler-safe progress summaries (current gains + next likely payoff), not as a giant opaque meta-score.
Tutorial Campaigns — Progressive Element Introduction (D065)
The campaign system supports tutorial campaigns — campaigns designed to teach game mechanics (or mod mechanics) one at a time. Tutorial campaigns use everything above (branching graphs, state persistence, adaptive difficulty) plus the Tutorial Lua global (D065) to restrict and reveal gameplay elements progressively.
This pattern works for the built-in Commander School and for modder-created tutorial campaigns. A modder introducing custom units, buildings, or mechanics in a total conversion can use the same infrastructure.
End-to-End Example: “Scorched Earth” Mod Tutorial
A modder has created a “Scorched Earth” mod that adds a flamethrower infantry unit, an incendiary airstrike superweapon, and a fire-spreading terrain mechanic. They want a 4-mission tutorial that introduces each new element before the player encounters it in the main campaign.
Campaign definition:
# mods/scorched-earth/campaigns/tutorial/campaign.yaml
campaign:
id: scorched_tutorial
title: "Scorched Earth — Field Training"
description: "Learn the fire mechanics before you burn everything down"
start_mission: se_01
category: tutorial # appears under Campaign → Tutorial
requires_mod: scorched-earth
icon: scorched_tutorial_icon
persistent_state:
unit_roster: false # no carryover for tutorial missions
custom_flags:
mechanics_learned: [] # explicit empty list is intentional here
missions:
se_01:
map: missions/scorched-tutorial/01-meet-the-pyro
briefing: briefings/scorched/01.yaml
outcomes:
pass:
next: se_02
state_effects:
append_flag:
mechanics_learned:
- flamethrower
- fire_spread
skip:
next: se_02
state_effects:
append_flag:
mechanics_learned:
- flamethrower
- fire_spread
se_02:
map: missions/scorched-tutorial/02-controlled-burn
briefing: briefings/scorched/02.yaml
outcomes:
pass:
next: se_03
state_effects:
append_flag:
mechanics_learned:
- firebreak
- extinguish
struggle:
next: se_02 # retry the same mission with more resources
adaptive:
on_previous_defeat:
bonus_units:
- fire_truck
- fire_truck
skip:
next: se_03
se_03:
map: missions/scorched-tutorial/03-call-the-airstrike
briefing: briefings/scorched/03.yaml
outcomes:
pass:
next: se_04
state_effects:
append_flag:
mechanics_learned:
- incendiary_airstrike
skip:
next: se_04
se_04:
map: missions/scorched-tutorial/04-trial-by-fire
briefing: briefings/scorched/04.yaml
outcomes:
pass:
description: "Training complete — you're ready for the Scorched Earth campaign"
Mission 01 Lua script — introducing the flamethrower and fire spread:
-- mods/scorched-earth/missions/scorched-tutorial/01-meet-the-pyro.lua
function OnMissionStart()
local player = Player.GetPlayer("GoodGuy")
local enemy = Player.GetPlayer("BadGuy")
-- Restrict everything except the new flame units
Tutorial.RestrictSidebar(true)
Tutorial.RestrictOrders({"move", "stop", "attack"})
-- Spawn player's flame squad
local pyros = Actor.Create("flame_trooper", player, spawn_south, { count = 3 })
-- Spawn enemy bunker (wood — flammable)
local bunker = Actor.Create("wood_bunker", enemy, bunker_pos)
-- Step 1: Move to position
Tutorial.SetStep("approach", {
title = "Deploy the Pyros",
hint = "Select your Flame Troopers and move them toward the enemy bunker.",
focus_area = bunker_pos,
eva_line = "new_unit_flame_trooper",
completion = { type = "move_to", area = approach_zone }
})
end
function OnStepComplete(step_id)
if step_id == "approach" then
-- Step 2: Attack the bunker
Tutorial.SetStep("ignite", {
title = "Set It Ablaze",
hint = "Right-click the wooden bunker to attack it. " ..
"Flame Troopers set structures on fire — watch it spread.",
highlight_ui = "command_bar",
completion = { type = "action", action = "attack", target_type = "wood_bunker" }
})
elseif step_id == "ignite" then
-- Step 3: Observe fire spread (no player action needed — just watch)
Tutorial.ShowHint(
"Fire spreads to adjacent flammable tiles. " ..
"Trees, wooden structures, and dry grass will catch fire. " ..
"Stone and water are fireproof.", {
title = "Fire Spread",
duration = 10,
position = "near_building",
icon = "hint_fire",
})
-- Wait for the fire to spread to at least 3 tiles
Tutorial.SetStep("watch_spread", {
title = "Watch It Burn",
hint = "Observe the fire spreading to nearby trees.",
completion = { type = "custom", lua_condition = "GetFireTileCount() >= 3" }
})
elseif step_id == "watch_spread" then
Tutorial.ShowHint("Fire is a powerful tool — but it burns friend and foe alike. " ..
"Be careful where you aim.", {
title = "A Word of Caution",
duration = 8,
position = "screen_center",
})
Trigger.AfterDelay(DateTime.Seconds(10), function()
Campaign.complete("pass")
end)
end
end
Mod-specific hints for in-game discovery:
# mods/scorched-earth/hints/fire-hints.yaml
hints:
- id: se_fire_near_friendly
title: "Watch Your Flames"
text: "Fire is spreading toward your own buildings! Move units away or build a firebreak."
category: mod_specific
trigger:
type: custom
lua_condition: "IsFireNearFriendlyBuilding(5)" # within 5 cells
suppression:
mastery_action: build_firebreak
mastery_threshold: 2
cooldown_seconds: 120
max_shows: 5
experience_profiles:
- all
priority: high
position: near_building
eva_line = se_fire_warning
This pattern scales to any complexity — the modder uses the same YAML campaign format for a 3-mission mod tutorial that the engine uses for its 6-mission Commander School. The Tutorial Lua API, hints.yaml schema, and scenario editor Tutorial modules (D038) all work identically for first-party and third-party content.
LLM Campaign Generation
The LLM (ic-llm) can generate entire campaign graphs, not just individual missions:
User: "Create a 5-mission Soviet campaign where you invade Alaska.
The player should be able to lose a mission and keep going
with consequences. Units should carry over between missions."
LLM generates:
→ campaign.yaml (graph with 5+ nodes, branching on outcomes)
→ 5-7 mission files (main path + fallback branches)
→ Lua scripts with Campaign API calls
→ briefing text for each mission
→ carryover rules per transition
The template/scene system makes this tractable — the LLM composes from known building blocks rather than generating raw code. Campaign graphs are validated at load time (no orphan nodes, all outcomes have targets).
Security (V40): LLM-generated content (YAML rules, Lua scripts, briefing text) must pass through the
ic mod checkvalidation pipeline before execution — same as Workshop submissions. Additional defenses: cumulative mission-lifetime resource limits, content filter for generated text, sandboxed preview mode. LLM output is treated as untrusted Tier 2 mod content, never trusted first-party. See06-SECURITY.md§ Vulnerability 40.