Basic working with ships partly implimented
This commit is contained in:
@@ -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: []
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
Reference in New Issue
Block a user