Basic working with ships partly implimented

This commit is contained in:
2026-01-07 23:11:46 -07:00
parent 1ec24fbf12
commit 6e914467ed
49 changed files with 2079 additions and 220 deletions
+163
View File
@@ -0,0 +1,163 @@
# Nathan Hinton 2 Jan 2025 Buildings for the game
#
# Production buildings:
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 2:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Space Construction Station Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Space Construction Station Level 2:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Research Factory Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Research Factory Level 2:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
+400
View File
@@ -0,0 +1,400 @@
# frozen_string_literal: true
# Nathan Hinton, 22 Dec 2025
# Game server for the space game
require 'game/player'
require 'game/planet'
require 'thread'
require 'securerandom'
# Main file for the Game module
module Game
# Controls how frequently autosaves are created (seconds)
SAVE_INTERVAL = 15
# Controls how frequently updates are sent to the client about the player (seconds)
PLAYER_UPDATE_INTERVAL = 2
# Controls how fast the game scales with player count. Bigger means more
# players required to change the speed.
GAME_SPEED_FACTOR = 1
##
# The central server object that keeps track of players, planets, and
# game-time.
#
# @api public
class Game
# The name of the game
attr_reader :name
# Returns the width of the game space
attr_reader :width
# Returns the height of the game space
attr_reader :height
# Returns the depth of the game space
attr_reader :depth
# Collection of players in the game
attr_reader :players
# Create the game
##
# Initialize the game from a configuration hash. The hash is expected
# to be the result of parsing the YAML file `game_data/game_conf.yaml`.
#
# @param conf_data [Hash] The parsed configuration
# @raise [ArgumentError] if the configuration is missing a required key
#
def initialize(conf_data)
# Validate that the correct keys exist
['name', 'width', 'height', 'depth', 'players', 'planets', 'ships', 'time', 'player_defaults'].each do |key|
if conf_data[key].nil?
raise ArgumentError, "Missing toplevel key '#{key}' in configuration"
end
end
@conf_data = conf_data
@name = conf_data['name']
@width = conf_data['width']
@height = conf_data['height']
@depth = conf_data['depth']
@to_load_players = conf_data['players']
@to_load_planets = conf_data['planets']
@game_time = conf_data['time']
@player_defaults = @conf_data['player_defaults']
@connected_players = [] # When we start up we do not have any players connected.
# Load the players:
@players = []
@to_load_players.each do |player_id|
@players.append(Player.load_from_save(player_id))
end
# If we are an integer then we need to create new planets.
@planets = []
if @to_load_planets.is_a? Integer
puts "Appears to be the first time running. Creating #{@to_load_planets} planets"
@to_load_planets.times do
@planets.append(Planet.new(@width, @height, @depth))
end
else
@to_load_planets.length.times do |idx|
planet = Planet.load_from_save(@to_load_planets[idx])
@planets.append(planet)
end
end
# Load ships
@ships = []
conf_data['ships'].each do
require 'byebug'; debugger
end
end
##
# Register a client connection. This method sccepts a TCP docket and a hash
# that must comtain a `player_id`. If the player already exists in the game
# it is reconnected otherwise a new `Player` is created and added to the
# game if possible (There must be at least one free planet)
#
# @param client [TCPSocket] The client socket
# @param command [Hash] JSON parsed command from the client.
# @option command [String] :player_id Required, The player ID
#
# @return [Hash] status hash that will be converted to JSON before being
# sent back to the client
#
def player_connect(client, command)
player_id = command.dig('payload', 'player_id')
faction = command.dig('payload', 'faction')
raise ArgumentError, 'player_id missing in command' unless player_id
raise ArgumentError, 'faction is missing in command' unless faction
player = player_registered?(player_id)
if player
# Re-connect an existing but disconnected player
if player.connected?
raise GameError, 'Player already connected!'
end
player.connect(client)
else
# Create a brand new player and claim a planet for them
player = create_player(player_id, faction)
return player if player.is_a?(Hash) && player['type'] == 'error' # error returned
player.connect(client)
@players << player
end
{ 'type' => 'update', 'payload' => 'success' }
end
##
# Create a new player from scratch. The new player is assigned a free
# planet and receives the default starting resources.
#
# @param player_id [String] the unique identifier that will be stored
# on the new player; if `nil` a random hex string will be generated
# @param faction [String] The faction that the player belongs too.
#
# @return [Player, Hash] The new player object on success or an error
# hash if no free planet is available.
#
def create_player(player_id, faction)
free_planets = @planets.select { |p| p.owner.nil? }
if free_planets.empty?
return {
'type' => 'error',
'payload' => 'Unable to allocate a planet for a new player'
}
end
home = free_planets.sample
player = Player.new(player_id || SecureRandom.hex, faction)
player.create_new(
@player_defaults['credits'],
@player_defaults['metal'],
@player_defaults['crystals'],
@player_defaults['fuel'],
)
player.claim_planet(home)
player.make_home_planet(home)
player
end
##
# The entry point for all messages that arrive from a client. The
# payload must be a hash decoded from JSON and contain a `type` field.
#
# @param command [Hash] The parsed client command
#
# @return [String] JSON-encoded response
#
def parse_command(command)
return error_response('Player not registered!') unless command['player_id']
case command['type']
when 'dbg'
# Debugger - do nothing in production
{ 'type' => 'ok', 'payload' => 'debugging' }.to_json
when 'hello'
error_response('Hello command should already have been handled.')
when 'query'
parse_query(command)
when 'command'
error_response('Command handling not implemented')
else
error_response('Unknown type')
end
end
##
# Dispatch a `query` command to the appropriate helper. Only a small
# subset of query types is required for the test-suite; all others
# return a generic error.
#
# @param query [Hash] The query to be parsed
#
# @return [String] JSON-encoded response
#
def parse_query(query)
return error_response('Missing query payload') unless query['payload']
player = player_registered?(query['player_id'])
return error_response('Player not found') unless player
case query['domain']
when 'player'
case query['payload']
when 'update'
return player.update(query['payload']['request_id'])
else
error_response('Unknown payload for player domain')
end
else
error_response('Unknown domain')
end
end
##
# Return a status hash that contains a friendly error message. The
# helper exists only to keep the body of all error branches short.
#
# @param msg [String] The error message
# @return [String] JSON-encoded error response
#
def error_response(msg)
{ 'type' => 'error', 'payload' => msg }.to_json
end
alias error_response error_response
##
# Return a JSON object that tells a client about the current
# server-time. Useful for debugging or “ping” operations.
#
# @return [String] JSON-encoded hash
#
def info(request_id)
{
'type' => 'update',
'domain' => 'game',
'request_id' => request_id,
'payload' => {
'name' => @name,
'width' => @width,
'height' => @height,
'depth' => @depth,
'time' => @game_time
}
}.to_json
end
# Actually start running the game
def run()
# Main game loop
last_time = Time.now # The delta time will be very small on the first one.
next_save = @game_time + SAVE_INTERVAL
next_client_update = @game_time + PLAYER_UPDATE_INTERVAL
while true
# This is the **ONLY** place where Time.now should be used
delta_time = (Time.now - last_time)
last_time = Time.now
# Here is where we do our time scaling. Count the number of players in the game then use that mult
player_count = @players.map {|player| player.connected?}.count true
@game_speed = (player_count / GAME_SPEED_FACTOR)
delta_time * (player_count / GAME_SPEED_FACTOR)
@game_time += delta_time
@planets.each do |planet|
planet.update(delta_time)
end
@ships.each do |ship|
ship.update(delta_time)
end
#@players.each do |player|
# player.update(delta_time)
#end
# Perform game saves (Or not for now...)
if (@game_time) > next_save
puts 'Trying to save...'
next_save = @game_time + SAVE_INTERVAL
save_game_state
end
# Send out regular updates to the clients (Every 2 seconds)
if @game_time > next_client_update
next_client_update = @game_time + PLAYER_UPDATE_INTERVAL
@players.each do |player|
if player.connected?
player.send_update
end
end
end
if player_count == 0
puts 'No players connected, pausing time'
while 0 == (@players.map {|player| player.connected?}.count true)
sleep(1)
end
end
#sleep(0.05) # About 20 TPS/FPS
sleep(1)
end
end
##
# Persist the current state of the game to the disk. The method is
# fully unit-tested via a double that captures the YAML payload
# instead of actually writing a file.
#
# @return [void]
#
def save_game_state
config = {
'name' => @name,
'width' => @width,
'height' => @height,
'depth' => @depth,
'players' => @players.map(&:player_id),
'planets' => @planets.map(&:planet_id),
'ships' => @ships.map(&:ship_id),
'time' => @game_time,
'player_defaults' => @player_defaults
}
File.open('game_data/game_conf.yaml', 'w') { |f| f.write(YAML.dump(config)) }
# Save the planets
@planets.each do |planet|
planet.save_to_file
end
# Save the ships
@ships.each do |ship|
ship.save_to_file
end
# Save the players
@players.each do |player|
player.save_to_file
end
end
##
# Determine whether a player with a given id is known to the game.
#
# @param player_id [String] the id that the caller is looking for
#
# @return [Player, false] the `Player` instance if found or `false`
# otherwise
#
def player_registered?(player_id)
@players.find { |p| p.player_id == player_id } || false
end
# --------------------------------------------------------------------
# PRIVATE HELPERS - kept simple for the test-suite
# --------------------------------------------------------------------
private
def error_response(msg)
{ 'type' => 'error', 'payload' => msg }.to_json
end
def load_players(ids)
ids.map { |id| Player.load_from_id(id) }
end
def load_planets(planets)
if planets.is_a?(Integer) # legacy config - integer means “create N”
Array.new(planets) { Planet.new }
else
planets.map { |id| Planet.load_from_id(id) }
end
end
def load_ships(ids)
ids.map { |id| Ship.load_from_id(id) } # Ship is a placeholder
end
end
##
# Raised when an invalid operation is attempted (e.g. reconnecting a
# player that already has a socket). The tests catch this error
# to confirm that the server protects against accidental double
# connections.
class GameError < StandardError; end
end
+40
View File
@@ -0,0 +1,40 @@
# Nathan Hinton 31 Dec 2025
# This holds a list of all modules in the game
# Here is how to create a module:
#
# name:
# classification:
# auxiluary/attack/defense # Pick one
# faction:
# good/evil
# production:
# metal/crystals/fuel
# upkeep:
# metal/crystals/fuel
# add_stats: # These are additive to the base stat
# stat_name: value
# mult_stats: # These are multipulicative to the base stat (After adding the additive stats?)
# stat_name: value # Greater than one will increase while less than one will decrease
# required_tech: [] # The required tech to be researched *before* this one can be started
# required_buildings: [] # The buildings required to begin this research
#
#
#
light shield:
classification: defense
faction: good
production:
metal: 1,000
crystals: 1,000
fuel: 0
upkeep:
metal: 10
crustals: 10
fuel: 0
add_stats:
shield: 1,000
mult_stats: []
required_tech: []
required_buildings: []
+75
View File
@@ -0,0 +1,75 @@
# Nathan Hinton 22 Dec 2025
# Planet file
require 'securerandom'
# A planet object for the Game module
class Planet
attr_reader :owner, :planet_id
# Creates a new planet.
#
# @param width [Integer] The width of the game
# @param height [Integer] The height of the game
# @param depth [Integer] The depth of the game
def initialize(width, height, depth)
@owner = nil
@buildings = []
@position = [rand(width), rand(height), rand(depth)]
@planet_id = SecureRandom.hex
end
# Returns if the planet can be claimed. Currently requires that there be no
# buildings on the planet.
def claimable?
if @owner.nil?
return true
elsif @buildings == []
return true
end
return false
end
# Function that allows a player to claim the planet and to become the owner.
def claim(player)
@owner = player
end
# Function called to update the planet.
def update(delta_time)
end
# Function to return info to the client about a planet
def info_update
raise NotImplementedError, "Need to write tests for this function"
return {
'id' => @planet_id,
'position' => @position
}
end
##########################
##### UTIL FUNCTIONS #####
##########################
# Save a player to a file
def save_to_file
File.open("game_data/#{self.planet_id}_planet_.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self))
end
return self.planet_id
end
# Load a player from a file
def self.load_from_save(fname)
File.open("game_data/#{fname}_planet_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read)
end
end
end
+156
View File
@@ -0,0 +1,156 @@
# Nathan Hinton 22 Dec 2025
# The main player file
require 'securerandom'
require 'yaml'
# A player object for the class Game
class Player
attr_reader :player_resources, :player_id, :player_planets, :player_ships, :faction, :researched_tech, :buildings
def initialize(player_id, faction)
@player_id = player_id
@player_planets = []
@player_ships = []
@home_planet = nil
# Validate faction:
if !(['good', 'evil'].include? faction)
raise PlayerError, 'Faction must be `good` or `evil`'
end
@faction = faction
end
# Create a new player. This is actually just setting it up. All the defaults
# are defined in the game_config.yaml file.
#
# @param credits [Integer] The number of starting credits
# @param metal [Integer] The amount of starting metal
# @param crystals [Integer] The number of starting crystals
# @param fuel [Integer] The amount of fuel to start with.
# @param faction [String] The faction that the player should start as. Should be `good` or `evil`
def create_new(credits, metal, crystals, fuel)
@player_resources = {
'credits' => credits,
'metal' => metal,
'crystals' => crystals,
'fuel' => fuel
}
end
# Connect a player
#
# @param client [TCPSocket] The socket used to communicate with the player
def connect(client)
@socket = client
end
# Returns if the player is connected or not
def connected?
if @socket.nil?
return false
elsif @socket.closed?
return false
end
return true
end
# Allows a player to claim a planet.
#
# @param planet
def claim_planet(planet)
if planet.claimable?
@player_planets.append(planet)
planet.claim(self)
end
end
# Makes a planet the player's home planet
#
# @param planet
def make_home_planet(planet)
if planet.owner == self
@home_planet = planet
else
return {'type' => 'error',
'payload' => 'Planet must be owned by the player'}
end
end
# Create an update to send out
def update(request_id)
planet_data = []
@player_planets.each do |planet|
planet_data.append(planet.info_update())
end
{
'type' => 'update',
'domain' => 'player',
'request_id' => request_id,
'payload' => {
'resources' => @player_resources,
'planets' => planet_data,
'ships' => @player_ships
}
}
end
# Used to send regular updates for the game data.
def send_update
# Send an update of all of the player data and stuff
return if !connected?
begin
@socket.puts(update(nil).to_json)
rescue
@socket.close
end
end
# Save a player to a file through marshaling it.
def save_to_file
File.open("game_data/#{@player_id}_player_.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self))
end
return @player_id
end
# Load a player from a file. Requires the player id to load with as that is
# the file name.
def self.load_from_save(player_id)
File.open("game_data/#{player_id}_player_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read)
end
end
# Custom funciton to dump marshal data for players. We do not want to try to
# dump the socket...
def marshal_dump
{
player_id: @player_id,
player_planets: @player_planets,
player_ships: @player_ships,
home_planet: @home_planet,
faction: @faction,
player_resources: @player_resources
}
end
# Custom function to load the marshaled data for the player.
#
# @param data [Hash] A hash containing the data.
def marshal_load(data)
@player_id = data[:player_id]
@player_planets = data[:player_planets]
@player_ships = data[:player_ships]
@home_planet = data[:home_planet]
@faction = data[:faction]
@player_resources = data[:player_resources]
end
end
# Errors for the player
class PlayerError < StandardError
end
+146
View File
@@ -0,0 +1,146 @@
# Nathan Hinton 30 Dec 2025
# Ship file for the game
require 'securerandom'
# Load the ships into a constant:
require 'yaml'
SHIP_DATA = File.open('lib/game/ships.yaml', 'r') do |fi|
YAML.safe_load(fi.read())
end
class Ship
attr_reader :owner, :position
# Ships are built by a player so we should *always* know what player is
# owning a ship
#
# @param player [Player] The player who owns the ship
# @param position_x [Float] The starting x position of the ship.
# @param position_y [Float] The starting y position of the ship.
# @param position_z [Float] The starting z position of the ship.
def initialize(player, position_x, position_y, position_z, name)
@owner = player
@ship_id = SecureRandom.hex
@position = [position_x, position_y, position_z]
@target = @position
@speed = 1
@ship_info = validate_creation(name)
end
# Validates that the ship name exists and it is unlocked by the player (Has
# the right buildings and tech to actually create). This means that you might
# be able to start a ship however when it is 'finished' if a required
# building has been removed it will fail to finish properly. In this case the
# player resources should be refunded (with some lost) and the ship creation
# will raise an error.
def validate_creation(name)
ship = SHIP_DATA[name]
raise ShipError, "invalid ship name '#{name}'" if ship.nil?
# Validate that the owner has the correct tech:
ship['required_tech'].each do |tech_needed|
if !(@owner.researched_tech.include? tech_needed)
raise ShipError, "Player needs the tech of '#{tech_needed}'"
end
end
# Validate the buildings:
ship['required_buildings'].each do |building_needed|
if !(@owner.buildings.include? building_needed)
raise ShipError, "Player needs the building of '#{building_needed}' to build this ship"
end
end
end
# Teleport the ship to a given position. Really only should be used for admin
# stuff.
#
# @param position_x [Float] The new x position of the ship.
# @param position_y [Float] The new y position of the ship.
# @param position_z [Float] The new z position of the ship.
def teleport(position_x, position_y, position_z)
@position = [position_x, position_y, position_z]
end
# Sets the target position of a ship.
#
# @param position_x [Float] The new x position of the ship.
# @param position_y [Float] The new y position of the ship.
# @param position_z [Float] The new z position of the ship.
def move(position_x, position_y, position_z)
@target = [position_x, position_y, position_z]
end
# Contains the logic to move a ship
#
# @param delta_time [Float] The delta time elapsed in the game
def update(delta_time)
# Movement logic:
if @position == @target
# Do nothing
else
# Calculate the difference of the position and the target
x_dist = @target[0] - @position[0]
y_dist = @target[1] - @position[1]
z_dist = @target[2] - @position[2]
total_dist = Math.sqrt(x_dist**2 + y_dist**2 + z_dist**2)
speed = (@speed/total_dist) * delta_time
new_x = @position[0] + (x_dist * speed)
new_y = @position[1] + (y_dist * speed)
new_z = @position[2] + (z_dist * speed)
if x_dist > 0 # Going right
if new_x > @target[0]
new_x = @target[0]
end
elsif x_dist < 0 # Going left
if new_x < @target[0]
new_x = @target[0]
end
end
if y_dist > 0 # Going right
if new_y > @target[1]
new_y = @target[1]
end
elsif y_dist < 0 # Going left
if new_y < @target[1]
new_y = @target[1]
end
end
if z_dist > 0 # Going right
if new_z > @target[2]
new_z = @target[2]
end
elsif z_dist < 0 # Going left
if new_z < @target[2]
new_z = @target[2]
end
end
@position = [new_x, new_y, new_z]
end
end
# Save a player to a file
def save_to_file
File.open("game_data/#{self.planet_id}.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self))
end
return self.planet_id
end
# Load a player from a file
def self.load_from_save(fname)
File.open("game_data/#{fname}_ship_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read)
end
end
end
# Error class for ships
class ShipError < StandardError
end
+235
View File
@@ -0,0 +1,235 @@
# Nathan Hinton 31 Dec 2025
# This file will hold configuration for ships in the game.
# Here is how to create a ship:
#
# name:
# classification:
# flagship/battleship/heavy cruiser/light cruiser/destroyer/frigate/fighter # Pick one
# faction:
# good/evil
# production:
# metal/crystals/fuel/etc
# build_time: A value that represents how long it takes to build. Standard shipyards will build at a rate of 100 per tick. This means that a value of 1000 would take (for one shipyard) 10 ticks to produce.
# upkeep:
# metal/crystals/fuel/etc
# base_stats:
# hp/shield
# armor/def
# att
# speed # Units per tick
# total_module_slots: integer # The total slots available on this ship
# module_slots: [] # List of module names found from modules.yaml
# required_tech: [] # List of all tech required to build the ship
# required_buildings: # List of all buildings required to build the ship
#
#
#
#
##########################################
############## ##############
############## GOOD FACTION ##############
############## ##############
##########################################
#
Nebula Dominion:
classification: flagship
faction: good
production:
metal: 100,000
crystals: 100,000
fuel: 5,000
time: 10,000 # In production power
upkeep:
metal: 100
crystals: 100
fuel: 150
base_stats:
hp: 10,000
shield: 10,000
armor: 50
def: 50
att: 1,000
speed: 0.2
total_module_slots: 12
module_slots: []
required_tech: [Heavy Shield, Large Engine, Laser Cannons, Neoplated Armor]
required_buildings: [Space Construction Station]
Void Breaker:
classification: battleship
faction: good
production:
metal: 70,000
crystals: 70,000
fuel: 3,500
time: 7,000
upkeep:
metal: 70
crystals: 70
fuel: 100
base_stats:
hp: 7,000
shield: 7,000
armor: 35
def: 35
att: 700
speed: 0.26
total_module_slots: 8
module_slots: []
required_tech: []
required_buildings: []
Solar Sentinal:
classification: heavy cruiser
faction: good
production:
metal: 49,000
crystals: 49,000
fuel: 2,500
time: 4,900
upkeep:
metal: 49
crystals: 49
fuel: 73
base_stats:
hp: 4,900
shield: 4,900
armor: 25
def: 25
att: 490
speed: 0.34
total_module_slots: 6
module_slots: []
required_tech: []
required_buildings: []
Photon Protector:
classification: light cruiser
faction: good
production:
metal: 34,000
crystals: 34,000
fuel: 1,750
time: 3,430
upkeep:
metal: 34
crystals: 34
fuel: 51
base_stats:
hp: 3,400
shield: 3,400
armor: 18
def: 18
att: 343
speed: 0.44
total_module_slots: 4
module_slots: []
required_tech: []
required_buildings: []
Third Smallest:
classification: destroyer
faction: good
production:
metal: 23,800
crystals: 23,800
fuel: 1225
time: 2,380
upkeep:
metal: 24
crystals: 24
fuel: 36
base_stats:
hp: 2,380
shield: 2,380
armor: 12
def: 12
att: 240
speed: 0.57
total_module_slots: 3
module_slots: []
required_tech: []
required_buildings: []
Starlight:
classification: frigate
faction: good
production:
metal: 17,000
crystals: 17,000
fuel: 858
time: 1,700
upkeep:
metal: 17
crystals: 17
fuel: 25
base_stats:
hp: 1,700
shield: 1,700
armor: 8
def: 8
att: 168
speed: 0.74
total_module_slots: 2
module_slots: []
required_tech: []
required_buildings: []
Starblade:
classification: fighter
faction: good
production:
metal: 11,900
crystals: 11,900
fuel: 600
time: 1,100
upkeep:
metal: 17
crystals: 17
fuel: 25
base_stats:
hp: 1,190
shield: 1,190
armor: 8
def: 8
att: 168
speed: 0.74
total_module_slots: 1
module_slots: []
required_tech: [Engine_1, Body_1, Weapons_1]
required_buildings: []
#
##########################################
############## ##############
############## EVIL FACTION ##############
############## ##############
##########################################
#
Void Bastion:
classification: flagship
faction: evil
production:
metal: 100,000
crystals: 100,000
fuel: 5,000
time: 1,000 # In game ticks
upkeep:
metal: 100
crystals: 100
fuel: 150
base_stats:
hp: 10,000
shield: 10,000
armor: 50
def: 50
att: 1,000
total_module_slots:
module_slots: []
required_tech: [Plastered glass]
required_buildings: []