recipe iteration

This commit is contained in:
2026-06-13 10:59:22 +02:00
parent dbf334c829
commit 68c1345660
4 changed files with 715 additions and 79 deletions

View File

@@ -1,3 +1,29 @@
# recipes.toml
#
# First real-content iteration of the production tree. Quantities and
# durations are a first guess; the balancing pass will tune them and assign
# real unlock_at_station_level values (everything is unlocked for now so the
# full tree is testable).
#
# Input chain per game phase — each phase adds exactly one new base input:
#
# early iron_ore + copper_ore -> ingots -> copper_wire, steel_plate,
# circuit_board
# mid + titanium_ore -> titanium_frame; assembler-made
# mechanical_parts, targeting_unit,
# drive_unit
# late + advanced_alloy -> reinforced_plating, capital_core.
# advanced_alloy CANNOT be mined; it only
# comes from reprocessing salvaged scrap,
# so capital production requires combat.
#
# Run tools/verify_recipes.py after editing to check that every consumed
# item has a producer and every item has a visuals.toml entry.
# -----------------------------------------------------------------------------
# Mining (tier 0)
# -----------------------------------------------------------------------------
[[recipe]] [[recipe]]
id = "mine_iron_ore" id = "mine_iron_ore"
building = "miner" building = "miner"
@@ -12,6 +38,18 @@ inputs = []
outputs = [{item = "copper_ore", amount = 1}] outputs = [{item = "copper_ore", amount = 1}]
duration_seconds = 1.5 duration_seconds = 1.5
# Titanium is the midgame ore: mined three times slower than iron.
[[recipe]]
id = "mine_titanium_ore"
building = "miner"
inputs = []
outputs = [{item = "titanium_ore", amount = 1}]
duration_seconds = 3.0
# -----------------------------------------------------------------------------
# Smelting (tier 1)
# -----------------------------------------------------------------------------
[[recipe]] [[recipe]]
id = "iron_ingot" id = "iron_ingot"
building = "smelter" building = "smelter"
@@ -27,54 +65,17 @@ outputs = [{item = "copper_ingot", amount = 1}]
duration_seconds = 2.5 duration_seconds = 2.5
[[recipe]] [[recipe]]
id = "circuit_board" id = "titanium_ingot"
building = "assembler" building = "smelter"
inputs = [{item = "iron_ingot", amount = 1}, {item = "copper_ingot", amount = 2}] inputs = [{item = "titanium_ore", amount = 3}]
outputs = [{item = "circuit_board", amount = 1}] outputs = [{item = "titanium_ingot", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "drone_hull"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 1}]
outputs = [{item = "drone_hull", amount = 1}]
duration_seconds = 4.0 duration_seconds = 4.0
[[recipe]] # -----------------------------------------------------------------------------
id = "laser_cannon_xs_module" # Reprocessing
building = "assembler" #
inputs = [{item = "iron_ingot", amount = 2}, {item = "circuit_board", amount = 1}] # The only source of advanced_alloy: salvaged scrap from destroyed ships.
outputs = [{item = "laser_cannon_xs_module", amount = 1}] # -----------------------------------------------------------------------------
duration_seconds = 3.0
[[recipe]]
id = "laser_cannon_s_module"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
outputs = [{item = "laser_cannon_s_module", amount = 1}]
duration_seconds = 6.0
[[recipe]]
id = "salvager_module"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
outputs = [{item = "salvager_module", amount = 1}]
duration_seconds = 6.0
[[recipe]]
id = "repair_tool_module"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 2}]
outputs = [{item = "repair_tool_module", amount = 1}]
duration_seconds = 6.0
[[recipe]]
id = "building_blocks"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "iron_ingot", amount = 4}]
outputs = [{item = "building_block", amount = 10}]
duration_seconds = 4.0
[[recipe]] [[recipe]]
id = "reprocessing_cycle" id = "reprocessing_cycle"
@@ -85,15 +86,354 @@ duration_seconds = 3.0
[[recipe.outputs]] [[recipe.outputs]]
item = "iron_ingot" item = "iron_ingot"
amount = 2 amount = 2
probability = 0.6 probability = 0.45
[[recipe.outputs]] [[recipe.outputs]]
item = "circuit_board" item = "copper_ingot"
amount = 1 amount = 1
probability = 0.3 probability = 0.25
[[recipe.outputs]]
item = "titanium_ingot"
amount = 1
probability = 0.15
[[recipe.outputs]] [[recipe.outputs]]
item = "advanced_alloy" item = "advanced_alloy"
amount = 1 amount = 1
probability = 0.1 probability = 0.15
# -----------------------------------------------------------------------------
# Basic components (tier 2, early game)
# -----------------------------------------------------------------------------
[[recipe]]
id = "copper_wire"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "copper_ingot", amount = 1}]
outputs = [{item = "copper_wire", amount = 2}]
duration_seconds = 1.5
[[recipe]]
id = "steel_plate"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "iron_ingot", amount = 2}]
outputs = [{item = "steel_plate", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "circuit_board"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "iron_ingot", amount = 1}, {item = "copper_wire", amount = 2}]
outputs = [{item = "circuit_board", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "building_blocks"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "iron_ingot", amount = 4}]
outputs = [{item = "building_block", amount = 10}]
duration_seconds = 4.0
# -----------------------------------------------------------------------------
# Advanced components (tier 3, midgame)
# -----------------------------------------------------------------------------
[[recipe]]
id = "mechanical_parts"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "steel_plate", amount = 1}, {item = "iron_ingot", amount = 1}]
outputs = [{item = "mechanical_parts", amount = 2}]
duration_seconds = 2.5
[[recipe]]
id = "targeting_unit"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "circuit_board", amount = 2}, {item = "copper_wire", amount = 1}]
outputs = [{item = "targeting_unit", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "drive_unit"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "steel_plate", amount = 1},
{item = "mechanical_parts", amount = 1},
{item = "circuit_board", amount = 1},
]
outputs = [{item = "drive_unit", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "titanium_frame"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "titanium_ingot", amount = 2}, {item = "steel_plate", amount = 1}]
outputs = [{item = "titanium_frame", amount = 1}]
duration_seconds = 4.0
# -----------------------------------------------------------------------------
# Capital components (tier 4, lategame — gated on advanced_alloy)
# -----------------------------------------------------------------------------
[[recipe]]
id = "reinforced_plating"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "steel_plate", amount = 2}, {item = "advanced_alloy", amount = 1}]
outputs = [{item = "reinforced_plating", amount = 1}]
duration_seconds = 5.0
[[recipe]]
id = "capital_core"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "targeting_unit", amount = 1},
{item = "drive_unit", amount = 1},
{item = "advanced_alloy", amount = 2},
]
outputs = [{item = "capital_core", amount = 1}]
duration_seconds = 8.0
# -----------------------------------------------------------------------------
# Module items — early game
# -----------------------------------------------------------------------------
[[recipe]]
id = "laser_cannon_s_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "iron_ingot", amount = 2}, {item = "circuit_board", amount = 1}]
outputs = [{item = "laser_cannon_s_module", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "salvager_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "steel_plate", amount = 1}, {item = "circuit_board", amount = 1}]
outputs = [{item = "salvager_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "repair_tool_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "circuit_board", amount = 2}, {item = "copper_wire", amount = 1}]
outputs = [{item = "repair_tool_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "armor_plates_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "steel_plate", amount = 2}]
outputs = [{item = "armor_plates_module", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "sensor_booster_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "circuit_board", amount = 1}, {item = "copper_wire", amount = 2}]
outputs = [{item = "sensor_booster_module", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "maneuvering_thrusters_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "mechanical_parts", amount = 1}, {item = "copper_wire", amount = 1}]
outputs = [{item = "maneuvering_thrusters_module", amount = 1}]
duration_seconds = 3.0
# -----------------------------------------------------------------------------
# Module items — midgame
# -----------------------------------------------------------------------------
[[recipe]]
id = "afterburner_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "drive_unit", amount = 1}, {item = "steel_plate", amount = 1}]
outputs = [{item = "afterburner_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "weapon_upgrade_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "targeting_unit", amount = 1}, {item = "steel_plate", amount = 1}]
outputs = [{item = "weapon_upgrade_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "weapon_primer_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "targeting_unit", amount = 1}, {item = "copper_wire", amount = 2}]
outputs = [{item = "weapon_primer_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "weapon_stabilizer_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "targeting_unit", amount = 1}, {item = "mechanical_parts", amount = 1}]
outputs = [{item = "weapon_stabilizer_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "laser_cannon_m_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "targeting_unit", amount = 1}, {item = "titanium_frame", amount = 1}]
outputs = [{item = "laser_cannon_m_module", amount = 1}]
duration_seconds = 6.0
[[recipe]]
id = "drone_bay_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "titanium_frame", amount = 1},
{item = "mechanical_parts", amount = 1},
{item = "circuit_board", amount = 1},
]
outputs = [{item = "drone_bay_module", amount = 1}]
duration_seconds = 6.0
# -----------------------------------------------------------------------------
# Module items — lategame
# -----------------------------------------------------------------------------
[[recipe]]
id = "laser_cannon_l_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "targeting_unit", amount = 2},
{item = "reinforced_plating", amount = 2},
{item = "titanium_frame", amount = 1},
]
outputs = [{item = "laser_cannon_l_module", amount = 1}]
duration_seconds = 12.0
[[recipe]]
id = "drone_hangar_module"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "capital_core", amount = 1},
{item = "titanium_frame", amount = 2},
{item = "reinforced_plating", amount = 2},
]
outputs = [{item = "drone_hangar_module", amount = 1}]
duration_seconds = 20.0
# -----------------------------------------------------------------------------
# Ship hulls
# -----------------------------------------------------------------------------
[[recipe]]
id = "drone_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 1}]
outputs = [{item = "drone_hull", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "frigate_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "steel_plate", amount = 3},
{item = "mechanical_parts", amount = 1},
{item = "circuit_board", amount = 1},
]
outputs = [{item = "frigate_hull", amount = 1}]
duration_seconds = 8.0
[[recipe]]
id = "destroyer_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "steel_plate", amount = 5},
{item = "mechanical_parts", amount = 2},
{item = "circuit_board", amount = 1},
]
outputs = [{item = "destroyer_hull", amount = 1}]
duration_seconds = 10.0
[[recipe]]
id = "cruiser_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "titanium_frame", amount = 2},
{item = "steel_plate", amount = 4},
{item = "drive_unit", amount = 1},
]
outputs = [{item = "cruiser_hull", amount = 1}]
duration_seconds = 15.0
[[recipe]]
id = "battlecruiser_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "titanium_frame", amount = 3},
{item = "steel_plate", amount = 6},
{item = "drive_unit", amount = 1},
{item = "targeting_unit", amount = 1},
]
outputs = [{item = "battlecruiser_hull", amount = 1}]
duration_seconds = 20.0
[[recipe]]
id = "battleship_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "titanium_frame", amount = 4},
{item = "reinforced_plating", amount = 2},
{item = "drive_unit", amount = 2},
]
outputs = [{item = "battleship_hull", amount = 1}]
duration_seconds = 30.0
[[recipe]]
id = "dreadnought_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "capital_core", amount = 1},
{item = "titanium_frame", amount = 6},
{item = "reinforced_plating", amount = 4},
{item = "drive_unit", amount = 2},
]
outputs = [{item = "dreadnought_hull", amount = 1}]
duration_seconds = 60.0
[[recipe]]
id = "carrier_hull"
unlock_at_station_level = -1
building = "assembler"
inputs = [
{item = "capital_core", amount = 1},
{item = "titanium_frame", amount = 5},
{item = "reinforced_plating", amount = 3},
{item = "drive_unit", amount = 2},
]
outputs = [{item = "carrier_hull", amount = 1}]
duration_seconds = 60.0

View File

@@ -106,6 +106,8 @@ glyph = "E"
# drawn around it. One section per ItemType. # drawn around it. One section per ItemType.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# --- ores ---
[items.iron_ore] [items.iron_ore]
fill = "#8a5a4a" fill = "#8a5a4a"
outline = "#201010" outline = "#201010"
@@ -114,6 +116,12 @@ outline = "#201010"
fill = "#c47a3a" fill = "#c47a3a"
outline = "#3a1a0a" outline = "#3a1a0a"
[items.titanium_ore]
fill = "#9aa3ad"
outline = "#2a2e33"
# --- ingots ---
[items.iron_ingot] [items.iron_ingot]
fill = "#b0b0b8" fill = "#b0b0b8"
outline = "#202028" outline = "#202028"
@@ -122,34 +130,80 @@ outline = "#202028"
fill = "#d48a4a" fill = "#d48a4a"
outline = "#402010" outline = "#402010"
[items.circuit_board] [items.titanium_ingot]
fill = "#2ea35a" fill = "#c8d2dc"
outline = "#0a2a14" outline = "#3a4048"
[items.advanced_alloy] # --- salvage loop ---
fill = "#a06acc"
outline = "#201030"
[items.building_block]
fill = "#c8b070"
outline = "#302810"
[items.scrap] [items.scrap]
fill = "#7a7268" fill = "#7a7268"
outline = "#201a14" outline = "#201a14"
[items.drone_hull] [items.advanced_alloy]
fill = "#1b1b1b" fill = "#a06acc"
outline = "#1402b3" outline = "#201030"
[items.laser_cannon_xs_module] # --- basic components ---
fill = "#691313"
outline = "#f3ff4f" [items.copper_wire]
fill = "#e09a50"
outline = "#3a2008"
[items.steel_plate]
fill = "#8a92a0"
outline = "#22262c"
[items.circuit_board]
fill = "#2ea35a"
outline = "#0a2a14"
[items.building_block]
fill = "#c8b070"
outline = "#302810"
# --- advanced components ---
[items.mechanical_parts]
fill = "#6f7a66"
outline = "#1c2018"
[items.targeting_unit]
fill = "#3a9e8c"
outline = "#0c2824"
[items.drive_unit]
fill = "#4a6ad0"
outline = "#101a38"
[items.titanium_frame]
fill = "#b8c4d4"
outline = "#343c48"
# --- capital components ---
[items.reinforced_plating]
fill = "#8a6ad0"
outline = "#1c1038"
[items.capital_core]
fill = "#b040d0"
outline = "#280c30"
# --- module items ---
[items.laser_cannon_s_module] [items.laser_cannon_s_module]
fill = "#691313" fill = "#691313"
outline = "#f3ff4f" outline = "#f3ff4f"
[items.laser_cannon_m_module]
fill = "#892020"
outline = "#f3ff4f"
[items.laser_cannon_l_module]
fill = "#a92d2d"
outline = "#f3ff4f"
[items.salvager_module] [items.salvager_module]
fill = "#b2cfdd" fill = "#b2cfdd"
outline = "#236137" outline = "#236137"
@@ -158,6 +212,76 @@ outline = "#236137"
fill = "#2e9ba3" fill = "#2e9ba3"
outline = "#689275" outline = "#689275"
[items.armor_plates_module]
fill = "#808080"
outline = "#202020"
[items.sensor_booster_module]
fill = "#40a0ff"
outline = "#102840"
[items.maneuvering_thrusters_module]
fill = "#5090e0"
outline = "#142438"
[items.afterburner_module]
fill = "#6080c0"
outline = "#182030"
[items.weapon_upgrade_module]
fill = "#ff4040"
outline = "#401010"
[items.weapon_primer_module]
fill = "#e03838"
outline = "#380e0e"
[items.weapon_stabilizer_module]
fill = "#c03030"
outline = "#300c0c"
[items.drone_bay_module]
fill = "#cc66ff"
outline = "#331040"
[items.drone_hangar_module]
fill = "#9933cc"
outline = "#260c33"
# --- ship hulls (outline matches the ship's fleet color in [ships.*]) ---
[items.drone_hull]
fill = "#1b1b1b"
outline = "#3366ff"
[items.frigate_hull]
fill = "#1b1b1b"
outline = "#44aaff"
[items.destroyer_hull]
fill = "#1b1b1b"
outline = "#33ccaa"
[items.cruiser_hull]
fill = "#1b1b1b"
outline = "#66cc33"
[items.battlecruiser_hull]
fill = "#1b1b1b"
outline = "#cccc33"
[items.battleship_hull]
fill = "#1b1b1b"
outline = "#ff9933"
[items.dreadnought_hull]
fill = "#1b1b1b"
outline = "#ff5533"
[items.carrier_hull]
fill = "#1b1b1b"
outline = "#cc66ff"
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Ships # Ships
# #

View File

@@ -1,9 +1,9 @@
# Content Design — Ships & Modules # Content Design — Ships & Modules
First real-content iteration (June 2026). This pass defines ship hull grids First real-content iterations (June 2026). Pass 1 defined ship hull grids and
and module surface masks only. Stats, materials, recipes, and threat costs in module surface masks; pass 2 defined the production tree (recipes). Stats and
the config files are placeholders; the recipe pass and the balancing pass threat costs in the config files are still placeholders for the balancing
come later. pass.
## Design principle: footprint gating ## Design principle: footprint gating
@@ -129,6 +129,48 @@ Maximum simultaneous (disjoint) placements: m guns — cruiser 2,
battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3; battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3;
drone hangar — carrier 1. drone hangar — carrier 1.
## Production tree
Design principle: each game phase adds exactly one new base input chain, so
factory complexity ramps alongside ship size.
| Phase | New input | How acquired | Unlocks |
|-------|-----------|--------------|---------|
| early | iron_ore, copper_ore | mined | drone, frigate, destroyer; small guns and basic supports |
| mid | titanium_ore | mined (3x slower than iron) | cruiser, battlecruiser; m guns, drone bay, weapon modifiers |
| late | advanced_alloy | ONLY from reprocessing salvaged scrap | battleship, dreadnought, carrier; l guns, drone hangar |
The advanced_alloy gate is the core loop hook: capital ship production
requires fighting (salvaging scrap from kills and reprocessing it), not just
mining. The reprocessing plant turns 5 scrap into iron/copper/titanium ingots
or advanced_alloy probabilistically.
Intermediate components, by tier:
- **Tier 2 (early):** copper_wire (copper), steel_plate (iron), circuit_board
(iron + wire), building_block (iron).
- **Tier 3 (mid):** mechanical_parts (steel + iron), targeting_unit (circuits
+ wire), drive_unit (steel + mechanical_parts + circuit), titanium_frame
(titanium + steel).
- **Tier 4 (late):** reinforced_plating (steel + advanced_alloy),
capital_core (targeting_unit + drive_unit + 2 advanced_alloy).
Hulls and modules consume intermediates of their tier: early items are built
from tier-2 parts, midgame items require tier-3 parts (deeper chains, more
assemblers), capital items require tier-4 parts (and therefore combat). Hull
items are named `<ship>_hull`; module items `<module>_module`. Every item has
an `[items.*]` entry in visuals.toml; hull item outlines match the ship's
fleet color from `[ships.*]`.
Consistency is checked by `tools/verify_recipes.py` — re-run it after editing
recipes, ship/module materials, or visuals:
python dota_factory/tools/verify_recipes.py
It verifies every consumed item has a producer, every item has a visuals
entry, flags orphaned items, and prints which items are reprocessing-only
(currently exactly advanced_alloy).
## Deliberate placeholders / open questions for later passes ## Deliberate placeholders / open questions for later passes
- All new hulls have `threat.cost_formula = "0"` so enemy waves do not spawn - All new hulls have `threat.cost_formula = "0"` so enemy waves do not spawn
@@ -136,14 +178,12 @@ drone hangar — carrier 1.
eligible, regardless of unlock level). The balancing pass should set real eligible, regardless of unlock level). The balancing pass should set real
threat costs together with `default_modules` loadouts so waves spawn them threat costs together with `default_modules` loadouts so waves spawn them
armed. armed.
- All new hulls are `unlock_at_station_level = -1` (available from the start) - All new hulls and all assembler recipes are `unlock_at_station_level = -1`
to make layout testing easy; the progression pass should stagger these. (available from the start) to make testing easy; the balancing pass should
- Ship hull material items (`frigate_hull``carrier_hull`) and the new stagger these so mid/lategame recipes drop as schematics from enemy defence
module items (`laser_cannon_m_module`, `laser_cannon_l_module`, stations.
`drone_bay_module`, `drone_hangar_module`, …) have no recipes yet — that is - Recipe quantities and durations are a first guess, deliberately roughly
the recipe pass. The old `laser_cannon_xs_module` recipe is orphaned (the tiered (capital hulls ~60 s, drones 4 s); the balancing pass tunes them.
module was renamed to `laser_cannon_s`, consuming `laser_cannon_s_module`,
which already has a recipe).
- `drone_bay` and `drone_hangar` are footprint-only placeholders: the drone - `drone_bay` and `drone_hangar` are footprint-only placeholders: the drone
launching capability does not exist in the simulation yet, so they define launching capability does not exist in the simulation yet, so they define
no capability section. no capability section.

132
tools/verify_recipes.py Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Verify the recipe tree is closed and consistent.
Reads recipes.toml, ships.toml, modules.toml, and visuals.toml, then checks:
1. Producers — every item consumed anywhere (recipe inputs, ship hull
materials, module materials) is produced by some recipe. 'scrap' is
exempt: it drops from destroyed ships.
2. Visuals — every item that exists in the economy has an [items.*]
entry in visuals.toml, and visuals.toml has no entries for items
that no longer exist.
3. Orphans — items that are produced but never consumed (warning only;
'building_block' is exempt: the HQ consumes it).
It also prints which items are obtainable ONLY through reprocessing —
the combat-gated materials — so changes to that gate are visible.
Usage (from the repository root or anywhere else):
python dota_factory/tools/verify_recipes.py
python dota_factory/tools/verify_recipes.py --config-dir path/to/config
By default the config directory is resolved relative to this script
(../bin/app/data/config). Requires the 'toml' package on Python < 3.11
(pip install --user toml); on 3.11+ the standard tomllib is used.
Exits 1 if a producer or visuals check fails, 0 otherwise (warnings do
not affect the exit code).
"""
import argparse
import os
import sys
WORLD_SOURCED_ITEMS = {"scrap"} # dropped by destroyed ships
IMPLICITLY_CONSUMED_ITEMS = {"building_block"} # consumed by the HQ
def load_toml(path):
try:
import tomllib
with open(path, "rb") as fh:
return tomllib.load(fh)
except ImportError:
import toml
return toml.load(path)
def main():
default_dir = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..", "bin", "app", "data", "config"))
parser = argparse.ArgumentParser(
description="Check recipe tree consistency across the config files.")
parser.add_argument("--config-dir", default=default_dir,
help="directory containing the config toml files"
" (default: %(default)s)")
args = parser.parse_args()
recipes = load_toml(os.path.join(args.config_dir, "recipes.toml"))["recipe"]
ships = load_toml(os.path.join(args.config_dir, "ships.toml"))["ship"]
modules = load_toml(os.path.join(args.config_dir, "modules.toml"))["module"]
visuals = load_toml(os.path.join(args.config_dir, "visuals.toml"))
produced = {} # item id -> [producer descriptions]
consumed = {} # item id -> [consumer descriptions]
for recipe in recipes:
for output in recipe.get("outputs", []):
produced.setdefault(output["item"], []).append(
"recipe '{}'".format(recipe["id"]))
for inp in recipe.get("inputs", []):
consumed.setdefault(inp["item"], []).append(
"recipe '{}'".format(recipe["id"]))
for ship in ships:
for material in ship["schematic"]["materials"]:
consumed.setdefault(material["item"], []).append(
"ship '{}'".format(ship["id"]))
for module in modules:
for material in module["materials"]:
consumed.setdefault(material["item"], []).append(
"module '{}'".format(module["id"]))
all_items = set(produced) | set(consumed) | WORLD_SOURCED_ITEMS
visual_items = set(visuals.get("items", {}))
errors = []
warnings = []
for item in sorted(consumed):
if item not in produced and item not in WORLD_SOURCED_ITEMS:
errors.append("no producer for '{}' (consumed by {})".format(
item, ", ".join(sorted(set(consumed[item])))))
for item in sorted(all_items - visual_items):
errors.append("no [items.{}] entry in visuals.toml".format(item))
for item in sorted(visual_items - all_items):
warnings.append("visuals.toml entry [items.{}] matches no known item"
.format(item))
for item in sorted(produced):
if item not in consumed and item not in IMPLICITLY_CONSUMED_ITEMS:
warnings.append("'{}' is produced but never consumed (by {})"
.format(item, ", ".join(sorted(set(produced[item])))))
reprocessing_only = sorted(
item for item, producers in produced.items()
if all("reprocessing" in p for p in producers))
print("{} items, {} recipes, {} ships, {} modules".format(
len(all_items), len(recipes), len(ships), len(modules)))
print("obtainable only via reprocessing: {}".format(
", ".join(reprocessing_only) if reprocessing_only else "(none)"))
print()
for warning in warnings:
print("WARNING: {}".format(warning))
for error in errors:
print("ERROR: {}".format(error))
if not errors and not warnings:
print("all checks passed")
elif not errors:
print("no errors")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())