Node = {
id: number, -- Unique identifier (1-indexed)
x: number, -- Map X coordinate
z: number, -- Map Z coordinate
color: Color3?, -- Optional color
connections: {number},-- Array of connected node IDs
deleted: boolean?, -- True if node was removed
}TeamData = {
name: string, -- "Red", "Blue", "Green", "Yellow", "Neutral"
color: Color3, -- Team color for rendering
ownedNodes: {number}, -- Array of owned node IDs
spawnNodeId: number?, -- Capitol/spawn node ID
}MovingTroop = {
id: string, -- Unique ID: "{team}_{fromId}_{toId}_{timestamp}"
team: string, -- Owning team name
troopCount: number, -- Number of troops
fromNode: Node, -- Current segment start
toNode: Node, -- Current segment end
progress: number, -- 0-1 along current segment
remainingPath: {Node}, -- Nodes after toNode
originNodeId: number, -- Where troops started (for retreat)
finalDestId: number, -- Ultimate destination
speed: number, -- Units per second
}Building = {
type: string, -- "Spawn", "Factory", "Powerplant"
owner: string, -- Current owning team
nodeId: number, -- Node this building is on
}BuildingType = {
name: string,
productionRate: number, -- Troops per second
productionBoost: number?, -- Additive bonus to connected factories
cost: number, -- Troops required to build
color: Color3,
size: Vector3,
}-- Node ownership: nodeId (as string) → team name
NodeOwnership: {[string]: string}
-- Example: { ["1"] = "Red", ["5"] = "Blue" }
-- Per-team troops on each node
NodeTroops: {[string]: {[string]: number}}
-- Example: { ["3"] = { Red = 50, Blue = 30 }, ["7"] = { Neutral = 25 } }
-- Eliminated teams
EliminatedTeams: {[string]: boolean}
-- Example: { Blue = true }-- Buildings by node
NodeBuildings: {[string]: Building}
-- Example: { ["1"] = { type = "Spawn", owner = "Red", nodeId = 1 } }State = {
Seed: number,
Random: Random,
IsGenerated: boolean,
NodeCount: number,
Nodes: {Node}, -- Array indexed by node ID
NodesByChunk: {[string]: {Node}}, -- Spatial partitioning
}-- Tracked nodes for updates
CreatedNodes: {[number]: {part: Part, label: TextLabel}}
-- Roads (recreated each frame)
CreatedRoads: {Frame}MapData = {
nodes: {{id, x, z, color?}},
roads: {{from: {x, z}, to: {x, z}}},
teamData: {
nodeOwnership: {[nodeIdStr]: teamName},
nodeTroops: {[nodeIdStr]: {[teamName]: count}},
spawns: {Red: nodeId?, Blue: nodeId?, ...}
},
movingTroops: {SerializedTroop},
buildings: {[nodeIdStr]: {type, owner}}
}SerializedTroop = {
id: string,
team: string,
troopCount: number, -- Floored for display
fromNode: {x, z}, -- Coordinates, not full node
toNode: {x, z},
remainingPath: {{x, z}},
progress: number,
finalDestId: number,
}BatchData = {
[EventType]: {data, data, ...}
}
-- Example: { MapUpdate = {mapData1}, TroopMovement = {troop1, troop2} }-
Owned Nodes (distance 0)
- Full info: ownership, troops, buildings
- Can select and command from
-
Info-Visible Nodes (distance 1)
- Full info: ownership, troops, buildings
- See structure and troop counts
-
Ghost Nodes (distance 2-4)
- Structure only: position, roads
- No troop/ownership data
- Darker grey color, hidden billboard
-
Hidden Nodes (distance 5+)
- Not sent to client at all
-- In SerializeForPlayer:
visibleNodeIds = {} -- All nodes player can see (structure)
infoVisibleNodeIds = {} -- Nodes with full info (owned + 1 hop)
-- BFS from owned nodes:
for nodeId in ownedNodes:
distance[nodeId] = 0
visibleNodeIds[nodeId] = true
infoVisibleNodeIds[nodeId] = true
while queue not empty:
if current.distance < FogOfWarDistance:
for neighbor in node.connections:
visibleNodeIds[neighbor] = true
if current.distance == 0:
infoVisibleNodeIds[neighbor] = trueCOMBAT_DAMAGE_RATE = 0.1 -- Damage per second per enemy troop
-- Each frame on contested nodes:
for each team:
enemyTroops = totalTroops - team.count
damage = enemyTroops * COMBAT_DAMAGE_RATE * deltaTime
team.count = max(0, team.count - damage)-- Troops moving opposite directions on same road:
if troopA.fromNode == troopB.toNode and troopA.toNode == troopB.fromNode:
if close enough and approaching:
-- Smaller army retreats to origin
if troopA.count < troopB.count:
reverse(troopA)
elif troopB.count < troopA.count:
reverse(troopB)
else:
reverse(both)PRODUCTION_INTERVAL = 1.0 -- Seconds between production ticks
ProductionTimer = 0
function UpdateProduction(deltaTime):
ProductionTimer += deltaTime
if ProductionTimer < PRODUCTION_INTERVAL:
return
ProductionTimer -= PRODUCTION_INTERVAL
-- Process all buildings...function GetPowerplantBoost(nodeId):
boost = 0
for connectedId in node.connections:
if NodeBuildings[connectedId].type == "Powerplant":
boost += 20
return boost
-- Applied to Factory and Spawn production:
productionRate = baseRate + GetPowerplantBoost(nodeId)| Constant | Value | Location |
|---|---|---|
COMBAT_DAMAGE_RATE |
0.1 | ServerModule |
PRODUCTION_INTERVAL |
1.0 sec | Buildings |
FogOfWarDistance |
4 hops | MapGeneration |
BASE_SPEED |
0.5 units/sec | TroopConfig |
MIN_SPEED |
0.2 units/sec | TroopConfig |
ExpansionMultiplier |
5 | MapRenderer |
MinDistance (nodes) |
0.65 | MapGeneration |
MaxDistance (nodes) |
2 | MapGeneration |
| Team | RGB | Usage |
|---|---|---|
| Red | (255, 85, 85) | Nodes, troops, text |
| Blue | (85, 170, 255) | Nodes, troops, text |
| Green | (85, 255, 127) | Nodes, troops, text |
| Yellow | (255, 255, 127) | Nodes, troops, text |
| Neutral | (180, 180, 180) | Unowned nodes |
| Contested | (128, 128, 128) | Multi-team nodes |
| Ghost | (100, 100, 100) | Fog of war nodes |