Files
dota_factory/modular_ships.md
2026-05-27 17:03:13 +02:00

4.3 KiB

Modular Ships: Remove Ship Roles, Unify Capabilities as Modules

Why

Ships currently have a fixed role (combat, salvage, repair) baked into their definition. This limits ship customization — a ship is either a fighter or a salvage ship, never both. By moving weapon, salvage cargo, and repair tool capabilities into the module system, players can freely compose ship loadouts. A single hull can carry two weapons and a repair tool, or a weapon and a salvage bay, etc.

What Changes

Ship definitions lose role-specific sections

ShipDef drops std::optional<ShipCombat>, std::optional<ShipSalvage>, std::optional<ShipRepair>. Ships define only hull stats (HP, movement, sensor) and a layout grid. A new default_modules list is added per schematic for enemy wave ships (see below).

Capability modules replace roles

New module types in modules.toml provide capabilities. A module with base stat formulas (e.g. damage_formula) under a capability section ([module.weapon], [module.salvage], [module.repair]) is a capability module that creates a child entity. A module with only added_*/multiplied_* formulas is a passive module that modifies stats.

Example capability module:

[[module]]
id = "laser_turret"
[module.weapon]
damage_formula = "5 + 2*x"       # x = module's player_production_level
attack_range_formula = "8 + x"
attack_rate_formula = "1.5 + 0.1*x"

Example passive module boosting weapons:

[[module]]
id = "weapon_upgrade"
[module.weapon]
multiplied_damage_formula = "1.0 + 0.15 * x"

Example passive module boosting ship stats:

[[module]]
id = "armor_plate"
[module.health]
multiplied_hp_formula = "1.0 + 0.2 * x"

Capability modules become child entities

Each placed capability module instance becomes its own entt entity with a ModuleOwnerComponent { entt::entity ship } linking it to the parent ship. This allows multiple instances of the same type (e.g. three weapons, each with independent stats, cooldown, and target).

A new ModuleOwnerComponent is introduced:

struct ModuleOwnerComponent
{
    entt::entity ship;
};

Passive modifiers apply to both ship and module entities

During spawn, passive module modifiers are collected and routed by category:

  • [module.health], [module.movement], [module.sensor] modifiers apply to the ship entity's hull stats.
  • [module.weapon] modifiers apply to every weapon child entity on the ship.
  • [module.repair] modifiers apply to every repair child entity on the ship.
  • [module.salvage] modifiers apply to every salvage child entity on the ship.

Capability module child entities must be created first, then passive modifiers are applied. The formula variable x is always the module's player_production_level.

Behavior components stay on the ship entity

ThreatResponseBehaviorComponent, SalvageBehaviorComponent, RepairBehaviorComponent remain on the ship entity (they drive movement). They are attached if the ship has at least one module of the corresponding type.

Hybrid ships are allowed

A ship may have modules of different capability types. Movement arbitration currently uses last-writer-wins (the last behavior system ticked sets the intent). This is acceptable for now; dynamic priority-based arbitration will be added later.

Systems query module entities

Weapon, repair, and salvage tick systems query for their component + ModuleOwnerComponent and resolve position from the owner ship. Each module instance ticks independently.

Despawn cleans up child entities

ShipSystem::despawn destroys the ship entity and all module entities whose ModuleOwnerComponent::ship matches it.

Enemy wave ships use default modules

Since weapons are now modules, enemy ships need modules to fight. Each ship schematic in ships.toml defines a default_modules list (same format as layout blueprints). Wave-spawned enemy ships are instantiated with this module layout. If default_modules is absent or empty, the ship spawns with no modules (and therefore no combat/salvage/repair capability).

Visuals use per-schematic colors instead of per-role

visuals.toml defines fill/outline colors and glyphs per ship schematic (e.g. fighter, sniper, gunship) rather than per role (combat, salvage, repair). Debug draw sensor circles use the schematic's outline color.