Modding Guide
TinyGenerals is data-driven — game rules, factions, and visuals are defined in JSON files and sprite sheets. This guide covers the format and structure you need to create custom content.
Rulesets
Section titled “Rulesets”A ruleset is a single JSON file that defines all game rules: terrain, units, buildings, factions, combat, and vision. The default ruleset is “Classic TinyGenerals.”
Ruleset Structure
Section titled “Ruleset Structure”{ "id": "my-ruleset", "name": "My Custom Ruleset", "faction_mode": "optional", "faction_selection": { "required": false, "default_behavior": "prompt", "allow_mirror_matches": true, "auto_balance": false }, "factions": { ... }, "terrain_types": { ... }, "overlay_types": { ... }, "unit_types": { ... }, "building_types": { ... }, "vision_rules": { "mountain_vision_bonus": 1, "forest_blocks_vision": true, "mountain_blocks_vision": true }, "combat_rules": { "base_damage_formula": "attacker.attack", "adjacent_only": true, "attack_model": "soft_hard", "terrain_bonus_enabled": true, "building_bonus_enabled": true, "terrain_building_stack": "max", "ambush_bonus": 25, "suppression_enabled": true, "global_suppression_ratio": 0.0, "pinned_threshold": 1, "friendly_fire_enabled": false, "friendly_fire_damage": 0.5 }}Faction Mode
Section titled “Faction Mode”| Value | Behavior |
|---|---|
"none" | No factions. All players use the shared unit_types. |
"optional" | Players may pick a faction for unique units. |
"required" | Every player must select a faction before the game starts. |
Shared vs Faction Units
Section titled “Shared vs Faction Units”The top-level unit_types defines generic fallback units used only when no faction is selected. In faction games, each faction’s own unit_types completely replaces any shared unit with the same key:
{ "unit_types": { "INFANTRY": { "name": "Infantry", "role": "INFANTRY", "health": 10, ... }, "VEHICLE": { "name": "Vehicle", "role": "ARMOR", "health": 15, ... } }, "factions": { "ALLIANCE": { "unit_types": { "INFANTRY": { "name": "Militia", "role": "INFANTRY", "health": 10, ... }, "VEHICLE": { "name": "Battle Tank", "role": "ARMOR", "health": 18, ... }, "HOWITZER": { "name": "Howitzer", "role": "RANGED", "health": 10, ... } } } }}An Alliance player gets Militia, Battle Tank, and Howitzer — never the generic Infantry or Vehicle.
Unit Archetypes (Roles & Tiers)
Section titled “Unit Archetypes (Roles & Tiers)”Maps are faction-agnostic — the map creator doesn’t know which factions will play. Spawn points use archetypes (a role + power tier) that resolve to the correct faction unit at game start.
Every unit definition includes a role field:
"HOWITZER": { "name": "Howitzer", "role": "RANGED", "health": 10, "min_attack_range": 2, "max_attack_range": 4, ...}| Role | Description | Current units |
|---|---|---|
INFANTRY | Foot soldiers, can capture buildings | Militia, Trooper, Shock Trooper |
ARMOR | Heavy armored vehicles (tanks) | Battle Tank, Assault Tank |
RANGED | Long-range indirect fire (artillery, rockets) | Howitzer, Rocket Battery |
ANTI_ARMOR | (Reserved) Anti-tank / tank destroyers | — |
RECON | (Reserved) Fast scouts with high vision | — |
VEHICLE | (Reserved) Light vehicles (APCs, IFVs) | — |
ENGINEER | (Reserved) Builders, repair, field fortifications | — |
AIR_DEFENSE | (Reserved) Anti-air units | — |
FIGHTER | (Reserved) Air superiority aircraft | — |
BOMBER | (Reserved) Ground attack aircraft | — |
NAVAL | (Reserved) Surface warships | — |
Units in the same role can have different power tiers:
"SHOCK_TROOPER": { "name": "Shock Trooper", "role": "INFANTRY", "tier": 2, "move_points": 4, "allow_move_after_attack": true, ...}| Tier | Meaning | Example |
|---|---|---|
| 1 (default) | Basic unit | Trooper |
| 2 | Advanced variant | Shock Trooper |
| 3 | Elite variant | (future) |
Units without a tier field are tier 1.
Spawn Resolution
Section titled “Spawn Resolution”When a game starts, each spawn point resolves like this:
- Direct match — If the spawn value is a unit ID (e.g.
"HOWITZER"), use it directly - Role + tier scan — Find a faction unit with matching
roleandtier - Tier fallback — No match at requested tier? Try the next lower tier, down to T1
- Last resort — No unit matches the role at all? Spawn the faction’s Infantry
Example: A map spawn is set to INFANTRY_2 (Infantry, tier 2):
| Faction | Has T2 infantry? | Result |
|---|---|---|
| Federation | Yes (Shock Trooper) | Spawns Shock Trooper |
| Alliance | No | Falls back to T1 → Spawns Militia |
Using Archetypes in the Map Editor
Section titled “Using Archetypes in the Map Editor”In the map editor’s Spawns mode:
- Select a player slot (P1–P8)
- Pick a role — Infantry, Armor, or Ranged
- Pick a tier — T1, T2, or T3
- Click hexes to place spawn points
The spawn marker shows the role letter and tier (e.g. I for Infantry T1, R2 for Ranged T2).
Creating a Faction
Section titled “Creating a Faction”A faction lives inside the ruleset’s factions block. Here’s the minimal structure:
"MY_FACTION": { "id": "MY_FACTION", "name": "The Ironclads", "description": "Heavy defensive faction with powerful armor.", "version": "1.0.0", "difficulty": "intermediate", "playstyle_tags": ["defensive", "armor-heavy"], "emblem": { "type": "emoji", "value": "🛡️" }, "asset_pack": "default", "availability": { "game_modes": [], "seasonal": null, "event_locked": "" }, "unlock_requirements": { "default_unlocked": true, "premium_only": false, "required_trophies": [] }, "preview": { "tagline": "Unbreakable line", "showcase_units": ["INFANTRY", "HEAVY_TANK"], "unlock_hint": "Available by default" }, "relationships": { "counters": [], "countered_by": [], "lore_enemies": [] }, "unit_types": { "INFANTRY": { "name": "Garrison", "role": "INFANTRY", "symbol": "G", "health": 12, "move_points": 2, "attack": 3, "soft_attack": 3, "hard_attack": 2, "target_type": "SOFT", "splash_damage": 0, "defense": 0, "shots_per_attack": 1, "aiming_spread": 0, "area_target_radius": 0, "vision_range": 2, "can_traverse_all": true, "can_capture": true, "cost": 150, "build_time": 1, "attack_cost": 0, "min_attack_range": 1, "max_attack_range": 1, "max_attacks_per_turn": 1, "allow_move_after_attack": false, "allow_attack_after_move": true, "suppression_ratio": 0.0, "can_be_suppressed": true } }, "building_types": {}, "bonuses": { "starting_gold_bonus": 0, "income_multiplier": 1.0, "unit_cost_multiplier": 1.0, "vision_range_bonus": 0 }}Unit Definition Fields
Section titled “Unit Definition Fields”| Field | Type | Description |
|---|---|---|
name | string | Display name (also used for sprite lookup) |
role | string | Archetype — see roles table above |
tier | int | Power tier within role (omit or 0 for tier 1) |
symbol | string | 1-2 character fallback when no sprite is available |
health | int | Maximum hit points |
move_points | int | Movement points per turn |
attack | int | Legacy attack value (used when soft/hard are both 0) |
soft_attack | int | Damage vs soft targets (infantry, artillery) |
hard_attack | int | Damage vs hard targets (vehicles, tanks) |
target_type | string | "SOFT" or "HARD" — what this unit counts as when defending |
splash_damage | int | (Reserved) Area-of-effect damage — not yet implemented |
defense | int | (Reserved) Defense modifier — not yet implemented |
shots_per_attack | int | (Reserved) Projectiles per attack action (default 1) — not yet implemented |
aiming_spread | float | (Reserved) Shot distribution within target area (0.0 = perfect) — not yet implemented |
area_target_radius | int | (Reserved) Targetable area in hexes (0 = single tile) — not yet implemented |
vision_range | int | How far this unit can see (in hexes) |
can_traverse_all | bool | Can cross mountains, shallow water, and other vehicle-impassable terrain |
can_capture | bool | Can capture buildings |
cost | int | Gold cost to produce |
build_time | int | Turns to produce (1 = instant, 2+ = queued production) |
min_attack_range | int | Minimum attack range (1 = adjacent) |
max_attack_range | int | Maximum attack range (1 = melee only) |
attack_cost | int | Movement points consumed per attack (0 = free) |
max_attacks_per_turn | int | How many times this unit can attack per turn |
allow_move_after_attack | bool | Can move after attacking |
allow_attack_after_move | bool | Can attack after moving |
suppression_ratio | float | Fraction of dealt damage converted to suppression (0.0-1.0) |
can_be_suppressed | bool | Whether this unit can receive suppression effects |
Fields marked (Reserved) exist in the schema and should be included with their default values (0 or false) for forward compatibility, but have no gameplay effect yet.
Suppression and counter-attack tuning
Section titled “Suppression and counter-attack tuning”The suppression system is split between unit-level and combat-level settings:
- Unit-level
suppression_ratio: how much of this unit’s damage becomes suppression instead of hard HP losscan_be_suppressed: whether this unit can receive suppression
- Combat-level (
combat_rules)suppression_enabled: master switchglobal_suppression_ratio: fallback when a unit does not definesuppression_ratiopinned_threshold: effective strength at or below which units cannot counter-attack
This lets you make artillery heavily suppression-oriented while keeping direct-fire units mostly hard-damage focused.
Faction Checklist
Section titled “Faction Checklist”- Include at least one unit with
"role": "INFANTRY"— this is the last-resort spawn fallback - Cover the core roles (
INFANTRY,ARMOR,RANGED) so maps with varied archetypes work - Give each unit a unique
symbol(1-2 characters) for the fallback renderer - Faction bonuses are multipliers —
1.0means no change from baseline
Themes
Section titled “Themes”Themes control the visual appearance: terrain tiles, unit sprites, building graphics, overlays, and animations. The game ships with a default theme and falls back to colored shapes for any missing assets.
Theme Structure
Section titled “Theme Structure”A theme lives in public/themes/{theme-name}/ and contains a JSON manifest plus asset subdirectories. Every asset follows the naming pattern {name}_v{variant}_f{frame}.png:
themes/my-theme/ theme.json ← Manifest (declares assets and animations) terrain/ grass/ grass_v1_f1.png ← Variant 1, Frame 1 (static) grass_v2_f1.png ← Variant 2 (another static look) water/ water_v1_f1.png ← Animated: frame 1 water_v1_f2.png ← Animated: frame 2 forest/ forest_v1_f1.png ... overlays/ road/ road_isolated_v1_f1.png ← Autotile shapes for roads road_straight_ew_v1_f1.png road_bend60_e_ne_v1_f1.png ... river/ river_source_v1_f1.png river_straight_v1_f1.png ... units/ militia_v1_f1.png battle_tank_v1_f1.png howitzer_v1_f1.png ... buildings/ base_v1_f1.png factory_v1_f1.png city_v1_f1.png ...Key conventions:
v{N}= variant number (for visual variety — multiple grass looks, etc.)f{N}= frame number (multiple frames = animation, single frame = static)- Filenames are lowercase with underscores
- Terrain tiles go in subfolders per type; units and buildings are flat
Tile Dimensions
Section titled “Tile Dimensions”The default theme uses 64×64 pixel tiles. Declare your dimensions in theme.json:
{ "tile_dimensions": { "width": 64, "height": 64, "hex_size": 64 }}Theme Manifest (theme.json)
Section titled “Theme Manifest (theme.json)”The manifest declares every asset, its variants, and animation properties. Here’s a trimmed example matching the default theme:
{ "id": "my-theme", "name": "My Theme", "version": "1.0.0", "tile_dimensions": { "width": 64, "height": 64, "hex_size": 64 }, "default_fps": 8,
"terrain": { "GRASS": { "variants": { "1": { "frames": ["/themes/my-theme/terrain/grass/grass_v1_f1.png"], "fps": 0, "animated": false }, "2": { "frames": ["/themes/my-theme/terrain/grass/grass_v2_f1.png"], "fps": 0, "animated": false } } }, "SHALLOW_WATER": { "variants": { "1": { "frames": [ "/themes/my-theme/terrain/shallow_water/shallow_water_v1_f1.png", "/themes/my-theme/terrain/shallow_water/shallow_water_v1_f2.png", "/themes/my-theme/terrain/shallow_water/shallow_water_v1_f3.png" ], "fps": 1, "animated": true } } } },
"overlays": { "ROAD": { "autotile": true, "shapes": { "isolated": { "variants": { "1": { "frames": ["...road_isolated_v1_f1.png"], "animated": false } } }, "straight_ew": { "variants": { "1": { "frames": ["...road_straight_ew_v1_f1.png"], "animated": false } } }, "bend60_e_ne": { "variants": { "1": { "frames": ["...road_bend60_e_ne_v1_f1.png"], "animated": false } } } } } },
"units": { "MILITIA": { "variants": { "1": { "frames": ["/themes/my-theme/units/militia_v1_f1.png"], "animated": false } } }, "BATTLE_TANK": { "variants": { "1": { "frames": ["/themes/my-theme/units/battle_tank_v1_f1.png"], "animated": false } } } },
"buildings": { "BASE": { "variants": { "1": { "frames": ["/themes/my-theme/buildings/base_v1_f1.png"], "animated": false } } }, "FACTORY": { "variants": { "1": { "frames": ["/themes/my-theme/buildings/factory_v1_f1.png"], "animated": false } } } }}Unit Sprite Matching
Section titled “Unit Sprite Matching”Unit sprite keys in theme.json are matched by the unit’s display name (uppercased, spaces replaced with underscores):
| Unit name | theme.json key | Sprite file |
|---|---|---|
| Militia | MILITIA | militia_v1_f1.png |
| Battle Tank | BATTLE_TANK | battle_tank_v1_f1.png |
| Shock Trooper | SHOCK_TROOPER | shock_trooper_v1_f1.png |
| Rocket Battery | ROCKET_BATTERY | rocket_battery_v1_f1.png |
If the name-based sprite isn’t found, the game tries the unit type ID (e.g. INFANTRY). If neither exists, it renders a colored circle with the unit’s symbol text.
Overlays
Section titled “Overlays”Overlays are terrain-agnostic features drawn on top of base terrain (roads, rivers). They are defined in both the ruleset’s overlay_types and the theme’s overlays section.
Road overlays use an autotile system — the game automatically picks the correct shape (straight, bend, T-junction, crossroads, etc.) based on neighboring road tiles. The default theme ships with 20+ road shapes.
River overlays use a path-based autotile method — rivers are ordered sequences of tiles with source, mouth, straight, and curve shapes.
For detailed sprite dimensions and hex geometry, see the Graphics Guide.
Complete Field Reference
Section titled “Complete Field Reference”Terrain Fields (terrain_types)
Section titled “Terrain Fields (terrain_types)”| Field | Type | Description |
|---|---|---|
name | string | Display name |
move_cost | float | Movement point cost to enter (-1 = impassable) |
passable | bool | Can infantry/foot units enter |
vehicle_passable | bool | Can vehicles/armor enter |
naval_passable | bool | (Future) Can naval units enter |
air_passable | bool | (Future) Can air units enter |
vision_blocking | bool | Blocks line-of-sight rays |
income | int | (Legacy) Gold per turn when owned — income now comes from buildings |
defense_bonus | int | % damage reduction for a defender on this terrain |
attack_bonus | int | % damage bonus for an attacker on this terrain |
fortification_bonus | int | % extra damage reduction vs ranged/indirect attacks |
Overlay Fields (overlay_types)
Section titled “Overlay Fields (overlay_types)”| Field | Type | Description |
|---|---|---|
name | string | Display name |
move_cost_modifier | float | Effective move cost (replaces or modifies base terrain cost) |
stacking | string | How cost is applied: "replace" (default), "multiply", "add" |
passable | bool | Can infantry enter |
vehicle_passable | bool | Can vehicles enter |
naval_passable | bool | (Future) Can naval units enter |
air_passable | bool | (Future) Can air units enter |
vision_blocking | bool | Blocks line-of-sight |
compatible_terrains | [string] | Base terrains this overlay can exist on (empty = any passable) |
incompatible_terrains | [string] | Base terrains this overlay cannot exist on |
Building Fields (building_types)
Section titled “Building Fields (building_types)”| Field | Type | Description |
|---|---|---|
name | string | Display name |
income | int | Gold generated per turn when owned |
can_produce | bool | Can build units here |
producible_units | [string] | Unit types this building can produce (empty = all available) |
capture_required | int | Turns to capture |
defense_bonus | int | Combat defense modifier (%) |
upgrade_to | string | Building type this can upgrade to (empty = none) |
upgrade_cost | int | Gold cost to upgrade |
upgrade_turns | int | Turns to complete upgrade |
placement_rules | object | Where this building can be placed (see below) |
Placement Rules
Section titled “Placement Rules”| Field | Type | Description |
|---|---|---|
allowed_terrains | [string] | Terrain types where building can be placed (empty = any) |
forbidden_terrains | [string] | Terrain types where building cannot be placed |
requires_adjacent | [string] | Must be adjacent to at least one of these terrain types |
requires_distance_from | [object] | (Future) Minimum distance from other buildings |
max_per_map | int | (Future) Maximum count per map (0 = unlimited) |
min_distance_from_edge | int | (Future) Minimum distance from map edge |
Vision Rules (vision_rules)
Section titled “Vision Rules (vision_rules)”| Field | Type | Description | Default |
|---|---|---|---|
mountain_vision_bonus | int | Extra vision range for units on mountains | 1 |
forest_blocks_vision | bool | Forests block line-of-sight | true |
mountain_blocks_vision | bool | Mountains block line-of-sight | true |
Combat Rules (combat_rules)
Section titled “Combat Rules (combat_rules)”| Field | Type | Description | Default |
|---|---|---|---|
base_damage_formula | string | Damage formula identifier | "attacker.attack" |
adjacent_only | bool | Restrict combat to adjacent hexes only | true |
attack_model | string | "single" or "soft_hard" (choose attack stat by target type) | "soft_hard" |
terrain_bonus_enabled | bool | Apply terrain defense/attack/fortification bonuses | true |
building_bonus_enabled | bool | Apply building defense bonus in combat | true |
terrain_building_stack | string | How terrain + building bonuses combine: "max" or "add" | "max" |
ambush_bonus | int | % bonus for ambush attacks | 25 |
suppression_enabled | bool | Master switch for suppression system | true |
global_suppression_ratio | float | Fallback suppression ratio when unit has none set | 0.0 |
pinned_threshold | int | Effective strength at or below which counter-attack is disabled | 1 |
friendly_fire_enabled | bool | (Future) Whether area attacks can damage allies | false |
friendly_fire_damage | float | (Future) Damage multiplier for friendly fire | 0.5 |
Faction Bonuses (bonuses)
Section titled “Faction Bonuses (bonuses)”| Field | Type | Description | Default |
|---|---|---|---|
starting_gold_bonus | int | Extra gold at game start | 0 |
income_multiplier | float | Multiplier on building income (1.0 = no change) | 1.0 |
unit_cost_multiplier | float | Multiplier on unit costs (< 1.0 = discount) | 1.0 |
vision_range_bonus | int | Extra vision range for all faction units | 0 |
Quick Reference
Section titled “Quick Reference”Current Archetype Map
Section titled “Current Archetype Map”Role (role value) | Alliance T1 | Alliance T2 | Federation T1 | Federation T2 |
|---|---|---|---|---|
INFANTRY | Militia | — | Trooper | Shock Trooper |
ARMOR | Battle Tank | — | Assault Tank | — |
RANGED | Howitzer | — | Rocket Battery | — |
Terrain Types (default ruleset)
Section titled “Terrain Types (default ruleset)”| ID | Passable | Vehicle | Notes |
|---|---|---|---|
GRASS | Yes | Yes | Standard terrain, move cost 1 |
FOREST | Yes | Yes | Move cost 2, blocks vision |
MOUNTAIN | Yes | No | Move cost 2, blocks vision, +1 vision bonus |
SHALLOW_WATER | Yes* | No | Move cost 2, *infantry only (can_traverse_all) |
DEEP_WATER | No | No | Impassable to land units |
WATER | No | No | (Deprecated — use DEEP_WATER) |
BASE | Yes | Yes | Terrain under base buildings |
EMPTY | No | No | Void / off-map |
Overlay Types (default ruleset)
Section titled “Overlay Types (default ruleset)”| ID | Effect | Autotile | Notes |
|---|---|---|---|
ROAD | Move cost 0.5 (replaces base terrain cost) | Yes | Compatible with GRASS, FOREST, MOUNTAIN |
Building Types (default ruleset)
Section titled “Building Types (default ruleset)”| ID | Income | Produces | Defense | Notes |
|---|---|---|---|---|
BASE | 100 | Yes | 10% | Upgrades to HQ |
HQ | 200 | Yes | 25% | Top-tier command center |
FACTORY | 150 | Yes | 15% | Production powerhouse |
CITY | 50 | No | 15% | Economic point |
AIRFIELD | 100 | Yes | 10% | Flat terrain only |
SEAPORT | 100 | Yes | 10% | Must be adjacent to water |
OUTPOST | 75 | No | 5% | Can be placed anywhere |
DEPOT | 125 | No | 10% | Economic objective |
Spawn Resolution Flow
Section titled “Spawn Resolution Flow”Map spawn: "RANGED_2" (Ranged, tier 2) │ ├─ Faction has RANGED tier 2? → Yes → Spawn it │ → No → Try tier 1 │ ├─ Faction has RANGED tier 1? → Yes → Spawn it │ → No → Spawn Infantry T1 │ └─ DoneAs new units and factions are added, the archetype system picks them up automatically — existing maps don’t need updates.