diff --git a/i-mic-rts.rb b/i-mic-rts.rb index af25eb3..9a62256 100755 --- a/i-mic-rts.rb +++ b/i-mic-rts.rb @@ -11,6 +11,8 @@ require "nokogiri" require "json" require "socket" +require_relative "lib/exts/string" + require_relative "lib/version" require_relative "lib/errors" require_relative "lib/window" @@ -40,9 +42,10 @@ require_relative "lib/order" require_relative "lib/friendly_hash" require_relative "lib/director" require_relative "lib/player" -require_relative "lib/entity_controller" +require_relative "lib/tool" require_relative "lib/connection" + require_relative "lib/networking/protocol" require_relative "lib/networking/packet" require_relative "lib/networking/server" diff --git a/lib/component.rb b/lib/component.rb index d2f68ee..cec2908 100644 --- a/lib/component.rb +++ b/lib/component.rb @@ -7,7 +7,7 @@ class IMICRTS end def self.inherited(klass) - name = klass.to_s.split("::").last.gsub(/([^A-Z])([A-Z]+)/,'\1_\2').downcase.to_sym + name = klass.to_s.to_snakecase if get(name) raise "#{klass.inspect} is already defined!" diff --git a/lib/components/sidebar_actions.rb b/lib/components/sidebar_actions.rb index de71bae..1d7d468 100644 --- a/lib/components/sidebar_actions.rb +++ b/lib/components/sidebar_actions.rb @@ -9,28 +9,28 @@ class IMICRTS @actions = [] end - def add(type, *args) + def add(type, data = {}) action = Action.new case type when :add_to_build_queue - ent = IMICRTS::Entity.get(args.first) - raise "Failed to find entity: #{args.first.inspect}" unless ent + ent = IMICRTS::Entity.get(data[:entity]) + raise "Failed to find entity: #{data[:entity].inspect}" unless ent action.label = ent.name.to_s.split("_").map{ |s| s.capitalize }.join(" ") action.description = "Cost: #{ent.cost}\n#{ent.description}" - action.block = proc { @parent.component(:build_queue).add(args.first) } + action.block = proc { @parent.component(:build_queue).add(data[:entity]) } - when :set_build_tool - ent = IMICRTS::Entity.get(args[1]) - raise "Failed to find entity: #{args[1].inspect}" unless ent + when :set_tool + ent = IMICRTS::Entity.get(data[:entity]) + raise "Failed to find entity: #{data[:entity].inspect}" unless ent action.label = ent.name.to_s.split("_").map { |s| s.capitalize }.join(" ") action.description = "Cost: #{ent.cost}\n#{ent.description}" - action.block = proc { @parent.director.game.set_tool(:building, ent) } + action.block = proc { @parent.director.game.set_tool(data[:tool], data) } else - raise "Unhandled sidebar action: #{action.inspect}" + raise "Unhandled sidebar action: #{type.inspect}" end @actions << action diff --git a/lib/director.rb b/lib/director.rb index 8a4bdcc..e8d7aa7 100644 --- a/lib/director.rb +++ b/lib/director.rb @@ -1,7 +1,7 @@ class IMICRTS class Director attr_reader :current_tick, :map, :game - def initialize(game:, map:, players:, networking_mode:, tick_rate: 10) + def initialize(game:, map:, players: [], networking_mode:, tick_rate: 10) @game = game @map = map @players = players @@ -14,6 +14,10 @@ class IMICRTS @current_tick = 0 end + def add_player(player) + @players << player + end + def update if (Gosu.milliseconds - @last_tick_at) >= @tick_time @last_tick_at = Gosu.milliseconds diff --git a/lib/entities/buildings/construction_yard.rb b/lib/entities/buildings/construction_yard.rb index 357b008..67db116 100644 --- a/lib/entities/buildings/construction_yard.rb +++ b/lib/entities/buildings/construction_yard.rb @@ -1,7 +1,7 @@ IMICRTS::Entity.define_entity(:construction_yard, :building, 2_000, "Provides radar and builds construction workers") do |entity| entity.has(:build_queue) entity.has(:sidebar_actions) - entity.component(:sidebar_actions).add(:add_to_build_queue, :construction_worker) + entity.component(:sidebar_actions).add(:add_to_build_queue, {entity: :construction_worker}) entity.radius = 40 entity.max_health = 100.0 diff --git a/lib/entities/buildings/helipad.rb b/lib/entities/buildings/helipad.rb index 521fded..ab1100a 100644 --- a/lib/entities/buildings/helipad.rb +++ b/lib/entities/buildings/helipad.rb @@ -1,7 +1,7 @@ IMICRTS::Entity.define_entity(:helipad, :building, 1_000, "Builds and rearms helicopters") do |entity| entity.has(:build_queue) entity.has(:sidebar_actions) - entity.component(:sidebar_actions).add(:add_to_build_queue, :helicopter) + entity.component(:sidebar_actions).add(:add_to_build_queue, {entity: :helicopter}) entity.radius = 26 entity.max_health = 100.0 diff --git a/lib/entities/buildings/war_factory.rb b/lib/entities/buildings/war_factory.rb index fe861f5..53fdb73 100644 --- a/lib/entities/buildings/war_factory.rb +++ b/lib/entities/buildings/war_factory.rb @@ -1,9 +1,9 @@ IMICRTS::Entity.define_entity(:war_factory, :building, 2_000, "Builds units") do |entity| entity.has(:build_queue) entity.has(:sidebar_actions) - entity.component(:sidebar_actions).add(:add_to_build_queue, :jeep) - entity.component(:sidebar_actions).add(:add_to_build_queue, :tank) - entity.component(:sidebar_actions).add(:add_to_build_queue, :harvester) + entity.component(:sidebar_actions).add(:add_to_build_queue, {entity: :jeep}) + entity.component(:sidebar_actions).add(:add_to_build_queue, {entity: :tank}) + entity.component(:sidebar_actions).add(:add_to_build_queue, {entity: :harvester}) entity.radius = 48 entity.max_health = 100.0 diff --git a/lib/entities/units/construction_worker.rb b/lib/entities/units/construction_worker.rb index c2e506a..aa98702 100644 --- a/lib/entities/units/construction_worker.rb +++ b/lib/entities/units/construction_worker.rb @@ -3,7 +3,7 @@ IMICRTS::Entity.define_entity(:construction_worker, :unit, 1000, "Constructs bui entity.has(:build_queue) entity.has(:sidebar_actions) [:power_plant, :refinery, :barracks, :war_factory, :helipad, :construction_yard].each do |ent| - entity.component(:sidebar_actions).add(:set_build_tool, :place_building, ent) + entity.component(:sidebar_actions).add(:set_tool, {tool: :place_entity, entity: ent, construction_worker: entity}) end entity.radius = 14 diff --git a/lib/entity.rb b/lib/entity.rb index 1652934..6994b35 100644 --- a/lib/entity.rb +++ b/lib/entity.rb @@ -18,7 +18,7 @@ class IMICRTS attr_reader :director, :player, :id, :name, :type, :speed attr_accessor :position, :angle, :radius, :target, :state, :movement, :health, :max_health, - :turret, :center, :particle_emitters + :turret, :center, :particle_emitters, :color def initialize(name:, player:, id:, position:, angle:, director:) @player = player @id = id @@ -26,6 +26,7 @@ class IMICRTS @angle = angle @director = director @speed = 0.5 + @color = Gosu::Color.rgba(255, 255, 255, 255) @sight_radius = 5 # tiles @range_radius = 3 # tiles @@ -104,6 +105,14 @@ class IMICRTS @position.distance(vector) < @radius + 1 end + def die? + if @health + @health <= 0 + else + false + end + end + def render @render = Gosu.render(@shell_image.width, @shell_image.height, retro: true) do @body_image.draw(0, 0, 0) if @body_image @@ -114,7 +123,7 @@ class IMICRTS def draw render unless @render - @render.draw_rot(@position.x, @position.y, @position.z, @angle, @center.x, @center.y) + @render.draw_rot(@position.x, @position.y, @position.z, @angle, @center.x, @center.y, 1, 1, @color) component(:turret).draw if component(:turret) @particle_emitters.each(&:draw) diff --git a/lib/exts/string.rb b/lib/exts/string.rb new file mode 100644 index 0000000..f8692a8 --- /dev/null +++ b/lib/exts/string.rb @@ -0,0 +1,5 @@ +class String + def to_snakecase + self.to_s.split("::").last.gsub(/([^A-Z])([A-Z]+)/,'\1_\2').downcase.to_sym + end +end \ No newline at end of file diff --git a/lib/player.rb b/lib/player.rb index a24b27a..653416c 100644 --- a/lib/player.rb +++ b/lib/player.rb @@ -1,9 +1,10 @@ class IMICRTS class Player - attr_reader :id, :name, :color, :team, :entities, :orders, :camera + attr_reader :id, :name, :color, :team, :entities, :orders, :camera, :spawnpoint attr_reader :selected_entities - def initialize(id:, name: nil, color: Gosu::Color.rgb(rand(150..200), rand(100..200), rand(150..200)), team: nil) + def initialize(id:, spawnpoint:, name: nil, color: Gosu::Color.rgb(rand(150..200), rand(100..200), rand(150..200)), team: nil) @id = id + @spawnpoint = spawnpoint @name = name ? name : "Novice-#{id}" @color = color @team = team diff --git a/lib/states/game.rb b/lib/states/game.rb index 2969539..1d3bacb 100644 --- a/lib/states/game.rb +++ b/lib/states/game.rb @@ -3,14 +3,17 @@ class IMICRTS Overlay = Struct.new(:image, :position, :angle, :alpha) attr_reader :sidebar, :sidebar_actions, :overlays + attr_accessor :selected_entities def setup window.show_cursor = true @options[:networking_mode] ||= :host - @player = Player.new(id: 0) - @director = Director.new(game: self, map: Map.new(map_file: "maps/test_map.tmx"), networking_mode: @options[:networking_mode], players: [@player]) - @entity_controller = EntityController.new(game: self, director: @director, player: @player) + @director = Director.new(game: self, map: Map.new(map_file: "maps/test_map.tmx"), networking_mode: @options[:networking_mode]) + @player = Player.new(id: 0, spawnpoint: @director.map.spawnpoints.last) + @director.add_player(@player) + @selected_entities = [] + @tool = set_tool(:entity_controller) @overlays = [] @debug_info = CyberarmEngine::Text.new("", y: 10, z: Float::INFINITY, shadow_color: Gosu::Color.rgba(0, 0, 0, 200)) @@ -20,38 +23,8 @@ class IMICRTS label "SIDEBAR", text_size: 78, margin_bottom: 20 - flow(width: 1.0) do + flow(width: 1.0, height: 1.0) do @sidebar_actions = stack(width: 0.9) do - button("Harvester", width: 1.0) do - @player.entities << Entity.new( - name: :harvester, - director: @director, - player: @player, - id: @player.next_entity_id, - position: CyberarmEngine::Vector.new(rand(window.width), rand(window.height), ZOrder::GROUND_VEHICLE), - angle: rand(360) - ) - end - button("Construction Worker", width: 1.0) do - @player.entities << Entity.new( - name: :construction_worker, - director: @director, - player: @player, - id: @player.next_entity_id, - position: CyberarmEngine::Vector.new(rand(window.width), rand(window.height), ZOrder::GROUND_VEHICLE), - angle: rand(360) - ) - end - button("Tank", width: 1.0) do - @player.entities << Entity.new( - name: :tank, - director: @director, - player: @player, - id: @player.next_entity_id, - position: CyberarmEngine::Vector.new(rand(window.width), rand(window.height), ZOrder::GROUND_VEHICLE), - angle: rand(360) - ) - end end # Power meter @@ -59,49 +32,17 @@ class IMICRTS background Gosu::Color::GREEN end end - - - button("Leave", width: 1.0, margin_top: 20) do - finalize - push_state(MainMenu) - end end - # 100.times { |i| [@c, @h, @t].sample.instance_variable_get("@block").call } - spawnpoint = @director.map.spawnpoints.last + # TODO: implement tools @director.spawn_entity( player_id: @player.id, name: :construction_yard, - position: CyberarmEngine::Vector.new(spawnpoint.x, spawnpoint.y, ZOrder::BUILDING) + position: CyberarmEngine::Vector.new(@player.spawnpoint.x, @player.spawnpoint.y, ZOrder::BUILDING) ) @director.spawn_entity( player_id: @player.id, name: :construction_worker, - position: CyberarmEngine::Vector.new(spawnpoint.x - 64, spawnpoint.y + 64, ZOrder::GROUND_VEHICLE) - ) - - @director.spawn_entity( - player_id: @player.id, name: :power_plant, - position: CyberarmEngine::Vector.new(spawnpoint.x + 64, spawnpoint.y + 64, ZOrder::BUILDING) - ) - - @director.spawn_entity( - player_id: @player.id, name: :refinery, - position: CyberarmEngine::Vector.new(spawnpoint.x + 130, spawnpoint.y + 64, ZOrder::BUILDING) - ) - - @director.spawn_entity( - player_id: @player.id, name: :war_factory, - position: CyberarmEngine::Vector.new(spawnpoint.x + 130, spawnpoint.y - 64, ZOrder::BUILDING) - ) - - @director.spawn_entity( - player_id: @player.id, name: :helipad, - position: CyberarmEngine::Vector.new(spawnpoint.x - 32, spawnpoint.y - 96, ZOrder::BUILDING) - ) - - @director.spawn_entity( - player_id: @player.id, name: :barracks, - position: CyberarmEngine::Vector.new(spawnpoint.x - 32, spawnpoint.y + 128, ZOrder::BUILDING) + position: CyberarmEngine::Vector.new(@player.spawnpoint.x - 64, @player.spawnpoint.y + 64, ZOrder::GROUND_VEHICLE) ) end @@ -113,7 +54,7 @@ class IMICRTS @player.camera.draw do @director.map.draw(@player.camera) @director.entities.each(&:draw) - @entity_controller.selected_entities.each(&:selected_draw) + @selected_entities.each(&:selected_draw) @overlays.each do |overlay| overlay.image.draw_rot(overlay.position.x, overlay.position.y, overlay.position.z, overlay.angle, 0.5, 0.5, 1, 1, Gosu::Color.rgba(255, 255, 255, overlay.alpha)) @@ -123,7 +64,7 @@ class IMICRTS @overlays.delete(overlay) if overlay.alpha <= 0 end - @entity_controller.draw + @tool.draw if @tool end @debug_info.draw if Setting.enabled?(:debug_info_bar) @@ -134,7 +75,7 @@ class IMICRTS @director.update @player.camera.update - @entity_controller.update + @tool.update if @tool mouse = @player.camera.transform(window.mouse) tile = @director.map.tile_at(mouse.x / @director.map.tile_size, mouse.y / @director.map.tile_size) @@ -161,19 +102,23 @@ class IMICRTS def button_down(id) super - @entity_controller.button_down(id) + @tool.button_down(id) if @tool @player.camera.button_down(id) unless @sidebar.hit?(window.mouse_x, window.mouse_y) end def button_up(id) super - @entity_controller.button_up(id) + @tool.button_up(id) if @tool @player.camera.button_up(id) end - def set_tool(tool, *args) - pp tool, args + def set_tool(tool, data = {}) + unless tool + set_tool(:entity_controller) + else + @tool = Tool.get(tool).new(data, game: self, director: @director, player: @player) + end end def finalize diff --git a/lib/tool.rb b/lib/tool.rb new file mode 100644 index 0000000..6d34857 --- /dev/null +++ b/lib/tool.rb @@ -0,0 +1,49 @@ +class IMICRTS + class Tool + @@tools = {} + def self.get(tool) + @@tools.dig(tool) + end + + def self.inherited(subclass) + @@tools[subclass.to_s.to_snakecase] = subclass + end + + attr_reader :game, :director, :player + def initialize(options = {}, game: nil, director: nil, player: nil) + @options = options + @game = game + @director = director + @player = player + + setup + end + + def setup + end + + def draw + end + + def update + end + + def button_down(id) + end + + def button_up(id) + end + + def cost + return 0 + end + + def cancel_tool + @game.set_tool(nil) + end + end +end + +Dir.glob("#{IMICRTS::GAME_ROOT_PATH}/lib/tools/**/*.rb").each do |tool| + require_relative tool +end \ No newline at end of file diff --git a/lib/entity_controller.rb b/lib/tools/entity_controller.rb similarity index 81% rename from lib/entity_controller.rb rename to lib/tools/entity_controller.rb index 65cdeb4..05baeeb 100644 --- a/lib/entity_controller.rb +++ b/lib/tools/entity_controller.rb @@ -1,14 +1,8 @@ class IMICRTS - # Handles entity de/selection and order manipulation - class EntityController - attr_reader :selected_entities - def initialize(game:, director:, player:) - @game = game - @director = director - @player = player - + # Handles entity de/selection and basic move order + class EntityController < Tool + def setup @drag_start = CyberarmEngine::Vector.new - @selected_entities = [] end def draw @@ -46,7 +40,7 @@ class IMICRTS @selection_start = @player.camera.transform(@game.window.mouse) end when Gosu::MS_RIGHT - if @selected_entities.size > 0 + if @game.selected_entities.size > 0 @director.schedule_order(Order::MOVE, @player.id, @player.camera.transform(@game.window.mouse)) @game.overlays << Game::Overlay.new(Gosu::Image.new("#{IMICRTS::ASSETS_PATH}/cursors/move.png"), @player.camera.transform(@game.window.mouse), 0, 255) @@ -62,14 +56,14 @@ class IMICRTS @box = nil @selection_start = nil - diff = (@player.selected_entities - @selected_entities) + diff = (@player.selected_entities - @game.selected_entities) @director.schedule_order(Order::DESELECTED_UNITS, @player.id, diff) if diff.size > 0 - if @selected_entities.size > 0 - @director.schedule_order(Order::SELECTED_UNITS, @player.id, @selected_entities) + if @game.selected_entities.size > 0 + @director.schedule_order(Order::SELECTED_UNITS, @player.id, @game.selected_entities) else pick_entity - if ent = @selected_entities.first + if ent = @game.selected_entities.first return unless ent.component(:sidebar_actions) @game.sidebar_actions.clear do |stack| @@ -92,8 +86,8 @@ class IMICRTS end if found - @selected_entities = [found] - @director.schedule_order(Order::SELECTED_UNITS, @player.id, @selected_entities) + @game.selected_entities = [found] + @director.schedule_order(Order::SELECTED_UNITS, @player.id, @game.selected_entities) end end @@ -107,9 +101,9 @@ class IMICRTS end if Gosu.button_down?(Gosu::KB_LEFT_SHIFT) || Gosu.button_down?(Gosu::KB_RIGHT_SHIFT) - @selected_entities = @selected_entities.union(selected_entities) + @game.selected_entities = @game.selected_entities.union(selected_entities) else - @selected_entities = selected_entities + @game.selected_entities = selected_entities end end end diff --git a/lib/tools/place_entity.rb b/lib/tools/place_entity.rb new file mode 100644 index 0000000..3f370cd --- /dev/null +++ b/lib/tools/place_entity.rb @@ -0,0 +1,49 @@ +class IMICRTS + class PlaceEntity < Tool + attr_reader :entity, :construction_worker + + def setup + @entity = @options[:entity] + @construction_worker = @options[:construction_worker] + + @preview = Entity.new(name: @entity, player: @player, id: 0, position: CyberarmEngine::Vector.new, angle: 0, director: @director) + end + + def draw + # TODO: draw affected tiles + @preview.draw + end + + def update + # TODO: ensure that construction worker is alive + cancel_tool if @construction_worker.die? + @preview.position = @player.camera.transform(@game.window.mouse) + @preview.position.z = ZOrder::OVERLAY + + @preview.color.alpha = 150 + end + + def button_down(id) + case id + when Gosu::MsRight + cancel_tool + end + end + + def button_up(id) + case id + when Gosu::MsLeft + return if @game.sidebar.hit?(@game.window.mouse_x, @game.window.mouse_y) + + transform = @player.camera.transform(@game.window.mouse) + + @director.spawn_entity( + player_id: @player.id, name: @entity, + position: CyberarmEngine::Vector.new(transform.x, transform.y, ZOrder::BUILDING) + ) + + cancel_tool + end + end + end +end \ No newline at end of file diff --git a/lib/tools/repair_entity.rb b/lib/tools/repair_entity.rb new file mode 100644 index 0000000..0b7eb76 --- /dev/null +++ b/lib/tools/repair_entity.rb @@ -0,0 +1,9 @@ +class IMICRTS + class SellEntity < Tool + attr_accessor :entity + + def setup + @entity = nil + end + end +end \ No newline at end of file diff --git a/lib/tools/sell_entity.rb b/lib/tools/sell_entity.rb new file mode 100644 index 0000000..0b7eb76 --- /dev/null +++ b/lib/tools/sell_entity.rb @@ -0,0 +1,9 @@ +class IMICRTS + class SellEntity < Tool + attr_accessor :entity + + def setup + @entity = nil + end + end +end \ No newline at end of file