diff --git a/.gitignore b/.gitignore
index db869a5..1776656 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,10 @@ pkg/*
data/**/*.json
data/settings.json
data/simulator.rb
-data/*.csv
\ No newline at end of file
+data/*.csv
+media/sounds/*
+!media/sounds/.gitkeep
+media/particles/*
+!media/particles/.gitkeep
+media/music/*
+!media/music/.gitkeep
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..ee86e6a
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,28 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ clipboard (1.3.6)
+ cyberarm_engine (0.19.1)
+ clipboard (~> 1.3.5)
+ excon (~> 0.78.0)
+ gosu (~> 1.1)
+ gosu_more_drawables (~> 0.3)
+ excon (0.78.1)
+ ffi (1.15.4)
+ gosu (1.2.0)
+ gosu_more_drawables (0.3.1)
+ gosu_notifications (0.1.0)
+ ocra (1.3.11)
+
+PLATFORMS
+ x86_64-linux
+
+DEPENDENCIES
+ clipboard
+ cyberarm_engine
+ ffi
+ gosu_notifications
+ ocra
+
+BUNDLED WITH
+ 2.2.19
diff --git a/lib/game_clock/clock.rb b/lib/game_clock/clock.rb
new file mode 100644
index 0000000..507d699
--- /dev/null
+++ b/lib/game_clock/clock.rb
@@ -0,0 +1,70 @@
+module TAC
+ class PracticeGameClock
+ class Clock
+ CLOCK_SIZE = Gosu.screen_height
+ TITLE_SIZE = 128
+
+ attr_reader :title
+
+ def initialize
+ @title = CyberarmEngine::Text.new("FIRST TECH CHALLENGE", size: TITLE_SIZE, text_shadow: true, y: 10, color: Gosu::Color::GRAY)
+ @title.x = $window.width / 2 - @title.width / 2
+
+ @text = CyberarmEngine::Text.new(":1234567890", size: CLOCK_SIZE, text_shadow: true, shadow_size: 2, shadow_color: Gosu::Color::GRAY)
+ @text.width # trigger font-eager loading
+
+ @title.z, @text.z = -1, -1
+
+ @controller = nil
+ end
+
+ def controller=(controller)
+ @controller = controller
+ end
+
+ def draw
+ @title.draw
+ @text.draw
+ end
+
+ def update
+ @title.x = $window.width / 2 - @title.width / 2
+
+ if @controller
+ @text.color = @controller.display_color
+ @text.text = clock_time(@controller.time_left)
+ else
+ @text.color = Gosu::Color::WHITE
+ @text.text = "0:00"
+ end
+
+ @text.x = $window.width / 2 - @text.textobject.text_width("0:00") / 2
+ @text.y = $window.height / 2 - @text.height / 2
+
+ @controller&.update
+ end
+
+ def active?
+ if @controller
+ @controller.clock? || @controller.countdown?
+ else
+ false
+ end
+ end
+
+ def value
+ @text.text
+ end
+
+ def clock_time(time_left)
+ minutes = ((time_left + 0.5) / 60.0).floor
+
+ seconds = time_left.round % 60
+ seconds = "0#{seconds}" if seconds < 10
+
+ return "#{minutes}:#{seconds}" if time_left.round.even?
+ return "#{minutes}:#{seconds}" if time_left.round.odd?
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/clock_controller.rb b/lib/game_clock/clock_controller.rb
new file mode 100644
index 0000000..2e87cdc
--- /dev/null
+++ b/lib/game_clock/clock_controller.rb
@@ -0,0 +1,136 @@
+module TAC
+ class PracticeGameClock
+ class ClockController
+ Event = Struct.new(:event, :trigger_after, :arguments)
+
+ include EventHandlers
+
+ def self.create_event(event, trigger_after, arguments = nil)
+ arguments = [arguments] unless arguments.is_a?(Array) || arguments == nil
+
+ Event.new(event, trigger_after, arguments)
+ end
+
+ AUTONOMOUS = [
+ create_event(:change_clock, 0.0, "2:30"),
+ create_event(:change_countdown, 0.0, "0:03"),
+ create_event(:change_color, 0.0, :red),
+ create_event(:change_display, 0.0, :countdown),
+ create_event(:start_countdown, 0.0),
+ create_event(:play_sound, 0.0, :autonomous_countdown),
+ create_event(:change_color, 3.0, :white),
+ create_event(:change_display, 3.0, :clock),
+ create_event(:play_sound, 3.0, :autonomous_start),
+ create_event(:change_display, 3.0, :clock),
+ create_event(:stop_countdown, 3.0),
+ create_event(:start_clock, 3.0),
+ create_event(:play_sound, 33.0, :autonomous_ended),
+ create_event(:stop_clock, 33.0),
+ ].freeze
+
+ PRE_TELEOP = [
+ create_event(:change_color, 33.0, :orange),
+ create_event(:change_countdown, 33.0, "0:08"),
+ create_event(:change_display, 33.0, :countdown),
+ create_event(:start_countdown, 33.0),
+ create_event(:play_sound, 34.5, :teleop_pickup_controllers),
+ create_event(:change_color, 37.0, :red),
+ create_event(:play_sound, 38.0, :teleop_countdown),
+ create_event(:stop_countdown, 41.0),
+ ].freeze
+
+ TELEOP_ENDGAME = [
+ create_event(:change_color, 131.0, :white),
+ create_event(:change_clock, 131.0, "0:30"),
+ create_event(:start_clock, 131.0),
+ create_event(:play_sound, 131.0, :end_game),
+ create_event(:play_sound, 158.0, :autonomous_countdown),
+ create_event(:play_sound, 161.0, :end_match),
+ create_event(:stop_clock, 161.0),
+ ].freeze
+
+ TELEOP = [
+ create_event(:change_color, 41.0, :white),
+ create_event(:change_clock, 41.0, "2:00"),
+ create_event(:play_sound, 41.0, :teleop_started),
+ create_event(:change_display, 41.0, :clock),
+ create_event(:start_clock, 41.0),
+ TELEOP_ENDGAME
+ ].flatten.freeze
+
+ FULL_TELEOP = [
+ PRE_TELEOP,
+ TELEOP,
+ TELEOP_ENDGAME,
+ ].flatten.freeze
+
+ FULL_MATCH = [
+ # Autonomous
+ AUTONOMOUS,
+ FULL_TELEOP
+ ].flatten.freeze
+
+ attr_reader :display_color
+ def initialize(elapsed_time = 0, events = [])
+ @events = events.dup
+ @last_update = Gosu.milliseconds
+
+ @elapsed_time = elapsed_time
+ @display = :clock
+
+ @clock_time = 0.0
+ @countdown_time = 0.0
+
+ @clock_running = false
+ @countdown_running = false
+
+ @display_color = Gosu::Color::WHITE
+ end
+
+ def update
+ dt = (Gosu.milliseconds - @last_update) / 1000.0
+ update_active_timer(dt)
+
+ @events.select { |event| event.trigger_after <= @elapsed_time }.each do |event|
+ @events.delete(event)
+
+ if event.arguments
+ self.send(event.event, *event.arguments)
+ else
+ self.send(event.event)
+ end
+ end
+
+ @last_update = Gosu.milliseconds
+ end
+
+ def update_active_timer(dt)
+ if @clock_running
+ @clock_time -= dt
+ elsif @countdown_running
+ @countdown_time -= dt
+ end
+
+ @elapsed_time += dt
+ end
+
+ def clock?
+ @clock_running
+ end
+
+ def countdown?
+ @countdown_running
+ end
+
+ def time_left
+ if @clock_running
+ return @clock_time
+ elsif @countdown_running
+ return @countdown_time
+ else
+ return 60 * 2 + 30
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/clock_proxy.rb b/lib/game_clock/clock_proxy.rb
new file mode 100644
index 0000000..684890f
--- /dev/null
+++ b/lib/game_clock/clock_proxy.rb
@@ -0,0 +1,100 @@
+module TAC
+ class PracticeGameClock
+ class ClockProxy
+ def initialize(clock, jukebox)
+ @clock = clock
+ @jukebox = jukebox
+
+ @callbacks = {}
+ end
+
+ def register(callback, method)
+ @callbacks[callback] = method
+ end
+
+ def start_clock(mode)
+ return if @clock.active? || $window.current_state.is_a?(Randomizer)
+
+ @clock.controller = case mode
+ when :full_match
+ ClockController.new(0, ClockController::FULL_MATCH)
+ when :autonomous
+ ClockController.new(0, ClockController::AUTONOMOUS)
+ when :full_teleop
+ ClockController.new(33.0, ClockController::FULL_TELEOP)
+ when :teleop_only
+ ClockController.new(41.0, ClockController::TELEOP)
+ when :endgame_only
+ ClockController.new(131.0, ClockController::TELEOP_ENDGAME)
+ else
+ nil
+ end
+ end
+
+ def abort_clock
+ $window.current_state&.get_sample("media/fogblast.wav")&.play if @clock.active?
+ @clock.controller = nil
+ end
+
+ def set_clock_title(string)
+ @clock.title.text = string.to_s
+ @clock.title.x = $window.width / 2 - @clock.title.width / 2
+ end
+
+ def get_clock_title(string)
+ @clock.title
+ end
+
+ def jukebox_previous_track
+ @jukebox.previous_track
+ end
+
+ def jukebox_next_track
+ @jukebox.next_track
+ end
+
+ def jukebox_stop
+ @jukebox.stop
+ end
+
+ def jukebox_play
+ @jukebox.play
+ end
+
+ def jukebox_pause
+ @jukebox.pause
+ end
+
+ def jukebox_set_volume(float)
+ @jukebox.set_volume(float)
+ end
+
+ def jukebox_volume
+ @jukebox.volume
+ end
+
+ def jukebox_current_track
+ @jukebox.now_playing
+ end
+
+ def jukebox_set_sound_effects(boolean)
+ @jukebox.set_sfx(boolean)
+ end
+
+ def jukebox_volume_changed(float)
+ @callbacks[:volume_changed]&.call(float)
+ end
+
+ def jukebox_track_changed(name)
+ @callbacks[:track_changed]&.call(name)
+ end
+
+ def randomizer_changed(boolean)
+ @callbacks[:randomizer_changed]&.call(boolean)
+ end
+
+ def shutdown!
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/event_handlers.rb b/lib/game_clock/event_handlers.rb
new file mode 100644
index 0000000..b153ecd
--- /dev/null
+++ b/lib/game_clock/event_handlers.rb
@@ -0,0 +1,89 @@
+module TAC
+ class PracticeGameClock
+ module EventHandlers
+ ### Clock ###
+ def start_clock
+ @clock_running = true
+ end
+
+ def stop_clock
+ @clock_running = false
+ end
+
+ def change_clock(value)
+ @clock_time = time_from_string(value)
+ end
+
+ ### Countdown ###
+ def start_countdown
+ @countdown_running = true
+ end
+
+ def stop_countdown
+ @countdown_running = false
+ end
+
+
+ def change_countdown(value)
+ @countdown_time = time_from_string(value)
+ end
+
+ def change_display(display)
+ end
+
+ def change_color(color)
+ out = case color
+ when :white
+ Gosu::Color::WHITE
+ when :orange
+ Gosu::Color.rgb(150, 75, 0)
+ when :red
+ Gosu::Color.rgb(150, 0, 0)
+ end
+
+ @display_color = out
+ end
+
+ private def time_from_string(string)
+ split = string.split(":")
+ minutes = (split.first.to_i) * 60
+ seconds = (split.last.to_i)
+
+ return minutes + seconds
+ end
+
+ def play_sound(sound)
+ path = nil
+ case sound
+ when :autonomous_countdown
+ path = "media/sounds/3-2-1.wav"
+ when :autonomous_start
+ path = "media/sounds/charge.wav"
+ when :autonomous_ended
+ path = "media/sounds/endauto.wav"
+ when :teleop_pickup_controllers
+ path = "media/sounds/Pick_Up_Controllers.wav"
+ when :abort_match
+ path = "media/sounds/fogblast.wav"
+ when :teleop_countdown
+ path = "media/sounds/3-2-1.wav"
+ when :teleop_started
+ path = "media/sounds/firebell.wav"
+ when :end_game
+ path = "media/sounds/factwhistle.wav"
+ when :end_match
+ path = "media/sounds/endmatch.wav"
+ end
+
+ path = "#{ROOT_PATH}/#{path}"
+
+ if path && File.exist?(path) && !File.directory?(path)
+ Jukebox::SAMPLES[path] = Gosu::Sample.new(path) unless Jukebox::SAMPLES[path].is_a?(Gosu::Sample)
+ Jukebox::SAMPLES[path].play
+ else
+ warn "WARNING: Sample for #{sound.inspect} could not be found at '#{path}'"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/jukebox.rb b/lib/game_clock/jukebox.rb
new file mode 100644
index 0000000..8477503
--- /dev/null
+++ b/lib/game_clock/jukebox.rb
@@ -0,0 +1,144 @@
+module TAC
+ class PracticeGameClock
+ class Jukebox
+ MUSIC = Dir.glob(ROOT_PATH + "/media/music/*.*").freeze
+ SAMPLES = {}
+
+ if File.exist?(ROOT_PATH + "/media/sounds/skystone")
+ BEEPS_AND_BOOPS = Dir.glob(ROOT_PATH + "/media/sounds/skystone/*.*").freeze
+ end
+
+ attr_reader :volume, :now_playing
+
+ def initialize(clock)
+ @clock = clock
+
+ @order = MUSIC.shuffle
+ @now_playing = ""
+ @playing = false
+ @song = nil
+ @volume = 1.0
+ @last_check_time = Gosu.milliseconds
+
+ @last_sfx_time = Gosu.milliseconds
+ @sfx_random_interval = generate_sfx_period
+ @play_sfx = true
+
+ if defined?(BEEPS_AND_BOOPS)
+ BEEPS_AND_BOOPS.each do |beep|
+ SAMPLES[beep] = Gosu::Sample.new(beep)
+ end
+ end
+ end
+
+ def update
+ return unless Gosu.milliseconds - @last_check_time >= 2000.0
+
+ @last_check_time = Gosu.milliseconds
+
+ if @song && !@song.playing? && !@song.paused?
+ next_track if @playing
+ end
+
+ if @play_sfx && defined?(BEEPS_AND_BOOPS)
+ play_sfx
+ end
+ end
+
+ def play_sfx
+ if Gosu.milliseconds - @last_sfx_time >= @sfx_random_interval
+ @last_sfx_time = Gosu.milliseconds
+ @sfx_random_interval = generate_sfx_period
+
+ pan = rand(0.49999..0.50001)
+ volume = rand(0.75..1.0)
+ speed = rand(0.5..1.25)
+ SAMPLES[BEEPS_AND_BOOPS.sample].play_pan(pan, volume, speed) unless @clock.active?
+ end
+ end
+
+ def generate_sfx_period
+ # rand(15..120) * 1000.0
+ rand(5..10) * 1000.0
+ end
+
+ def set_sfx(boolean)
+ @play_sfx = boolean
+ end
+
+ def play_sfx?
+ @play_sfx
+ end
+
+ def play
+ if @song && @song.paused?
+ @song.play
+ else
+ return false unless @order.size > 0
+
+ @current_song = @order.first
+ @song = Gosu::Song.new(@current_song)
+ @song.volume = @volume
+ @song.play
+ @now_playing = File.basename(@current_song)
+ @order.rotate!(1)
+ end
+
+ @playing = true
+ end
+
+ def pause
+ @playing = false
+ @song.pause if @song
+ end
+
+ def song
+ @song
+ end
+
+ def stop
+ @song.stop if @song
+ @playing = false
+ @now_playing = ""
+ end
+
+ def previous_track
+ return false unless @order.size > 0
+
+ @song.stop if @song
+ @order.rotate!(-1)
+ @current_song = @order.first
+ @song = Gosu::Song.new(@current_song)
+ @song.volume = @volume
+ @song.play
+
+ @playing = true
+ @now_playing = File.basename(@current_song)
+ end
+
+ def next_track
+ return false unless @order.size > 0
+
+ @song.stop if @song
+ @current_song = @order.first
+ @song = Gosu::Song.new(@current_song)
+ @song.volume = @volume
+ @song.play
+ @order.rotate!(1)
+
+ @playing = true
+ @now_playing = File.basename(@current_song)
+ end
+
+ def current_track
+ @current_song
+ end
+
+ def set_volume(float)
+ @volume = float
+ @volume = @volume.clamp(0.1, 1.0)
+ @song.volume = @volume if @song
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/logger.rb b/lib/game_clock/logger.rb
new file mode 100644
index 0000000..bdcca32
--- /dev/null
+++ b/lib/game_clock/logger.rb
@@ -0,0 +1,28 @@
+module TAC
+ class PracticeGameClock
+ class ClockNet
+ class Logger
+ def printer(message)
+ # return
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")} #{message}"
+ end
+
+ def i(tag, message)
+ printer("INFO #{tag}: #{message}")
+ end
+
+ def d(tag, message)
+ printer("DEBUG #{tag}: #{message}")
+ end
+
+ def e(tag, message)
+ printer("ERROR #{tag}: #{message}")
+ end
+ end
+ end
+
+ def log
+ @logger ||= ClockNet::Logger.new
+ end
+ end
+end
diff --git a/lib/game_clock/net/client.rb b/lib/game_clock/net/client.rb
new file mode 100644
index 0000000..e4d9dcf
--- /dev/null
+++ b/lib/game_clock/net/client.rb
@@ -0,0 +1,162 @@
+require "securerandom"
+
+module TAC
+ class PracticeGameClock
+ class ClockNet
+ class Client
+ TAG = "ClockNet|Client"
+ CHUNK_SIZE = 4096
+ PACKET_TAIL = "\r\n\n"
+
+ attr_reader :uuid, :read_queue, :write_queue, :socket,
+ :packets_sent, :packets_received,
+ :data_sent, :data_received
+ attr_accessor :sync_interval, :last_socket_error, :socket_error
+ def initialize
+ @uuid = SecureRandom.uuid
+ @read_queue = []
+ @write_queue = []
+
+ @sync_interval = 100
+
+ @last_socket_error = nil
+ @socket_error = false
+ @bound = false
+
+ @packets_sent, @packets_received = 0, 0
+ @data_sent, @data_received = 0, 0
+ end
+
+ def uuid=(id)
+ @uuid = id
+ end
+
+ def socket=(socket)
+ @socket = socket
+ @bound = true
+
+ listen
+ end
+
+ def listen
+ Thread.new do
+ while connected?
+ # Read from socket
+ while message_in = read
+ if message_in.empty?
+ break
+ else
+ log.i(TAG, "Read: " + message_in)
+
+ @read_queue << message_in
+
+ @packets_received += 1
+ @data_received += message_in.length
+ end
+ end
+
+ sleep @sync_interval / 1000.0
+ end
+ end
+
+ Thread.new do
+ while connected?
+ # Write to socket
+ while message_out = @write_queue.shift
+ write(message_out)
+
+ @packets_sent += 1
+ @data_sent += message_out.to_s.length
+ log.i(TAG, "Write: " + message_out.to_s)
+ end
+
+ sleep @sync_interval / 1000.0
+ end
+ end
+ end
+
+ def sync(block)
+ block.call
+ end
+
+ def handle_read_queue
+ message = gets
+
+ while message
+ puts(message)
+
+ log.i(TAG, "Writing to Queue: " + message)
+
+ message = gets
+ end
+ end
+
+ def socket_error?
+ @socket_error
+ end
+
+ def connected?
+ if closed? == true || closed? == nil
+ false
+ else
+ true
+ end
+ end
+
+ def closed?
+ @socket.closed? if @socket
+ end
+
+ def write(message)
+ begin
+ @socket.puts("#{message}#{PACKET_TAIL}")
+ rescue => error
+ @last_socket_error = error
+ @socket_error = true
+ log.e(TAG, error.message)
+ close
+ end
+ end
+
+ def read
+ begin
+ message = @socket.gets
+ rescue => error
+ @last_socket_error = error
+ @socket_error = true
+
+ message = ""
+ end
+
+
+ return message.strip
+ end
+
+ def puts(message)
+ @write_queue << message
+ end
+
+ def gets
+ @read_queue.shift
+ end
+
+ def encode(message)
+ return message
+ end
+
+ def decode(blob)
+ return blob
+ end
+
+ def flush
+ @socket.flush if socket
+ end
+
+ def close(reason = nil)
+ write(reason) if reason
+ @socket.close if @socket
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/game_clock/net/connection.rb b/lib/game_clock/net/connection.rb
new file mode 100644
index 0000000..80266d3
--- /dev/null
+++ b/lib/game_clock/net/connection.rb
@@ -0,0 +1,97 @@
+module TAC
+ class PracticeGameClock
+ class ClockNet
+ class Connection
+ TAG = "ClockNet|Connection"
+ attr_reader :hostname, :port, :client, :proxy_object
+
+ def initialize(hostname: "localhost", port: 4567, proxy_object:)
+ @hostname = hostname
+ @port = port
+ @proxy_object = proxy_object
+
+ @client = nil
+
+ @last_sync_time = Gosu.milliseconds
+ @sync_interval = SYNC_INTERVAL
+
+ @last_heartbeat_sent = Gosu.milliseconds
+ @heartbeat_interval = HEARTBEAT_INTERVAL
+
+ @connection_handler = proc do
+ handle_connection
+ end
+
+ @packet_handler = PacketHandler.new(host_is_a_connection: true, proxy_object: @proxy_object)
+ end
+
+ def connect
+ return if @client
+
+ @client = Client.new
+
+ Thread.new do
+ begin
+ @client.socket = Socket.tcp(@hostname, @port, connect_timeout: 5)
+ @client.socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ log.i(TAG, "Connected to: #{@hostname}:#{@port}")
+
+ while @client && @client.connected?
+ if Gosu.milliseconds > @last_sync_time + @sync_interval
+ @last_sync_time = Gosu.milliseconds
+
+ @client.sync(@connection_handler)
+ end
+ end
+
+ rescue => error
+ log.e(TAG, error)
+
+ if @client
+ @client.close
+ @client.last_socket_error = error
+ @client.socket_error = true
+ end
+ end
+ end
+ end
+
+ def handle_connection
+ if @client && @client.connected?
+ message = @client.gets
+
+ @packet_handler.handle(message) if message
+
+ if Gosu.milliseconds > @last_heartbeat_sent + @heartbeat_interval
+ @last_heartbeat_sent = Gosu.milliseconds
+
+ @client.puts(PacketHandler.packet_heartbeat)
+ end
+
+ sleep @sync_interval / 1000.0
+ end
+ end
+
+ def puts(packet)
+ @client.puts(packet)
+ end
+
+ def gets
+ @client.gets
+ end
+
+ def connected?
+ @client.connected? if @client
+ end
+
+ def closed?
+ @client.closed? if @client
+ end
+
+ def close
+ @client.close if @client
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/net/packet.rb b/lib/game_clock/net/packet.rb
new file mode 100644
index 0000000..f05d710
--- /dev/null
+++ b/lib/game_clock/net/packet.rb
@@ -0,0 +1,109 @@
+module TAC
+ class PracticeGameClock
+ class ClockNet
+ SYNC_INTERVAL = 250
+ HEARTBEAT_INTERVAL = 1_500
+
+ class Packet
+ PROTOCOL_VERSION = 1
+ PROTOCOL_SEPERATOR = "|"
+ PROTOCOL_HEARTBEAT = "heartbeat"
+
+ PACKET_TYPES = {
+ handshake: 0,
+ heartbeat: 1,
+ error: 2,
+ shutdown: 3,
+
+ start_clock: 10,
+ abort_clock: 11,
+
+ set_clock_title: 20,
+ get_clock_title: 21,
+ clock_title: 22,
+ clock_time: 23,
+
+ randomizer_visible: 27,
+
+ jukebox_previous_track: 30,
+ jukebox_next_track: 31,
+ jukebox_stop: 32,
+ jukebox_play: 33,
+ jukebox_pause: 34,
+ jukebox_set_volume: 35,
+ jukebox_get_volume: 36,
+ jukebox_volume: 37,
+ jukebox_get_current_track: 38,
+ jukebox_current_track: 39,
+ jukebox_get_sound_effects: 40,
+ jukebox_set_sound_effects: 41,
+ jukebox_sound_effects: 42,
+ }
+
+ def self.from_stream(message)
+ slice = message.split("|", 4)
+
+ if slice.size < 4
+ warn "Failed to split packet along first 4 " + PROTOCOL_SEPERATOR + ". Raw return: " + slice.to_s
+ return nil
+ end
+
+ if slice.first != PROTOCOL_VERSION.to_s
+ warn "Incompatible protocol version received, expected: " + PROTOCOL_VERSION.to_s + " got: " + slice.first
+ return nil
+ end
+
+ unless valid_packet_type?(Integer(slice[1]))
+ warn "Unknown packet type detected: #{slice[1]}"
+ return nil
+ end
+
+ protocol_version = Integer(slice[0])
+ type = PACKET_TYPES.key(Integer(slice[1]))
+ content_length = Integer(slice[2])
+ body = slice[3]
+
+ raise "Type is #{type.inspect} [#{type.class}]" unless type.is_a?(Symbol)
+
+ return Packet.new(protocol_version, type, content_length, body)
+ end
+
+ def self.create(packet_type, body)
+ Packet.new(PROTOCOL_VERSION, PACKET_TYPES.key(packet_type), body.length, body)
+ end
+
+ def self.valid_packet_type?(packet_type)
+ PACKET_TYPES.values.find { |t| t == packet_type }
+ end
+
+ attr_reader :protocol_version, :type, :content_length, :body
+ def initialize(protocol_version, type, content_length, body)
+ @protocol_version = protocol_version
+ @type = type
+ @content_length = content_length
+ @body = body
+ end
+
+ def encode_header
+ string = ""
+ string += protocol_version.to_s
+ string += PROTOCOL_SEPERATOR
+ string += PACKET_TYPES[type].to_s
+ string += PROTOCOL_SEPERATOR
+ string += content_length.to_s
+ string += PROTOCOL_SEPERATOR
+
+ return string
+ end
+
+ def valid?
+ true
+ end
+
+ def to_s
+ "#{encode_header}#{body}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/net/packet_handler.rb b/lib/game_clock/net/packet_handler.rb
new file mode 100644
index 0000000..1f3e628
--- /dev/null
+++ b/lib/game_clock/net/packet_handler.rb
@@ -0,0 +1,304 @@
+module TAC
+ class PracticeGameClock
+ class ClockNet
+ class PacketHandler
+ TAG = "ClockNet|PacketHandler"
+ def initialize(host_is_a_connection: false, proxy_object:)
+ @host_is_a_connection = host_is_a_connection
+ @proxy_object = proxy_object
+ end
+
+ def handle(message)
+ packet = Packet.from_stream(message)
+
+ if packet
+ log.i(TAG, "Received packet of type: #{packet.type}")
+ hand_off(packet)
+ else
+ log.d(TAG, "Rejected raw packet: #{message}")
+ end
+ end
+
+ def hand_off(packet)
+ case packet.type
+ when :handshake
+ handle_handshake(packet)
+ when :heartbeat
+ handle_heartbeat(packet)
+ when :error
+ handle_error(packet)
+
+ when :start_clock
+ handle_start_clock(packet)
+ when :abort_clock
+ handle_abort_clock(packet)
+ when :get_clock_title
+ handle_get_clock_title(packet)
+ when :set_clock_title
+ handle_set_clock_title(packet)
+ when :clock_title
+ handle_clock_title(packet)
+ when :jukebox_previous_track
+ handle_jukebox_previous_track(packet)
+ when :jukebox_next_track
+ handle_jukebox_next_track(packet)
+ when :jukebox_play
+ handle_jukebox_play(packet)
+ when :jukebox_pause
+ handle_jukebox_pause(packet)
+ when :jukebox_stop
+ handle_jukebox_stop(packet)
+ when :jukebox_set_volume
+ handle_jukebox_set_volume(packet)
+ when :jukebox_volume
+ handle_jukebox_volume(packet)
+ when :jukebox_set_sound_effects
+ handle_jukebox_set_sound_effects(packet)
+ when :jukebox_current_track
+ handle_jukebox_current_track(packet)
+ when :clock_time
+ handle_clock_time(packet)
+ when :randomizer_visible
+ handle_randomizer_visible(packet)
+ when :shutdown
+ handle_shutdown(packet)
+ else
+ log.d(TAG, "No hand off available for packet type: #{packet.type}")
+ end
+ end
+
+ def handle_handshake(packet)
+ if @host_is_a_connection
+ end
+ end
+
+ # TODO: Reset socket timeout
+ def handle_heartbeat(packet)
+ end
+
+ # TODO: Handle errors
+ def handle_error(packet)
+ title, message = packet.body.split(Packet::PROTOCOL_SEPERATOR, 2)
+ log.e(TAG, "Remote error: #{title}: #{message}")
+ end
+
+ def handle_start_clock(packet)
+ unless @host_is_a_connection
+ @proxy_object.start_clock(packet.body.to_sym)
+ end
+ end
+
+ def handle_abort_clock(packet)
+ unless @host_is_a_connection
+ @proxy_object.abort_clock
+ end
+ end
+
+ def handle_set_clock_title(packet)
+ unless @host_is_a_connection
+ title = packet.body
+ @proxy_object.set_clock_title(title)
+ end
+ end
+
+ def handle_get_clock_title(packet)
+ unless @host_is_a_connection
+ $RemoteControl.server.active_client.puts(Packet.clock_title(@proxy_object.clock.title))
+ end
+ end
+
+ def handle_jukebox_previous_track(packet)
+ unless @host_is_a_connection
+ @proxy_object.jukebox_previous_track
+
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
+ end
+ end
+
+ def handle_jukebox_next_track(packet)
+ unless @host_is_a_connection
+ @proxy_object.jukebox_next_track
+
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
+ end
+ end
+
+ def handle_jukebox_play(packet)
+ unless @host_is_a_connection
+ @proxy_object.jukebox_play
+
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
+ end
+ end
+
+ def handle_jukebox_pause(packet)
+ unless @host_is_a_connection
+ @proxy_object.jukebox_pause
+
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
+ end
+ end
+
+ def handle_jukebox_stop(packet)
+ unless @host_is_a_connection
+ @proxy_object.jukebox_stop
+
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
+ end
+ end
+
+ def handle_jukebox_set_volume(packet)
+ unless @host_is_a_connection
+ float = packet.body.to_f
+ float = float.clamp(0.0, 1.0)
+
+ @proxy_object.jukebox_set_volume(float)
+
+ float = @proxy_object.jukebox_volume
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_volume(float))
+ end
+ end
+
+ def handle_jukebox_get_volume(packet)
+ unless @host_is_a_connection
+ float = @proxy_object.jukebox_volume
+
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_volume(float))
+ end
+ end
+
+ def handle_jukebox_volume(packet)
+ if @host_is_a_connection
+ float = packet.body.to_f
+
+ @proxy_object.volume_changed(float)
+ end
+ end
+
+ def handle_jukebox_set_sound_effects(packet)
+ unless @host_is_a_connection
+ boolean = packet.body == "true"
+
+ @proxy_object.jukebox_set_sound_effects(boolean)
+ end
+ end
+
+ def handle_jukebox_current_track(packet)
+ if @host_is_a_connection
+ @proxy_object.track_changed(packet.body)
+ end
+ end
+
+ def handle_clock_time(packet)
+ if @host_is_a_connection
+ @proxy_object.clock_changed(packet.body)
+ end
+ end
+
+ def handle_randomizer_visible(packet)
+ boolean = packet.body == "true"
+
+ @proxy_object.randomizer_changed(boolean)
+
+ unless @host_is_a_connection
+ # Send confirmation to client
+ $RemoteControl.server.active_client.puts(PacketHandler.packet_randomizer_visible(boolean))
+ end
+ end
+
+ def handle_shutdown(packet)
+ unless @host_is_a_connection
+ # $RemoteControl.server.close
+ # $window.close
+ Gosu::Song.current_song&.stop
+ exit
+ end
+ end
+
+ def self.packet_handshake(client_uuid)
+ Packet.create(Packet::PACKET_TYPES[:handshake], client_uuid)
+ end
+
+ def self.packet_heartbeat
+ Packet.create(Packet::PACKET_TYPES[:heartbeat], Packet::PROTOCOL_HEARTBEAT)
+ end
+
+ def self.packet_error(error_code, message)
+ Packet.create(Packet::PACKET_TYPES[:error], error_code.to_s, message.to_s)
+ end
+
+ def self.packet_start_clock(mode)
+ Packet.create(Packet::PACKET_TYPES[:start_clock], mode.to_s)
+ end
+
+ def self.packet_abort_clock
+ Packet.create(Packet::PACKET_TYPES[:abort_clock], "")
+ end
+
+ def self.packet_set_clock_title(string)
+ Packet.create(Packet::PACKET_TYPES[:set_clock_title], string.to_s)
+ end
+
+ def self.packet_get_clock_title
+ Packet.create(Packet::PACKET_TYPES[:get_clock_title], "")
+ end
+
+ def self.packet_clock_title(string)
+ Packet.create(Packet::PACKET_TYPES[:clock_title], string.to_s)
+ end
+
+ def self.packet_jukebox_previous_track
+ Packet.create(Packet::PACKET_TYPES[:jukebox_previous_track], "")
+ end
+
+ def self.packet_jukebox_next_track
+ Packet.create(Packet::PACKET_TYPES[:jukebox_next_track], "")
+ end
+
+ def self.packet_jukebox_play
+ Packet.create(Packet::PACKET_TYPES[:jukebox_play], "")
+ end
+
+ def self.packet_jukebox_pause
+ Packet.create(Packet::PACKET_TYPES[:jukebox_pause], "")
+ end
+
+ def self.packet_jukebox_stop
+ Packet.create(Packet::PACKET_TYPES[:jukebox_stop], "")
+ end
+
+ def self.packet_jukebox_set_volume(float)
+ Packet.create(Packet::PACKET_TYPES[:jukebox_set_volume], float.to_s)
+ end
+
+ def self.packet_jukebox_get_volume
+ Packet.create(Packet::PACKET_TYPES[:jukebox_get_volume], "")
+ end
+
+ def self.packet_jukebox_volume(float)
+ Packet.create(Packet::PACKET_TYPES[:jukebox_volume], float.to_s)
+ end
+
+ def self.packet_jukebox_set_sound_effects(boolean)
+ Packet.create(Packet::PACKET_TYPES[:jukebox_set_sound_effects], boolean.to_s)
+ end
+
+ def self.packet_jukebox_current_track(name)
+ Packet.create(Packet::PACKET_TYPES[:jukebox_current_track], name)
+ end
+
+ def self.packet_clock_time(string)
+ Packet.create(Packet::PACKET_TYPES[:clock_time], string)
+ end
+
+ def self.packet_randomizer_visible(boolean)
+ Packet.create(Packet::PACKET_TYPES[:randomizer_visible], boolean.to_s)
+ end
+
+ def self.packet_shutdown
+ Packet.create(Packet::PACKET_TYPES[:shutdown], "")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/net/server.rb b/lib/game_clock/net/server.rb
new file mode 100644
index 0000000..dff8afb
--- /dev/null
+++ b/lib/game_clock/net/server.rb
@@ -0,0 +1,146 @@
+module TAC
+ class PracticeGameClock
+ class ClockNet
+ class Server
+ TAG = "ClockNet|Server"
+ attr_reader :active_client,
+ :packets_sent, :packets_received, :data_sent, :data_received,
+ :client_last_packets_sent, :client_last_packets_received, :client_last_data_sent, :client_last_data_received
+ def initialize(hostname: "localhost", port: 4567, proxy_object: )
+ $server = self
+
+ @hostname = hostname
+ @port = port
+ @proxy_object = proxy_object
+
+ @socket = nil
+ @active_client = nil
+ @connection_attempts = 0
+ @max_connection_attempts = 10
+
+ @packets_sent, @packets_received, @client_last_packets_sent, @client_last_packets_received = 0, 0, 0, 0
+ @data_sent, @data_received, @client_last_data_sent, @client_last_data_received = 0, 0, 0, 0
+
+ @last_sync_time = Gosu.milliseconds
+ @sync_interval = SYNC_INTERVAL
+
+ @last_heartbeat_sent = Gosu.milliseconds
+ @heartbeat_interval = HEARTBEAT_INTERVAL
+
+ @client_handler_proc = proc do
+ handle_client
+ end
+
+ @packet_handler = PacketHandler.new(proxy_object: @proxy_object)
+ end
+
+ def start(run_on_main_thread: false)
+ thread = Thread.new do
+ while (!@socket && @connection_attempts < @max_connection_attempts)
+ begin
+ log.i(TAG, "Starting server...")
+ @socket = TCPServer.new(@port)
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ rescue IOError => error
+ log.e(TAG, error)
+
+ @connection_attempts += 1
+ retry if @connection_attempts < @max_connection_attempts
+ end
+ end
+
+ while @socket && !@socket.closed?
+ begin
+ run_server
+ rescue IOError => error
+ log.e(TAG, error)
+ @socket.close if @socket
+ end
+ end
+ end
+
+ thread.join if run_on_main_thread
+ end
+
+ def run_server
+ while !@socket.closed?
+ client = Client.new
+ client.sync_interval = @sync_interval
+ client.socket = @socket.accept
+
+ if @active_client && @active_client.connected?
+ log.i(TAG, "Too many clients, already have one connected!")
+ client.close("Too many clients!")
+ else
+ @active_client = client
+ # TODO: Backup local config
+ # SEND CONFIG
+
+ @active_client.puts(PacketHandler.packet_handshake(@active_client.uuid))
+
+ log.i(TAG, "Client connected!")
+
+ Thread.new do
+ while @active_client && @active_client.connected?
+ if Gosu.milliseconds > @last_sync_time + @sync_interval
+ @last_sync_time = Gosu.milliseconds
+
+ @active_client.sync(@client_handler_proc)
+ update_stats
+ end
+ end
+
+ update_stats
+ @active_client = nil
+
+ @client_last_packets_sent = 0
+ @client_last_packets_received = 0
+ @client_last_data_sent = 0
+ @client_last_data_received = 0
+ end
+ end
+ end
+ end
+
+ def handle_client
+ if @active_client && @active_client.connected?
+ message = @active_client.gets
+
+ if message && !message.empty?
+ @packet_handler.handle(message)
+ end
+
+ if Gosu.milliseconds > @last_heartbeat_sent + @heartbeat_interval
+ @last_heartbeat_sent = Gosu.milliseconds
+
+ @active_client.puts(PacketHandler.packet_heartbeat)
+ end
+
+ sleep @sync_interval / 1000.0
+ end
+ end
+
+ def close
+ @socket.close
+ end
+
+ private def update_stats
+ if @active_client
+ # NOTE: Sent and Received are reversed for Server stats
+
+ @packets_sent += @active_client.packets_received - @client_last_packets_received
+ @packets_received += @active_client.packets_sent - @client_last_packets_sent
+
+ @data_sent += @active_client.data_received - @client_last_data_received
+ @data_received += @active_client.data_sent - @client_last_data_sent
+
+ @client_last_packets_sent = @active_client.packets_sent
+ @client_last_packets_received = @active_client.packets_received
+ @client_last_data_sent = @active_client.data_sent
+ @client_last_data_received = @active_client.data_received
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/particle_emitter.rb b/lib/game_clock/particle_emitter.rb
new file mode 100644
index 0000000..0c776e7
--- /dev/null
+++ b/lib/game_clock/particle_emitter.rb
@@ -0,0 +1,135 @@
+module TAC
+ class PracticeGameClock
+ class ParticleEmitter
+ def initialize(max_particles: 50, time_to_live: 30_000, interval: 1_500, z: -2)
+ @max_particles = max_particles
+ @time_to_live = time_to_live
+ @interval = interval
+ @z = -2
+
+ @particles = []
+ @image_options = Dir.glob("#{ROOT_PATH}/media/particles/*.*")
+ @last_spawned = 0
+ @clock_active = false
+ end
+
+ def draw
+ @particles.each(&:draw)
+ end
+
+ def update
+ @particles.each { |part| part.update($window.dt) }
+ @particles.delete_if { |part| part.die? }
+
+ spawn_particles
+ end
+
+ def spawn_particles
+ # !clock_active? &&
+ if @particles.count < @max_particles && Gosu.milliseconds - @last_spawned >= @interval
+ screen_midpoint = CyberarmEngine::Vector.new($window.width / 2, $window.height / 2)
+ scale = rand(0.25..1.0)
+ image_name = @image_options.sample
+
+ return unless image_name
+
+ image = $window.current_state.get_image(image_name)
+ position = CyberarmEngine::Vector.new(0, 0)
+
+ r = rand
+ if r < 0.25 # LEFT
+ position.x = -image.width * scale
+ position.y = rand(0..($window.height - image.height * scale))
+ elsif r < 0.5 # RIGHT
+ position.x = $window.width + (image.width * scale)
+ position.y = rand(0..($window.height - image.height * scale))
+ elsif r < 0.75 # TOP
+ position.x = rand(0..($window.width - image.width * scale))
+ position.y = -image.height * scale
+ else #BOTTOM
+ position.x = rand(0..($window.width - image.width * scale))
+ position.y = $window.height + image.height * scale
+ end
+
+ position.x ||= 0
+ position.y ||= 0
+
+ velocity = (screen_midpoint - position)
+
+ @particles << Particle.new(
+ image: image,
+ position: position,
+ velocity: velocity,
+ time_to_live: @time_to_live,
+ speed: rand(24..128),
+ scale: scale,
+ clock_active: @clock_active,
+ z: @z
+ )
+
+ @last_spawned = Gosu.milliseconds
+ end
+ end
+
+ def clock_active!
+ @clock_active = true
+ @particles.each(&:clock_active!)
+ end
+
+ def clock_inactive!
+ @clock_active = false
+ @particles.each(&:clock_inactive!)
+ end
+
+ def clock_active?
+ @clock_active
+ end
+
+ class Particle
+ def initialize(image:, position:, velocity:, time_to_live:, speed:, z:, scale: 1.0, clock_active: false)
+ @image = image
+ @position = position
+ @velocity = velocity.normalized
+ @time_to_live = time_to_live
+ @speed = speed
+ @z = z
+ @scale = scale
+
+ @born_at = Gosu.milliseconds
+ @born_time_to_live = time_to_live
+ @color = Gosu::Color.new(0xff_ffffff)
+ @clock_active = clock_active
+ end
+
+ def draw
+ @image.draw(@position.x, @position.y, @z, @scale, @scale, @color)
+ end
+
+ def update(dt)
+ @position += @velocity * @speed * dt
+
+ @color.alpha = (255.0 * ratio).to_i.clamp(0, 255)
+ end
+
+ def ratio
+ r = 1.0 - ((Gosu.milliseconds - @born_at) / @time_to_live.to_f)
+ @clock_active ? r.clamp(0.0, 0.5) : r
+ end
+
+ def die?
+ ratio <= 0
+ end
+
+ def clock_active!
+ @clock_active = true
+ # @time_to_live = (Gosu.milliseconds - @born_at) + 1_000
+ end
+
+ def clock_inactive!
+ @clock_active = false
+ # @time_to_live = @born_time_to_live unless Gosu.milliseconds - @born_at < @time_to_live
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/randomizer.rb b/lib/game_clock/randomizer.rb
new file mode 100644
index 0000000..3eb428b
--- /dev/null
+++ b/lib/game_clock/randomizer.rb
@@ -0,0 +1,127 @@
+require "securerandom"
+
+module TAC
+ class PracticeGameClock
+ class Randomizer < CyberarmEngine::GameState
+ def setup
+ @roll = SecureRandom.random_number(1..6)
+
+ @position = CyberarmEngine::Vector.new
+
+ @dimple_color = 0xff_008000
+ @dimple_res = 36
+
+ @size = [window.width, window.height].min / 2.0
+
+ @rings = []
+
+ case @roll
+ when 1, 4
+ when 2, 5
+ @rings << Ring.new(position: CyberarmEngine::Vector.new(-@size, window.height * 0.9), speed: 512)
+ when 3, 6
+ @rings << Ring.new(position: CyberarmEngine::Vector.new(-@size, window.height * 0.9), speed: 512)
+ @rings << Ring.new(position: CyberarmEngine::Vector.new(-@size * 1.25, window.height * 0.8), speed: 512)
+ @rings << Ring.new(position: CyberarmEngine::Vector.new(-@size * 1.50, window.height * 0.7), speed: 512)
+ @rings << Ring.new(position: CyberarmEngine::Vector.new(-@size * 1.75, window.height * 0.6), speed: 512)
+ end
+ end
+
+ def draw
+ window.previous_state.draw
+
+ Gosu.flush
+
+ fill(0xdd_202020)
+
+ Gosu.translate(window.width * 0.5 - @size * 0.5, 24) do
+ Gosu.draw_rect(0, 0, @size, @size, Gosu::Color::BLACK)
+ Gosu.draw_rect(12, 12, @size - 24, @size - 24, Gosu::Color::GRAY)
+
+ self.send(:"dice_#{@roll}", @size)
+ end
+
+ @rings.each { |r| r.draw(@size) }
+ end
+
+ def dice_1(size)
+ Gosu.draw_circle(size / 2, size / 2, dimple(size), @dimple_res, @dimple_color)
+ end
+
+ def dice_2(size)
+ Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
+ end
+
+ def dice_3(size)
+ Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.50, size * 0.50, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
+ end
+
+ def dice_4(size)
+ Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.25, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.25, size * 0.75, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
+ end
+
+ def dice_5(size)
+ Gosu.draw_circle(size * 0.50, size * 0.50, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.25, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.25, size * 0.75, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
+ end
+
+ def dice_6(size)
+ Gosu.draw_circle(size * 0.25, size * 0.20, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.20, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.25, size * 0.50, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.50, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.25, size * 0.80, dimple(size), @dimple_res, @dimple_color)
+ Gosu.draw_circle(size * 0.75, size * 0.80, dimple(size), @dimple_res, @dimple_color)
+ end
+
+ def dimple(size)
+ size / 9.0
+ end
+
+ def update
+ window.previous_state&.update_non_gui
+
+ @rings.each { |r| r.update(window, @size) }
+
+ @size = [window.width, window.height].min / 2.0
+ end
+
+ def button_down(id)
+ case id
+ when Gosu::MS_LEFT, Gosu::KB_ESCAPE, Gosu::KB_SPACE
+ pop_state
+ end
+ end
+
+ class Ring
+ def initialize(position:, speed: 1)
+ @position = position
+ @speed = speed
+ @color = 0xff_ffaa00
+ end
+
+ def draw(size)
+ Gosu.translate(@position.x, @position.y) do
+ Gosu.draw_rect(0, 0, size, size * 0.10, @color)
+ end
+ end
+
+ def update(window, size)
+ center = window.width * 0.5 - size * 0.5
+
+ @position.x += @speed * window.dt
+ @position.x = center if @position.x > center
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/remote_control.rb b/lib/game_clock/remote_control.rb
new file mode 100644
index 0000000..39c7284
--- /dev/null
+++ b/lib/game_clock/remote_control.rb
@@ -0,0 +1,270 @@
+module TAC
+ class PracticeGameClock
+ class RemoteControl
+ @@connection = nil
+ @@server = nil
+
+ def self.connection
+ @@connection
+ end
+
+ def self.connection=(connection)
+ @@connection = connection
+ end
+
+ def self.server
+ @@server
+ end
+
+ def self.server=(server)
+ @@server = server
+ end
+
+ class NetConnect < CyberarmEngine::GuiState
+ def setup
+ theme(THEME)
+
+ background Palette::TACNET_NOT_CONNECTED
+
+ banner "ClockNet Remote Control", text_align: :center, width: 1.0
+ flow(width: 1.0) do
+ stack(width: 0.25) {}
+ stack(width: 0.5) do
+ title "Hostname"
+ @hostname = edit_line "localhost", width: 1.0
+ title "Port"
+ @port = edit_line "4567", width: 1.0
+
+ flow(width: 1.0, margin_top: 20) do
+ @back_button = button "Back", width: 0.5 do
+ window.pop_state
+ end
+
+ @connect_button = button "Connect", width: 0.5 do
+ begin
+ @connect_button.enabled = false
+ @back_button.enabled = false
+
+ @connection = ClockNet::Connection.new(hostname: @hostname.value, port: Integer(@port.value), proxy_object: RemoteProxy.new(window))
+
+ @connection.connect
+ RemoteControl.connection = @connection
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def update
+ super
+
+ if RemoteControl.connection
+ push_state(Controller) if RemoteControl.connection.connected?
+
+ RemoteControl.connection = nil if RemoteControl.connection.client.socket_error?
+ else
+ @back_button.enabled = true
+ @connect_button.enabled = true
+ end
+ end
+ end
+
+ class Controller < CyberarmEngine::GuiState
+ def setup
+ theme(THEME)
+
+ at_exit do
+ @connection&.close
+ end
+
+ @jukebox_volume = 1.0
+ @jukebox_sound_effects = true
+ @randomizer_visible = false
+
+ RemoteControl.connection.proxy_object.register(:track_changed, method(:track_changed))
+ RemoteControl.connection.proxy_object.register(:volume_changed, method(:volume_changed))
+ RemoteControl.connection.proxy_object.register(:clock_changed, method(:clock_changed))
+ RemoteControl.connection.proxy_object.register(:randomizer_changed, method(:randomizer_changed))
+
+ background Palette::TACNET_NOT_CONNECTED
+
+ banner "ClockNet Remote Control", text_align: :center, width: 1.0
+
+ flow width: 1.0, height: 1.0 do
+ stack width: 0.5 do
+ title "Match", width: 1.0, text_align: :center
+ button "Start Match", width: 1.0, text_size: 48, margin_bottom: 50 do
+ start_clock(:full_match)
+ end
+
+ title "Practice", width: 1.0, text_align: :center
+ button "Autonomous", width: 1.0 do
+ start_clock(:autonomous)
+ end
+ button "TeleOp with Countdown", width: 1.0 do
+ start_clock(:full_teleop)
+ end
+ button "TeleOp", width: 1.0 do
+ start_clock(:teleop_only)
+ end
+ button "TeleOp Endgame", width: 1.0, margin_bottom: 50 do
+ start_clock(:endgame_only)
+ end
+
+ button "Abort Clock", width: 1.0 do
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_abort_clock)
+ end
+
+ button "Shutdown", width: 1.0, **DANGEROUS_BUTTON do
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_shutdown)
+ sleep 1 # let packet escape before closing
+ exit
+ end
+ end
+
+ stack width: 0.495 do
+ title "Clock Title", width: 1.0, text_align: :center
+
+ stack width: 0.9, margin_left: 50 do
+ @title = edit_line "FIRST TECH CHALLENGE", width: 1.0
+
+ button "Update", width: 1.0, margin_bottom: 50 do
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_set_clock_title(@title.value.strip))
+ end
+ end
+
+ title "JukeBox", width: 1.0, text_align: :center
+ stack width: 0.9, margin_left: 50 do
+ flow width: 1.0 do
+ tagline "Now Playing: "
+ @track_name = tagline ""
+ end
+
+ flow width: 1.0 do
+ tagline "Volume: "
+ @volume = tagline "100%"
+ end
+
+ flow width: 1.0 do
+ button get_image("#{ROOT_PATH}/media/icons/previous.png") do
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_previous_track)
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/right.png") do |button|
+ if @jukebox_playing
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_pause)
+ button.value = get_image("#{ROOT_PATH}/media/icons/right.png")
+ @jukebox_playing = false
+ else
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_play)
+ button.value = get_image("#{ROOT_PATH}/media/icons/pause.png")
+ @jukebox_playing = true
+ end
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/stop.png") do
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_stop)
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/next.png") do
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_next_track)
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/minus.png"), margin_left: 20 do
+ @jukebox_volume -= 0.1
+ @jukebox_volume = 0.1 if @jukebox_volume < 0.1
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_set_volume(@jukebox_volume))
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/plus.png") do
+ @jukebox_volume += 0.1
+ @jukebox_volume = 0.1 if @jukebox_volume < 0.1
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_set_volume(@jukebox_volume))
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/musicOn.png"), margin_left: 20, tip: "Toggle Sound Effects" do |button|
+ if @jukebox_sound_effects
+ button.value = get_image("#{ROOT_PATH}/media/icons/musicOff.png")
+ @jukebox_sound_effects = false
+ else
+ button.value = get_image("#{ROOT_PATH}/media/icons/musicOn.png")
+ @jukebox_sound_effects = true
+ end
+
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_set_sound_effects(@jukebox_sound_effects))
+ end
+ end
+
+ button "Open Music Library", width: 1.0 do
+ path = "#{ROOT_PATH}/media/music"
+
+ if RUBY_PLATFORM.match(/ming|msys|cygwin/)
+ system("explorer \"#{path.gsub("/", "\\")}\"")
+ elsif RUBY_PLATFORM.match(/linux/)
+ system("xdg-open \"#{ROOT_PATH}/media/music\"")
+ else
+ # TODO.
+ end
+ end
+ end
+
+ stack width: 0.9, margin_left: 50, margin_top: 20 do
+ flow width: 1.0 do
+ title "Clock: "
+ @clock_label = title "0:123456789"
+ @clock_label.width
+ @clock_label.value = "0:00"
+ end
+
+ flow width: 1.0 do
+ title "Randomizer: "
+ @randomizer_label = title "Not Visible"
+ end
+
+ button "Randomizer", width: 1.0, **DANGEROUS_BUTTON do
+ @randomizer_visible = !@randomizer_visible
+
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_randomizer_visible(@randomizer_visible))
+ end
+ end
+ end
+ end
+ end
+
+ def update
+ super
+
+ return if RemoteControl.connection.connected?
+
+ # We've lost connection, unset window's connection object
+ # and send user back to connect screen to to attempt to
+ # reconnect
+ RemoteControl.connection = nil
+ pop_state
+ end
+
+ def start_clock(mode)
+ RemoteControl.connection.puts(ClockNet::PacketHandler.packet_start_clock(mode.to_s))
+ end
+
+ def track_changed(name)
+ @track_name.value = name
+ end
+
+ def volume_changed(float)
+ @volume.value = "#{float.round(1) * 100.0}%"
+ end
+
+ def clock_changed(string)
+ @clock_label.value = string
+ end
+
+ def randomizer_changed(boolean)
+ @randomizer_label.value = "Visible" if boolean
+ @randomizer_label.value = "Not Visible" unless boolean
+ end
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/remote_proxy.rb b/lib/game_clock/remote_proxy.rb
new file mode 100644
index 0000000..4b1d0f5
--- /dev/null
+++ b/lib/game_clock/remote_proxy.rb
@@ -0,0 +1,64 @@
+module TAC
+ class PracticeGameClock
+ class RemoteProxy
+ def initialize(window)
+ @window = window
+
+ @callbacks = {}
+ end
+
+ def register(callback, method)
+ @callbacks[callback] = method
+ end
+
+ def start_clock(mode)
+ end
+
+ def abort_clock
+ end
+
+ def set_clock_title(string)
+ end
+
+ def get_clock_title(string)
+ end
+
+ def jukebox_previous_track
+ end
+
+ def jukebox_next_track
+ end
+
+ def jukebox_stop
+ end
+
+ def jukebox_play
+ end
+
+ def jukebox_pause
+ end
+
+ def jukebox_sound_effects(boolean)
+ end
+
+ def volume_changed(float)
+ @callbacks[:volume_changed]&.call(float)
+ end
+
+ def track_changed(name)
+ @callbacks[:track_changed]&.call(name)
+ end
+
+ def clock_changed(string)
+ @callbacks[:clock_changed]&.call(string)
+ end
+
+ def randomizer_changed(boolean)
+ @callbacks[:randomizer_changed]&.call(boolean)
+ end
+
+ def shutdown!
+ end
+ end
+ end
+end
diff --git a/lib/game_clock/theme.rb b/lib/game_clock/theme.rb
new file mode 100644
index 0000000..e436cf5
--- /dev/null
+++ b/lib/game_clock/theme.rb
@@ -0,0 +1,73 @@
+module TAC
+ class PracticeGameClock
+ module Palette
+ TACNET_CONNECTED = Gosu::Color.new(0xff_008000)
+ TACNET_CONNECTING = Gosu::Color.new(0xff_ff8800)
+ TACNET_CONNECTION_ERROR = Gosu::Color.new(0xff_800000)
+ TACNET_NOT_CONNECTED = Gosu::Color.new(0xff_222222)
+
+ TIMECRAFTERS_PRIMARY = Gosu::Color.new(0xff_008000)
+ TIMECRAFTERS_SECONDARY = Gosu::Color.new(0xff_006000)
+ TIMECRAFTERS_TERTIARY = Gosu::Color.new(0xff_00d000)
+
+ BLUE_ALLIANCE = Gosu::Color.new(0xff_000080)
+ RED_ALLIANCE = Gosu::Color.new(0xff_800000)
+
+ TACNET_PRIMARY = Gosu::Color.new(0xff000080)
+ TACNET_SECONDARY = Gosu::Color.new(0xff000060)
+
+ GROUPS_PRIMARY = Gosu::Color.new(0xff_444444)
+ GROUPS_SECONDARY = Gosu::Color.new(0xff_444444)
+
+ ACTIONS_PRIMARY = Gosu::Color.new(0xff_4444aa)
+ ACTIONS_SECONDARY = Gosu::Color.new(0xff_040404)
+
+ VARIABLES_PRIMARY = Gosu::Color.new(0xff_660066)
+ VARIABLES_SECONDARY = Gosu::Color.new(0xff_440044)
+
+ EDITOR_PRIMARY = Gosu::Color.new(0xff_446688)
+ EDITOR_SECONDARY = Gosu::Color.new(0xff_224466)
+
+ ALERT = TACNET_CONNECTING
+ end
+
+ THEME = {
+ TextBlock: {
+ font: "Canterell",
+ color: Gosu::Color.new(0xee_ffffff)
+ },
+ Button: {
+ image_width: 40,
+ text_size: 40,
+ background: Palette::TIMECRAFTERS_PRIMARY,
+ border_thickness: 1,
+ border_color: Gosu::Color.new(0xff_111111),
+ hover: {
+ background: Palette::TIMECRAFTERS_SECONDARY,
+ },
+ active: {
+ background: Palette::TIMECRAFTERS_TERTIARY,
+ }
+ },
+ EditLine: {
+ caret_color: Gosu::Color.new(0xff_88ef90),
+ },
+ ToggleButton: {
+ width: 18,
+ checkmark_image: "#{File.expand_path("..", __dir__)}/media/icons/checkmark.png",
+ },
+ }
+
+ DANGEROUS_BUTTON = {
+ background: 0xff_800000,
+ border_thickness: 1,
+ border_color: Gosu::Color.new(0xff_111111),
+ hover: {
+ background: 0xff_600000,
+ },
+ active: {
+ background: 0xff_f00000,
+ }
+ }
+ end
+end
diff --git a/lib/game_clock/view.rb b/lib/game_clock/view.rb
new file mode 100644
index 0000000..422325e
--- /dev/null
+++ b/lib/game_clock/view.rb
@@ -0,0 +1,283 @@
+module TAC
+ class PracticeGameClock
+ class View < CyberarmEngine::GuiState
+
+ attr_reader :clock
+
+ def setup
+ window.show_cursor = true
+
+ @remote_control_mode = @options[:remote_control_mode]
+ @escape_counter = 0
+
+ @background_image = get_image("#{ROOT_PATH}/media/background.png")
+ @mouse = Mouse.new(window)
+ @clock = Clock.new
+ @clock.controller = nil
+ @last_clock_display_value = @clock.value
+
+ @particle_emitters = [
+ PracticeGameClock::ParticleEmitter.new
+ ]
+
+ @last_clock_state = @clock.active?
+
+ theme(THEME)
+
+ @menu_container = flow width: 1.0 do
+ stack(width: 0.35, padding: 5) do
+ background 0x55004159
+
+ title "Match", width: 1.0, text_align: :center
+ button "Start Match", width: 1.0, margin_bottom: 50 do
+ @clock_proxy.start_clock(:full_match)
+ end
+
+ title "Practice", width: 1.0, text_align: :center
+ button "Autonomous", width: 1.0 do
+ @clock_proxy.start_clock(:autonomous)
+ end
+
+ button "TeleOp with Countdown", width: 1.0 do
+ @clock_proxy.start_clock(:full_teleop)
+ end
+
+ button "TeleOp", width: 1.0 do
+ @clock_proxy.start_clock(:teleop_only)
+ end
+
+ button "TeleOp Endgame", width: 1.0 do
+ @clock_proxy.start_clock(:endgame_only)
+ end
+
+ button "Abort Clock", width: 1.0, margin_top: 50 do
+ @clock_proxy.abort_clock
+ end
+
+ button "Close", width: 1.0, **DANGEROUS_BUTTON do
+ if window.instance_variable_get(:"@states").size == 1
+ window.close
+ else
+ @server&.close
+
+ window.fullscreen = false
+ window.pop_state
+ end
+ end
+ end
+
+ stack width: 0.4, padding_left: 50 do
+ background 0x55004159
+
+ flow do
+ label "♫ Now playing:"
+ @current_song_label = label "♫ ♫ ♫"
+ end
+
+ flow do
+ label "Volume:"
+ @current_volume_label = label "100%"
+ end
+
+ flow do
+ button get_image("#{ROOT_PATH}/media/icons/previous.png") do
+ @jukebox.previous_track
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/pause.png") do |button|
+ if @jukebox.song && @jukebox.song.paused?
+ button.value = get_image("#{ROOT_PATH}/media/icons/right.png")
+ @jukebox.play
+ elsif !@jukebox.song
+ button.value = get_image("#{ROOT_PATH}/media/icons/right.png")
+ @jukebox.play
+ else
+ button.value = get_image("#{ROOT_PATH}/media/icons/pause.png")
+ @jukebox.pause
+ end
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/stop.png") do
+ @jukebox.stop
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/next.png") do
+ @jukebox.next_track
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/minus.png"), margin_left: 20 do
+ @jukebox.set_volume(@jukebox.volume - 0.1)
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/plus.png") do
+ @jukebox.set_volume(@jukebox.volume + 0.1)
+ end
+
+ button "Open Music Library", margin_left: 50 do
+ if RUBY_PLATFORM.match(/ming|msys|cygwin/)
+ system("explorer #{ROOT_PATH}/media/music")
+ elsif RUBY_PLATFORM.match(/linux/)
+ system("xdg-open #{ROOT_PATH}/media/music")
+ else
+ # TODO.
+ end
+ end
+
+ button get_image("#{ROOT_PATH}/media/icons/musicOn.png"), margin_left: 50, tip: "Toggle Sound Effects" do |button|
+ boolean = @jukebox.set_sfx(!@jukebox.play_sfx?)
+
+ if boolean
+ button.value = get_image("#{ROOT_PATH}/media/icons/musicOn.png")
+ else
+ button.value = get_image("#{ROOT_PATH}/media/icons/musicOff.png")
+ end
+ end
+ end
+
+ stack width: 1.0 do
+ button "Randomizer", width: 1.0, **DANGEROUS_BUTTON do
+ unless @clock.active?
+ push_state(Randomizer)
+ end
+ end
+ end
+ end
+ end
+
+ @jukebox = Jukebox.new(@clock)
+
+ @clock_proxy = ClockProxy.new(@clock, @jukebox)
+
+ if @remote_control_mode
+ @server = ClockNet::Server.new(proxy_object: @clock_proxy)
+ @server.start
+ RemoteControl.server = @server
+
+ @clock_proxy.register(:randomizer_changed, method(:randomizer_changed))
+ end
+ end
+
+ def draw
+ @background_image.draw(0, 0, -3)
+ @particle_emitters.each(&:draw)
+ @clock.draw
+
+ super
+ end
+
+ def update
+ super
+ @clock.update
+ @mouse.update
+ @jukebox.update
+ @particle_emitters.each(&:update)
+
+ if @last_clock_state != @clock.active?
+ @particle_emitters.each { |emitter| @clock.active? ? emitter.clock_active! : emitter.clock_inactive! }
+ end
+
+ if @remote_control_mode
+ @menu_container.hide
+ else
+ if @mouse.last_moved < 1.5
+ @menu_container.show unless @menu_container.visible?
+ window.show_cursor = true
+ else
+ @menu_container.hide if @menu_container.visible?
+ window.show_cursor = false
+ end
+ end
+
+ if @clock.value != @last_clock_display_value
+ @last_clock_display_value = @clock.value
+
+ if @remote_control_mode && @server.active_client
+ @server.active_client.puts(ClockNet::PacketHandler.packet_clock_time(@last_clock_display_value))
+ end
+ end
+
+ if @last_track_name != @jukebox.current_track
+ track_changed(@jukebox.current_track)
+ end
+
+ if @last_volume != @jukebox.volume
+ volume_changed(@jukebox.volume)
+ end
+
+ @last_track_name = @jukebox.current_track
+ @last_volume = @jukebox.volume
+ @last_clock_state = @clock.active?
+ end
+
+ def update_non_gui
+ @particle_emitters.each(&:update)
+ @jukebox.update
+ end
+
+ def button_down(id)
+ super
+
+ @mouse.button_down(id)
+
+ case id
+ when Gosu::KB_ESCAPE
+ @escape_counter += 1
+
+ if @escape_counter >= 3
+ @server&.close
+
+ window.fullscreen = false
+ window.pop_state
+ end
+ else
+ @escape_counter = 0
+ end
+ end
+
+ def track_changed(name)
+ @current_song_label.value = File.basename(name)
+ end
+
+ def volume_changed(float)
+ @current_volume_label.value = "#{(float.round(1) * 100.0).round}%"
+ end
+
+ def randomizer_changed(boolean)
+ if boolean
+ push_state(Randomizer)
+ else
+ pop_state if current_state.is_a?(Randomizer)
+ end
+ end
+
+ class Mouse
+ def initialize(window)
+ @window = window
+ @last_moved = 0
+
+ @last_position = CyberarmEngine::Vector.new(@window.mouse_x, @window.mouse_y)
+ end
+
+ def update
+ position = CyberarmEngine::Vector.new(@window.mouse_x, @window.mouse_y)
+
+ if @last_position != position
+ @last_position = position
+ @last_moved = Gosu.milliseconds
+ end
+ end
+
+ def button_down(id)
+ case id
+ when Gosu::MS_LEFT, Gosu::MS_MIDDLE, Gosu::MS_RIGHT
+ @last_moved = Gosu.milliseconds
+ end
+ end
+
+ def last_moved
+ (Gosu.milliseconds - @last_moved) / 1000.0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pages/drive_team_rotation_generator.rb b/lib/pages/drive_team_rotation_generator.rb
index 0a59859..bac8bf6 100644
--- a/lib/pages/drive_team_rotation_generator.rb
+++ b/lib/pages/drive_team_rotation_generator.rb
@@ -42,7 +42,7 @@ module TAC
flow(width: 1.0, margin_bottom: 20) do
@role_name = edit_line "", placeholder: "Add role", width: 0.9
- button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 0.1 do
+ button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 0.1, tip: "Add role" do
if @role_name.value.strip.length.positive?
@roles.push(@role_name.value.strip)
@role_name.value = ""
@@ -61,7 +61,7 @@ module TAC
flow(width: 1.0, margin_bottom: 20) do
@roster_name = edit_line "", placeholder: "Add name", width: 0.9
- button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 0.1 do
+ button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 0.1, tip: "Add name" do
if @roster_name.value.strip.length.positive?
@roster.push(@roster_name.value.strip)
@roster_name.value = ""
@@ -96,7 +96,7 @@ module TAC
background i.even? ? 0xff_007000 : 0xff_006000
tagline name, width: 0.9
- button "X", width: 0.1, text_size: 18, **THEME_DANGER_BUTTON do
+ button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: 0.1, tip: "Remove role", **THEME_DANGER_BUTTON do
@roles.delete(name)
populate_roles
end
@@ -112,7 +112,7 @@ module TAC
background i.even? ? 0xff_007000 : 0xff_006000
tagline name, width: 0.9
- button "X", width: 0.1, text_size: 18, **THEME_DANGER_BUTTON do
+ button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: 0.1, tip: "Remove name", **THEME_DANGER_BUTTON do
@roster.delete(name)
populate_roster
end
diff --git a/lib/pages/game_clock.rb b/lib/pages/game_clock.rb
index a09e79c..0df68fe 100644
--- a/lib/pages/game_clock.rb
+++ b/lib/pages/game_clock.rb
@@ -2,14 +2,41 @@ module TAC
class Pages
class GameClock < Page
def setup
- header_bar("Practice Game Clock")
+ header_bar("Game Clock")
body.clear do
- stack(width: 1.0, height: 1.0) do
- label TAC::NAME, width: 1.0, text_size: 48, text_align: :center
+ flow(width: 1.0, height: 1.0) do
+ @command_options = flow(width: 1.0) do
+ stack(width: 0.3) do
+ end
- stack(width: 1.0, height: 8) do
- background 0xff_006000
+ stack(width: 0.4) do
+ banner "Choose Mode", width: 1.0, text_align: :center
+ title "Local", width: 1.0, text_align: :center
+
+ button "Game Clock", width: 1.0 do
+ push_state(PracticeGameClock::View)
+
+ window.fullscreen = true
+ end
+
+ button "Dual Screen Game Clock", width: 1.0, enabled: false do
+ end
+
+ title "Remote", width: 1.0, text_align: :center, margin_top: 32
+ button "Game Clock Display", width: 1.0 do
+ push_state(PracticeGameClock::View, remote_control_mode: true)
+
+ window.fullscreen = true
+ end
+
+ button "Game Clock Remote Control", width: 1.0 do
+ push_state(PracticeGameClock::RemoteControl::NetConnect)
+ end
+ end
+
+ stack(width: 0.3) do
+ end
end
end
end
diff --git a/lib/states/editor.rb b/lib/states/editor.rb
index ee1250c..71a8df2 100644
--- a/lib/states/editor.rb
+++ b/lib/states/editor.rb
@@ -94,9 +94,13 @@ class Editor < CyberarmEngine::GuiState
page(TAC::Pages::DriveTeamRotationGenerator)
end
- button get_image("#{TAC::ROOT_PATH}/media/icons/trophy.png"), margin: 4, tip: "Practice Game Clock", image_width: 1.0 do
+ button get_image("#{TAC::ROOT_PATH}/media/icons/trophy.png"), margin: 4, tip: "Game Clock", image_width: 1.0 do
page(TAC::Pages::GameClock)
end
+
+ button get_image("#{TAC::ROOT_PATH}/media/icons/power.png"), margin: 4, tip: "Exit", image_width: 1.0, **TAC::THEME_DANGER_BUTTON do
+ window.close
+ end
end
@content = stack(width: window.width - @navigation.style.width, height: 1.0) do
diff --git a/media/background.png b/media/background.png
new file mode 100644
index 0000000..30e289b
Binary files /dev/null and b/media/background.png differ
diff --git a/media/background.svg b/media/background.svg
new file mode 100644
index 0000000..c959e66
--- /dev/null
+++ b/media/background.svg
@@ -0,0 +1,6240 @@
+
+
+
+
diff --git a/media/music/.gitkeep b/media/music/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/media/particles/.gitkeep b/media/particles/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/media/sounds/.gitkeep b/media/sounds/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/timecrafters_configuration_tool.rb b/timecrafters_configuration_tool.rb
index a886bb0..39da3a9 100644
--- a/timecrafters_configuration_tool.rb
+++ b/timecrafters_configuration_tool.rb
@@ -51,6 +51,24 @@ require_relative "lib/tacnet/client"
require_relative "lib/tacnet/connection"
require_relative "lib/tacnet/server"
+require_relative "lib/game_clock/view"
+require_relative "lib/game_clock/clock"
+require_relative "lib/game_clock/event_handlers"
+require_relative "lib/game_clock/clock_controller"
+require_relative "lib/game_clock/jukebox"
+require_relative "lib/game_clock/theme"
+require_relative "lib/game_clock/clock_proxy"
+require_relative "lib/game_clock/logger"
+require_relative "lib/game_clock/particle_emitter"
+require_relative "lib/game_clock/randomizer"
+require_relative "lib/game_clock/remote_control"
+require_relative "lib/game_clock/remote_proxy"
+require_relative "lib/game_clock/net/client"
+require_relative "lib/game_clock/net/server"
+require_relative "lib/game_clock/net/connection"
+require_relative "lib/game_clock/net/packet_handler"
+require_relative "lib/game_clock/net/packet"
+
# Thread.abort_on_exception = true
USE_REDESIGN = ARGV.include?("--redesign")