Last modified: January 24, 2026
This article is written in: πΊπΈ
The Mission Framework provides a formal authoring layer for creating structured gameplay experiences in Standard of Iron. It separates playable maps from mission logic, allowing designers to create reusable missions and organize them into campaigns.
assets/
βββ maps/ # Playable level geometry + terrain
β βββ map_forest.json
β βββ map_mountain.json
β βββ map_rivers.json
βββ missions/ # Gameplay rules that reference maps
β βββ defend_outpost.json
β βββ forest_ambush.json
βββ campaigns/ # Ordered mission collections
βββ second_punic_war.json
{
"id": "defend_outpost",
"title": "Defend the Outpost",
"summary": "Hold your position against waves of enemy attacks",
"map_path": ":/assets/maps/map_forest.json",
"player_setup": { ... },
"ai_setups": [ ... ],
"victory_conditions": [ ... ],
"defeat_conditions": [ ... ],
"events": [ ... ]
}
Defines the human player's starting configuration:
"player_setup": {
"nation": "roman_republic",
"faction": "roman",
"color": "red",
"starting_units": [
{
"type": "spearman",
"count": 10,
"position": {"x": 60, "z": 60}
}
],
"starting_buildings": [
{
"type": "barracks",
"position": {"x": 60, "z": 60},
"max_population": 200
}
],
"starting_resources": {
"gold": 1000,
"food": 500
}
}
Defines AI opponents with personality and behavior:
"ai_setups": [
{
"id": "enemy_1",
"nation": "carthage",
"faction": "carthaginian",
"color": "blue",
"difficulty": "medium",
"team_id": 1,
"personality": {
"aggression": 0.7,
"defense": 0.3,
"harassment": 0.5
},
"waves": [
{
"timing": 120.0,
"composition": [
{"type": "swordsman", "count": 8},
{"type": "archer", "count": 4}
],
"entry_point": {"x": 190, "z": 190}
}
]
}
]
AI opponents can be assigned to teams using the team_id field. AI players with the same team_id will be allied and won't attack each other. If team_id is not specified, each AI player will be on their own team (enemies to all other AI players).
Example with allied AI opponents:
"ai_setups": [
{
"id": "roman_legion_1",
"nation": "roman_republic",
"team_id": 1,
...
},
{
"id": "roman_legion_2",
"nation": "roman_republic",
"team_id": 1,
...
}
]
Supported victory condition types:
Example:
"victory_conditions": [
{
"type": "survive_duration",
"duration": 600.0,
"description": "Survive for 10 minutes"
}
]
Supported defeat condition types:
Example:
"defeat_conditions": [
{
"type": "lose_structure",
"structure_type": "barracks",
"description": "Do not lose your barracks"
}
]
Timed or state-based triggers for dynamic gameplay:
"events": [
{
"trigger": {
"type": "timer",
"time": 300.0
},
"actions": [
{
"type": "show_message",
"text": "Reinforcements approaching!"
}
]
}
]
{
"id": "second_punic_war",
"title": "Second Punic War",
"description": "Campaign across the Mediterranean",
"missions": [
{
"mission_id": "forest_ambush",
"order_index": 0,
"intro_text": "Your first task...",
"outro_text": "Well done!",
"difficulty_modifier": 1.0
}
]
}
order_index must be contiguous starting from 0 or 1Run content validation:
# Via CMake
cmake --build build --target validate-content
# Via Makefile
make validate-content
# Direct execution
./build/bin/content_validator assets
The validator performs:
Correct data types
Asset Reference Validation
Campaign missions reference valid mission files
Campaign Validation
Starts at 0 or 1
Cross-Reference Checks
Add to your build process:
# Make validation part of the build
add_dependencies(standard_of_iron validate-content)
When uncommented, invalid content will fail the build.
#include "game/map/mission_loader.h"
Game::Mission::MissionDefinition mission;
QString error;
if (!Game::Mission::MissionLoader::loadFromJsonFile(
":/assets/missions/defend_outpost.json",
mission,
&error)) {
qWarning() << "Failed to load mission:" << error;
return;
}
// Use mission data
qInfo() << "Loaded mission:" << mission.title;
#include "game/map/campaign_loader.h"
Game::Campaign::CampaignDefinition campaign;
QString error;
if (!Game::Campaign::CampaignLoader::loadFromJsonFile(
":/assets/campaigns/second_punic_war.json",
campaign,
&error)) {
qWarning() << "Failed to load campaign:" << error;
return;
}
// Iterate missions
for (const auto& mission : campaign.missions) {
qInfo() << "Mission" << mission.order_index << ":" << mission.mission_id;
}
// In GameEngine
void GameEngine::start_campaign_mission(const QString &mission_path) {
// mission_path format: "campaign_id/mission_id"
// Example: "second_punic_war/defend_outpost"
// Mission loader parses JSON and configures:
// - Player and AI setups
// - Victory/defeat conditions
// - Map reference
}
The CampaignMenu displays available campaigns and their missions:
Mission-specific victory conditions override map defaults:
assets/missions/
βββ 01_tutorial/
β βββ basic_combat.json
β βββ defend_base.json
βββ 02_main_campaign/
β βββ forest_battle.json
β βββ siege_warfare.json
βββ 03_advanced/
βββ survival_challenge.json
"Mission not found" - Check mission_id matches filename - Ensure mission file is in assets/missions/
"Order index not contiguous" - Campaign missions must have sequential indices - No gaps allowed (0, 1, 2, ... or 1, 2, 3, ...)
"Referenced map not found" - Verify map_path is correct - Check map file exists in assets/maps/
Enable verbose logging:
QT_LOGGING_RULES="*.debug=true" ./standard_of_iron
Check mission loading:
qDebug() << "Loading mission:" << mission.id;
qDebug() << "Victory conditions:" << mission.victory_conditions.size();
Planned features:
See also:
- game/map/mission_definition.h - Mission data structures
- game/map/campaign_definition.h - Campaign data structures
- game/map/mission_loader.h - Mission loading API
- game/map/campaign_loader.h - Campaign loading API
- tools/content_validator/ - Validation implementation