Compare commits

..

2 Commits

8 changed files with 1535 additions and 187 deletions

View File

@@ -1,122 +1,36 @@
[[module]] # modules.toml
id = "armor_plate" #
unlock_at_station_level = -1 # First real-content iteration: module ids and surface masks are the designed
surface_mask = ["OO"] # content; stats, materials, and threat costs are placeholders until the
materials = [{item = "armor_plate_module", amount = 1}] # recipe and balancing passes.
player_production_level = 1 #
production_time_seconds = 3 # Surface mask footprint ladder — footprints gate which hulls can mount a
fill_color = "#808080" # module, purely through geometry (see ships.toml for the matching hull
glyph = "A" # grids):
#
[module.health] # 1x1 laser_cannon_s, salvager, repair_tool fits every hull, incl. drones
added_hp_formula = "40" # 1x2 maneuvering_thrusters, sensor_booster,
# armor_plates frigate and up
# 1x3 afterburner frigate and up (eats most of a frigate)
# L-shape weapon_stabilizer, weapon_primer,
# weapon_upgrade frigate and up
# 2x2 laser_cannon_m, drone_bay cruiser and up (no 2x2 area on s hulls)
# 3x3 laser_cannon_l battleship and up (no 3x3 area on m hulls)
# 2x6 drone_hangar carrier only
# -----------------------------------------------------------------------------
# Weapons
# -----------------------------------------------------------------------------
[[module]] [[module]]
id = "sensor_booster" id = "laser_cannon_s"
unlock_at_station_level = -1 unlock_at_station_level = -1
surface_mask = ["O"] surface_mask = ["O"]
materials = [{item = "sensor_booster_module", amount = 1}] materials = [{item = "laser_cannon_s_module", amount = 1}]
player_production_level = 1
production_time_seconds = 2
fill_color = "#40A0FF"
glyph = "S"
[module.sensor]
added_sensor_range_m_formula = "50"
[[module]]
id = "manuvering_thrusters"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "manuvering_thrusters_module", amount = 1}]
player_production_level = 1
production_time_seconds = 2
fill_color = "#40A0FF"
glyph = "Mt"
[module.movement]
multiplied_speed_mps_formula = "1.2"
added_maneuvering_acceleration_mpss_formula = "10"
[[module]]
id = "afterburner"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "afterburner_module", amount = 1}]
player_production_level = 1
production_time_seconds = 2
fill_color = "#40A0FF"
glyph = "Ab"
[module.movement]
multiplied_speed_mps_formula = "1.6"
added_main_acceleration_mpss_formula = "60"
[[module]]
id = "weapon_upgrade"
unlock_at_station_level = -1
surface_mask = [
"OO",
"O ",
]
materials = [{item = "weapon_upgrade_module", amount = 1}]
player_production_level = 1
production_time_seconds = 4
fill_color = "#FF4040"
glyph = "Wu"
[module.weapon]
multiplied_damage_formula = "1.2"
[[module]]
id = "weapon_primer"
unlock_at_station_level = -1
surface_mask = [
"OO",
"O ",
]
materials = [{item = "weapon_primer_module", amount = 1}]
player_production_level = 1
production_time_seconds = 4
fill_color = "#FF4040"
glyph = "Wp"
[module.weapon]
multiplied_attack_rate_hz_formula = "1.2"
[[module]]
id = "weapon_stabilizer"
unlock_at_station_level = -1
surface_mask = [
"OO",
"O ",
]
materials = [{item = "weapon_stabilizer_module", amount = 1}]
player_production_level = 1
production_time_seconds = 4
fill_color = "#FF4040"
glyph = "Ws"
[module.weapon]
multiplied_attack_range_m_formula = "1.5"
multiplied_attack_rate_hz_formula = "0.8"
[[module]]
id = "laser_cannon_xs"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ore", amount = 1}]
player_production_level = 1 player_production_level = 1
production_time_seconds = 0.5 production_time_seconds = 0.5
fill_color = "#FF8040" fill_color = "#FF8040"
glyph = "L" glyph = "Ls"
[module.weapon] [module.weapon]
damage_formula = "2" damage_formula = "2"
@@ -125,16 +39,16 @@ attack_rate_hz_formula = "2.0"
[[module]] [[module]]
id = "laser_cannon_s" id = "laser_cannon_m"
unlock_at_station_level = -1 unlock_at_station_level = -1
surface_mask = [ surface_mask = [
"OO", "OO",
"OO"] "OO"]
materials = [{item = "laser_cannon_s_module", amount = 1}] materials = [{item = "laser_cannon_m_module", amount = 1}]
player_production_level = 1 player_production_level = 1
production_time_seconds = 0.5 production_time_seconds = 2
fill_color = "#FF8040" fill_color = "#FF8040"
glyph = "L" glyph = "Lm"
[module.weapon] [module.weapon]
damage_formula = "10" damage_formula = "10"
@@ -142,6 +56,28 @@ attack_range_m_formula = "70"
attack_rate_hz_formula = "1.5" attack_rate_hz_formula = "1.5"
[[module]]
id = "laser_cannon_l"
unlock_at_station_level = -1
surface_mask = [
"OOO",
"OOO",
"OOO"]
materials = [{item = "laser_cannon_l_module", amount = 1}]
player_production_level = 1
production_time_seconds = 8
fill_color = "#FF8040"
glyph = "Ll"
[module.weapon]
damage_formula = "40"
attack_range_m_formula = "100"
attack_rate_hz_formula = "0.8"
# -----------------------------------------------------------------------------
# Utility tools
# -----------------------------------------------------------------------------
[[module]] [[module]]
id = "salvager" id = "salvager"
unlock_at_station_level = -1 unlock_at_station_level = -1
@@ -171,3 +107,154 @@ glyph = "Rp"
[module.repair] [module.repair]
repair_rate_hz_formula = "5 + x" repair_rate_hz_formula = "5 + x"
repair_range_m_formula = "800" repair_range_m_formula = "800"
# -----------------------------------------------------------------------------
# Propulsion
# -----------------------------------------------------------------------------
[[module]]
id = "afterburner"
unlock_at_station_level = -1
surface_mask = ["OOO"]
materials = [{item = "afterburner_module", amount = 1}]
player_production_level = 1
production_time_seconds = 2
fill_color = "#40A0FF"
glyph = "Ab"
[module.movement]
multiplied_speed_mps_formula = "1.6"
added_main_acceleration_mpss_formula = "60"
[[module]]
id = "maneuvering_thrusters"
unlock_at_station_level = -1
surface_mask = ["OO"]
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
player_production_level = 1
production_time_seconds = 2
fill_color = "#40A0FF"
glyph = "Mt"
[module.movement]
multiplied_speed_mps_formula = "1.2"
added_maneuvering_acceleration_mpss_formula = "10"
# -----------------------------------------------------------------------------
# Defense & sensors
# -----------------------------------------------------------------------------
[[module]]
id = "armor_plates"
unlock_at_station_level = -1
surface_mask = ["OO"]
materials = [{item = "armor_plates_module", amount = 1}]
player_production_level = 1
production_time_seconds = 3
fill_color = "#808080"
glyph = "A"
[module.health]
added_hp_formula = "40"
[[module]]
id = "sensor_booster"
unlock_at_station_level = -1
surface_mask = ["OO"]
materials = [{item = "sensor_booster_module", amount = 1}]
player_production_level = 1
production_time_seconds = 2
fill_color = "#40A0FF"
glyph = "S"
[module.sensor]
added_sensor_range_m_formula = "50"
# -----------------------------------------------------------------------------
# Weapon modifiers
# -----------------------------------------------------------------------------
[[module]]
id = "weapon_upgrade"
unlock_at_station_level = -1
surface_mask = [
"OO",
"OX",
]
materials = [{item = "weapon_upgrade_module", amount = 1}]
player_production_level = 1
production_time_seconds = 4
fill_color = "#FF4040"
glyph = "Wu"
[module.weapon]
multiplied_damage_formula = "1.2"
[[module]]
id = "weapon_primer"
unlock_at_station_level = -1
surface_mask = [
"OO",
"OX",
]
materials = [{item = "weapon_primer_module", amount = 1}]
player_production_level = 1
production_time_seconds = 4
fill_color = "#FF4040"
glyph = "Wp"
[module.weapon]
multiplied_attack_rate_hz_formula = "1.2"
[[module]]
id = "weapon_stabilizer"
unlock_at_station_level = -1
surface_mask = [
"OO",
"OX",
]
materials = [{item = "weapon_stabilizer_module", amount = 1}]
player_production_level = 1
production_time_seconds = 4
fill_color = "#FF4040"
glyph = "Ws"
[module.weapon]
multiplied_attack_range_m_formula = "1.5"
multiplied_attack_rate_hz_formula = "0.8"
# -----------------------------------------------------------------------------
# Drone modules
#
# Footprint-only placeholders: the drone launching capability is not
# implemented yet, so these modules define no capability section.
# -----------------------------------------------------------------------------
[[module]]
id = "drone_bay"
unlock_at_station_level = -1
surface_mask = [
"OO",
"OO"]
materials = [{item = "drone_bay_module", amount = 1}]
player_production_level = 1
production_time_seconds = 5
fill_color = "#CC66FF"
glyph = "Db"
[[module]]
id = "drone_hangar"
unlock_at_station_level = -1
surface_mask = [
"OOOOOO",
"OOOOOO"]
materials = [{item = "drone_hangar_module", amount = 1}]
player_production_level = 1
production_time_seconds = 20
fill_color = "#9933CC"
glyph = "Dh"

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

@@ -1,8 +1,25 @@
# ships.toml
#
# First real-content iteration: ship ids and layout grids are the designed
# content; stats, materials, and production times are placeholders until the
# recipe and balancing passes.
#
# Size classes:
# xs drone 1 cell — exactly one 1x1 module
# s frigate, destroyer no 2x2 area anywhere: only 1x1/1x2/1x3/L modules fit
# m cruiser, battlecruiser 2x2 areas (m guns, drone bays) but no 3x3 area
# l battleship four m guns, or exactly one 3x3 l gun at heavy
# opportunity cost
# xl dreadnought, carrier dreadnought fits three l guns but no drone
# hangar; carrier fits one drone hangar (2x6)
# but no l gun (its deck rows are broken up
# by elevator shafts)
[[ship]] [[ship]]
id = "drone" id = "drone"
unlock_at_station_level = -1 unlock_at_station_level = -1
layout = ["O"] layout = ["O"]
default_modules = [{type = "laser_cannon_xs", x = 0, y = 0, rotation = "east"}] default_modules = [{type = "laser_cannon_s", x = 0, y = 0, rotation = "east"}]
[ship.schematic] [ship.schematic]
materials = [{item = "iron_ore", amount = 1}] materials = [{item = "iron_ore", amount = 1}]
@@ -24,3 +41,255 @@ sensor_range_m_formula = "150"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2
# Frigate — 5 cells in a plus shape. Holds a couple of small guns plus at
# most one 1x2 support (every 1x2 placement crosses the center cell), or one
# L-shaped weapon modifier, or an afterburner spanning the full center line.
[[ship]]
id = "frigate"
unlock_at_station_level = -1
layout = [
"XOX",
"OOO",
"XOX",
]
[ship.schematic]
materials = [{item = "frigate_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 10
[ship.health]
hp_formula = "30"
[ship.movement]
speed_mps_formula = "30"
main_acceleration_mpss_formula = "50"
maneuvering_acceleration_mpss_formula = "25"
angular_acceleration_radpss_formula = "8"
max_rotation_speed_radps_formula = "4"
[ship.sensor]
sensor_range_m_formula = "200"
[ship.loot]
scrap_drop = 5
# Destroyer — 8 cells: a long gun deck with three turret bumps on top.
# Still no 2x2 area, so it packs more small guns than a frigate but can never
# mount medium hardware.
[[ship]]
id = "destroyer"
unlock_at_station_level = -1
layout = [
"OXOXO",
"OOOOO",
]
[ship.schematic]
materials = [{item = "destroyer_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 15
[ship.health]
hp_formula = "50"
[ship.movement]
speed_mps_formula = "25"
main_acceleration_mpss_formula = "40"
maneuvering_acceleration_mpss_formula = "20"
angular_acceleration_radpss_formula = "6"
max_rotation_speed_radps_formula = "3"
[ship.sensor]
sensor_range_m_formula = "220"
[ship.loot]
scrap_drop = 8
# Cruiser — 12 cells with notched corners. Fits at most two 2x2 m guns
# (stacked through the middle), leaving the four side cells for small
# supports; no 3x3 area exists for an l gun.
[[ship]]
id = "cruiser"
unlock_at_station_level = -1
layout = [
"XOOX",
"OOOO",
"OOOO",
"XOOX",
]
[ship.schematic]
materials = [{item = "cruiser_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 25
[ship.health]
hp_formula = "120"
[ship.movement]
speed_mps_formula = "20"
main_acceleration_mpss_formula = "30"
maneuvering_acceleration_mpss_formula = "15"
angular_acceleration_radpss_formula = "4"
max_rotation_speed_radps_formula = "2"
[ship.sensor]
sensor_range_m_formula = "250"
[ship.loot]
scrap_drop = 15
# Battlecruiser — 16 cells: a wide bow split into two gun cheeks, tapering
# toward the stern. Fits three 2x2 m guns (two in the cheeks, one through
# the middle) with small support slots left over; the split bow and tapered
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
[[ship]]
id = "battlecruiser"
unlock_at_station_level = -1
layout = [
"OOXXOO",
"OOOOOO",
"XOOOOX",
"XXOOXX",
]
[ship.schematic]
materials = [{item = "battlecruiser_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 35
[ship.health]
hp_formula = "180"
[ship.movement]
speed_mps_formula = "18"
main_acceleration_mpss_formula = "25"
maneuvering_acceleration_mpss_formula = "12"
angular_acceleration_radpss_formula = "3"
max_rotation_speed_radps_formula = "1.5"
[ship.sensor]
sensor_range_m_formula = "260"
[ship.loot]
scrap_drop = 20
# Battleship — 24 cells: a broadside hull with notched flanks on every other
# row. Fits four 2x2 m guns (two per gun deck) with the bow, stern, and flank
# cells left for supports. All 3x3 placements crowd the center columns, so at
# most ONE l gun fits — and mounting it blocks every m gun mount, leaving
# only narrow support strips. The notched rows are never adjacent-and-full,
# so no 2x6 drone hangar fits.
[[ship]]
id = "battleship"
unlock_at_station_level = -1
layout = [
"XOOOOX",
"OOOOOO",
"XOOOOX",
"OOOOOO",
"XOOOOX",
]
[ship.schematic]
materials = [{item = "battleship_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 60
[ship.health]
hp_formula = "350"
[ship.movement]
speed_mps_formula = "14"
main_acceleration_mpss_formula = "18"
maneuvering_acceleration_mpss_formula = "8"
angular_acceleration_radpss_formula = "2"
max_rotation_speed_radps_formula = "1"
[ship.sensor]
sensor_range_m_formula = "280"
[ship.loot]
scrap_drop = 35
# Dreadnought — 36 cells: the main battery deck is split into three 3x3 gun
# slots by structural spacer columns, so exactly three l guns fit side by
# side (or m guns / supports in unused slots). The spacers cap every
# horizontal run at 5 cells, so the 2x6 drone hangar can never fit — carriers
# stay the only hangar hull. Bow and stern strips hold supports.
[[ship]]
id = "dreadnought"
unlock_at_station_level = -1
layout = [
"XXXOOOOOXXX",
"OOOXOOOXOOO",
"OOOXOOOXOOO",
"OOOXOOOXOOO",
"XXOOXXXOOXX",
]
[ship.schematic]
materials = [{item = "dreadnought_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 120
[ship.health]
hp_formula = "800"
[ship.movement]
speed_mps_formula = "8"
main_acceleration_mpss_formula = "10"
maneuvering_acceleration_mpss_formula = "5"
angular_acceleration_radpss_formula = "1"
max_rotation_speed_radps_formula = "0.5"
[ship.sensor]
sensor_range_m_formula = "300"
[ship.loot]
scrap_drop = 60
# Carrier — 37 cells: the top flight deck (rows 0-1) is the only place wide
# enough for the 2x6 drone hangar, and exactly one fits. The middle deck row
# is broken up by elevator shafts (the X cells) so no 3x3 l gun can ever fit;
# the lower decks hold supports and 2x2 point-defense m guns.
[[ship]]
id = "carrier"
unlock_at_station_level = -1
layout = [
"XOOOOOOOOX",
"OOOOOOOOOO",
"OOXOOXOOXO",
"XOOOOOOOOX",
"XXXOOOOXXX",
]
[ship.schematic]
materials = [{item = "carrier_hull", amount = 1}]
player_production_level = 1
production_time_seconds = 120
[ship.health]
hp_formula = "700"
[ship.movement]
speed_mps_formula = "9"
main_acceleration_mpss_formula = "10"
maneuvering_acceleration_mpss_formula = "5"
angular_acceleration_radpss_formula = "1"
max_rotation_speed_radps_formula = "0.5"
[ship.sensor]
sensor_range_m_formula = "350"
[ship.loot]
scrap_drop = 60

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,30 +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 ---
[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]
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"
@@ -154,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
# #
@@ -164,6 +292,34 @@ outline = "#689275"
fill = "#3366ff" fill = "#3366ff"
outline = "#ffffff" outline = "#ffffff"
[ships.frigate]
fill = "#44aaff"
outline = "#ffffff"
[ships.destroyer]
fill = "#33ccaa"
outline = "#ffffff"
[ships.cruiser]
fill = "#66cc33"
outline = "#ffffff"
[ships.battlecruiser]
fill = "#cccc33"
outline = "#ffffff"
[ships.battleship]
fill = "#ff9933"
outline = "#ffffff"
[ships.dreadnought]
fill = "#ff5533"
outline = "#ffffff"
[ships.carrier]
fill = "#cc66ff"
outline = "#ffffff"
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Laser beams (REQ-SHP-FIRING-BEAM) # Laser beams (REQ-SHP-FIRING-BEAM)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -12,7 +12,7 @@ enemy_buffer_width_tiles = 10
level = 1 level = 1
count = 5 count = 5
modules = [ modules = [
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"}, {type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
] ]
[[arena.team]] [[arena.team]]
@@ -22,7 +22,7 @@ enemy_buffer_width_tiles = 10
level = 1 level = 1
count = 2 count = 2
modules = [ modules = [
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"}, {type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
{type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"}, {type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"},
{type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"}, {type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"},
{type = "weapon_upgrade", x = 1, y = 1, rotation = "east"}, {type = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
@@ -44,7 +44,7 @@ enemy_buffer_width_tiles = 10
level = 1 level = 1
count = 5 count = 5
modules = [ modules = [
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"}, {type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
] ]
[[arena.team]] [[arena.team]]
@@ -54,7 +54,7 @@ enemy_buffer_width_tiles = 10
level = 1 level = 1
count = 3 count = 3
modules = [ modules = [
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"}, {type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
] ]
[[arena.team.ship]] [[arena.team.ship]]
@@ -79,7 +79,7 @@ enemy_buffer_width_tiles = 15
level = 1 level = 1
count = 3 count = 3
modules = [ modules = [
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"}, {type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
] ]
[[arena.team.station]] [[arena.team.station]]
type = "player_station" type = "player_station"
@@ -99,5 +99,5 @@ enemy_buffer_width_tiles = 15
level = 1 level = 1
count = 8 count = 8
modules = [ modules = [
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"}, {type = "laser_cannon_s", x = 1, y = 1, rotation = "east"},
] ]

194
docs/content_design.md Normal file
View File

@@ -0,0 +1,194 @@
# Content Design — Ships & Modules
First real-content iterations (June 2026). Pass 1 defined ship hull grids and
module surface masks; pass 2 defined the production tree (recipes). Stats and
threat costs in the config files are still placeholders for the balancing
pass.
## Design principle: footprint gating
Which module fits on which hull is controlled purely by geometry — no
explicit allow-lists. Each hull grid is shaped so that it physically cannot
contain the footprint of modules from a larger size class. This keeps the
rules transparent to the player ("it doesn't fit because there is no room")
and makes them trivially moddable through the config files alone.
### Module footprint ladder
| Footprint | Modules | Smallest hull that fits it |
|-----------|---------|----------------------------|
| 1x1 | laser_cannon_s, salvager, repair_tool | drone |
| 1x2 | maneuvering_thrusters, sensor_booster, armor_plates | frigate |
| 1x3 | afterburner | frigate (eats most of it) |
| L-shape (3 cells) | weapon_stabilizer, weapon_primer, weapon_upgrade | frigate |
| 2x2 | laser_cannon_m, drone_bay | cruiser |
| 3x3 | laser_cannon_l | battleship |
| 2x6 | drone_hangar | carrier (only) |
### Hull grids
`O` = buildable cell, `X` = hull structure (not buildable).
**drone (xs, 1 cell)** — exactly one 1x1 module: a small gun, a salvager, or
a repair tool. This is what makes drone roles swappable.
O
**frigate (s, 5 cells)** — plus shape. Every 1x2 placement crosses the center
cell, so at most ONE 1x2 support fits; alternatively one L-shaped weapon
modifier or one afterburner through the center line. Gun-boat with one or two
support modules, as intended.
XOX
OOO
XOX
**destroyer (s, 8 cells)** — gun deck with three turret bumps. More cells
than the frigate (more small guns), but still no 2x2 area anywhere, so medium
hardware can never be mounted.
OXOXO
OOOOO
**cruiser (m, 12 cells)** — notched corners. Fits at most two 2x2 m guns
(stacked through the middle), leaving the side cells for supports. No 3x3
area.
XOOX
OOOO
OOOO
XOOX
**battlecruiser (m, 16 cells)** — split bow with two gun cheeks, tapered
stern. Fits three 2x2 m guns — one more than the cruiser — with small support
slots left over. The bow split and stern taper prevent any 3x3 area (no l
gun) and any 2x6 area (no drone hangar).
OOXXOO
OOOOOO
XOOOOX
XXOOXX
**battleship (l, 24 cells)** — broadside hull with notched flanks on every
other row. Fits four 2x2 m guns (two per gun deck) — one more than the
battlecruiser — with bow, stern, and flank cells for supports. All 3x3
placements crowd the center columns, so at most ONE l gun fits: mounted
center it blocks every m gun mount (pure support strips remain), mounted
offset it still allows two m guns. The notched rows are never adjacent-and-
full, so no 2x6 drone hangar fits.
XOOOOX
OOOOOO
XOOOOX
OOOOOO
XOOOOX
**dreadnought (xl, 36 cells)** — the main battery deck is split into three
3x3 gun slots by structural spacer columns, so exactly three l guns fit side
by side (or m guns / supports in unused slots), plus bow/stern strips for
supports. The spacers cap every horizontal run at 5 cells, so the 2x6 drone
hangar can never fit — the carrier stays the only hangar hull.
XXXOOOOOXXX
OOOXOOOXOOO
OOOXOOOXOOO
OOOXOOOXOOO
XXOOXXXOOXX
**carrier (xl, 37 cells)** — the top flight deck (rows 01) is the only
region wide enough for the 2x6 drone hangar, and exactly one fits. The middle
deck row is broken up by elevator shafts (X cells placed so every 3-column
window hits one), which is what prevents any 3x3 l gun from ever fitting.
Lower decks hold supports and 2x2 point-defense m guns.
XOOOOOOOOX
OOOOOOOOOO
OOXOOXOOXO
XOOOOOOOOX
XXXOOOOXXX
### Verified gating matrix
Checked programmatically against the configs (all four mask rotations,
all placements) with `tools/verify_layouts.py` — re-run it after editing
layout grids or surface masks:
python dota_factory/tools/verify_layouts.py
| Footprint | drone | frigate | destroyer | cruiser | battlecruiser | battleship | dreadnought | carrier |
|-----------|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 1x1 | x | x | x | x | x | x | x | x |
| 1x2 | | x | x | x | x | x | x | x |
| 1x3 | | x | x | x | x | x | x | x |
| L-shape | | x | x | x | x | x | x | x |
| 2x2 | | | | x | x | x | x | x |
| 3x3 | | | | | | x | x | |
| 2x6 | | | | | | | | x |
Maximum simultaneous (disjoint) placements: m guns — cruiser 2,
battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3;
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
- All new hulls have `threat.cost_formula = "0"` so enemy waves do not spawn
them yet (WaveSystem treats any ship with positive threat cost as wave-
eligible, regardless of unlock level). The balancing pass should set real
threat costs together with `default_modules` loadouts so waves spawn them
armed.
- All new hulls and all assembler recipes are `unlock_at_station_level = -1`
(available from the start) to make testing easy; the balancing pass should
stagger these so mid/lategame recipes drop as schematics from enemy defence
stations.
- Recipe quantities and durations are a first guess, deliberately roughly
tiered (capital hulls ~60 s, drones 4 s); the balancing pass tunes them.
- `drone_bay` and `drone_hangar` are footprint-only placeholders: the drone
launching capability does not exist in the simulation yet, so they define
no capability section.
- Renames in this pass: `laser_cannon_xs``laser_cannon_s` (the old 2x2
`laser_cannon_s` became `laser_cannon_m`), `armor_plate``armor_plates`,
`manuvering_thrusters``maneuvering_thrusters` (typo fix). Test data
under `bin/test/data/config` intentionally still uses the old ids — it is
an independent fixture set.

170
tools/verify_layouts.py Normal file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Verify which module surface masks fit which ship layout grids.
Reads ships.toml and modules.toml, then checks every module footprint
against every ship layout the same way the game does (all four mask
rotations, every placement position). Prints three reports:
1. Fit matrix — can the footprint be placed on the hull at all?
2. Max simultaneous — how many disjoint copies of a footprint fit at
once (only computed for footprints of 4+ cells;
smaller ones would be slow and uninteresting).
3. Cell counts — buildable cells per hull.
Use it after editing layout grids or surface masks to confirm the
footprint-gating rules in docs/content_design.md still hold, e.g. that
no 2x2 area exists on s-class hulls or that the drone hangar fits the
carrier only.
Usage (from the repository root or anywhere else):
python dota_factory/tools/verify_layouts.py
python dota_factory/tools/verify_layouts.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.
The script is informational only — it always exits 0. Read the matrix
and compare it against the intended gating in docs/content_design.md.
"""
import argparse
import os
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 grid_cells(rows):
"""Set of (x, y) for every 'O' cell in a list of layout/mask strings."""
return {(x, y)
for y, row in enumerate(rows)
for x, ch in enumerate(row)
if ch == "O"}
def rotate_cw(shape):
height = max(y for x, y in shape) + 1
return {(height - 1 - y, x) for x, y in shape}
def normalize(shape):
min_x = min(x for x, y in shape)
min_y = min(y for x, y in shape)
return frozenset((x - min_x, y - min_y) for x, y in shape)
def orientations(shape):
"""All distinct 90-degree rotations of a shape, normalized to (0, 0)."""
result = []
current = shape
for _ in range(4):
norm = normalize(current)
if norm not in result:
result.append(norm)
current = rotate_cw(current)
return result
def placements(layout, shape):
"""Every position (in any rotation) where shape fits fully on layout."""
found = []
layout_w = max(x for x, y in layout) + 1
layout_h = max(y for x, y in layout) + 1
for orient in orientations(shape):
shape_w = max(x for x, y in orient) + 1
shape_h = max(y for x, y in orient) + 1
for off_x in range(layout_w - shape_w + 1):
for off_y in range(layout_h - shape_h + 1):
cells = frozenset((x + off_x, y + off_y) for x, y in orient)
if cells <= layout and cells not in found:
found.append(cells)
return found
def max_disjoint(layout, shape):
"""Maximum number of non-overlapping placements of shape on layout."""
options = placements(layout, shape)
best = [0]
def recurse(start_index, used, count):
if count > best[0]:
best[0] = count
for i in range(start_index, len(options)):
if not (options[i] & used):
recurse(i + 1, used | options[i], count + 1)
recurse(0, frozenset(), 0)
return best[0]
def shape_label(shape):
width = max(x for x, y in shape) + 1
height = max(y for x, y in shape) + 1
if len(shape) == width * height:
return "{}x{}".format(width, height)
return "{}x{}-{}c".format(width, height, len(shape))
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 module surface masks against ship layout grids.")
parser.add_argument("--config-dir", default=default_dir,
help="directory containing ships.toml and modules.toml"
" (default: %(default)s)")
args = parser.parse_args()
ships = load_toml(os.path.join(args.config_dir, "ships.toml"))["ship"]
modules = load_toml(os.path.join(args.config_dir, "modules.toml"))["module"]
layouts = [(s["id"], grid_cells(s["layout"])) for s in ships]
# Group modules that share the same footprint (up to rotation).
footprint_modules = {} # canonical shape -> [module ids]
for module in modules:
shape = grid_cells(module["surface_mask"])
canonical = min(orientations(shape), key=sorted)
footprint_modules.setdefault(canonical, []).append(module["id"])
footprints = sorted(footprint_modules.items(), key=lambda e: len(e[0]))
column_header = "".join("{:>8}".format(ship_id[:7])
for ship_id, _ in layouts)
print("Fit matrix (YES = at least one placement exists)")
print("{:48}{}".format("footprint (modules)", column_header))
for shape, module_ids in footprints:
name = "{:8}{}".format(shape_label(shape), ", ".join(module_ids))
row = "".join("{:>8}".format("YES" if placements(layout, shape) else "-")
for _, layout in layouts)
print("{:48}{}".format(name[:47], row))
print()
print("Max simultaneous (disjoint) placements, footprints of 4+ cells")
print("{:48}{}".format("footprint (modules)", column_header))
for shape, module_ids in footprints:
if len(shape) < 4:
continue
name = "{:8}{}".format(shape_label(shape), ", ".join(module_ids))
row = "".join("{:>8}".format(max_disjoint(layout, shape))
for _, layout in layouts)
print("{:48}{}".format(name[:47], row))
print()
print("Buildable cells per hull")
for ship_id, layout in layouts:
print("{:16}{}".format(ship_id, len(layout)))
if __name__ == "__main__":
main()

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())