lzrtag-base 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,136 @@
1
+
2
+ require 'xmlsimple'
3
+
4
+ require_relative 'map_zone.rb'
5
+
6
+ module LZRTag
7
+ module Map
8
+ class MyMapsParser
9
+ attr_accessor :styles
10
+ attr_accessor :polygons
11
+ attr_accessor :points
12
+
13
+ def initialize(filename)
14
+ @xmlStructure = XmlSimple.xml_in(filename)["Document"][0];
15
+
16
+ @styles = Hash.new();
17
+ _fetch_styles();
18
+
19
+ @polygons = Hash.new();
20
+ @polygons[""] = _fetch_polygons();
21
+
22
+ @points = Hash.new();
23
+ @points[""] = _fetch_marks();
24
+
25
+ if(folders = @xmlStructure["Folder"])
26
+ folders.each do |folder|
27
+ @polygons[folder["name"][0]] = _fetch_polygons(folder)
28
+ @points[folder["name"][0]] = _fetch_marks(folder);
29
+ end
30
+ end
31
+ end
32
+
33
+ def _fetch_styles()
34
+ @xmlStructure["Style"].each do |s|
35
+ id = s["id"];
36
+ if(id =~ /(.*)-normal$/)
37
+ id = $1;
38
+ else
39
+ next;
40
+ end
41
+
42
+ next unless(s.has_key?("PolyStyle") && s.has_key?("LineStyle"))
43
+
44
+ @styles["#" + id] = {
45
+ color: s["PolyStyle"][0]["color"][0],
46
+ borderColor: s["LineStyle"][0]["color"][0]
47
+ };
48
+ end
49
+ end
50
+ private :_fetch_styles
51
+
52
+ def _fetch_polygons(folder = nil)
53
+ folder ||= @xmlStructure;
54
+
55
+ outZones = Array.new();
56
+
57
+ return outZones unless(placemarks = folder["Placemark"])
58
+
59
+ placemarks.each do |zone|
60
+ next unless zone["Polygon"];
61
+
62
+ outZone = Hash.new();
63
+ outZone[:name] = zone["name"][0];
64
+ outZone[:description] = (zone["description"] || [""])[0];
65
+
66
+ outZone[:arguments] = Hash.new();
67
+ outZone[:description].split("<br>").each do |tag|
68
+ if(tag =~ /([^:]*):([^:]*)/)
69
+ outZone[:arguments][$1] = $2;
70
+ end
71
+ end
72
+
73
+ outZone[:style] = @styles[zone["styleUrl"][0]];
74
+
75
+ rawPolyData = zone["Polygon"][0]["outerBoundaryIs"][0]["LinearRing"][0]["coordinates"][0];
76
+ rawPolyData.gsub!(" ", "");
77
+ rawPolyArray = rawPolyData.split("\n");
78
+
79
+ outZone[:polygon] = Array.new();
80
+ rawPolyArray.each do |point|
81
+ point = point.split(",");
82
+ next if point.empty?
83
+ outZone[:polygon] << [point[0].to_f, point[1].to_f];
84
+ end
85
+
86
+ outZones << outZone;
87
+ end
88
+
89
+ return outZones;
90
+ end
91
+ private :_fetch_polygons
92
+
93
+ def _fetch_marks(folder = nil)
94
+ folder ||= @xmlStructure;
95
+
96
+ outPoints = Array.new();
97
+ return outPoints unless(placemarks = folder["Placemark"])
98
+
99
+ placemarks.each do |pmark|
100
+ next unless pmark["Point"];
101
+
102
+ outPoint = Hash.new();
103
+ outPoint[:name] = pmark["name"][0];
104
+ outPoint[:description] = (pmark["description"] || [""])[0];
105
+
106
+ outPoint[:description].split("<br>").each do |tag|
107
+ if(tag =~ /([^:]*):([^:]*)/)
108
+ outPoint[:arguments][$1] = $2;
109
+ end
110
+ end
111
+
112
+ outPoint[:point] = pmark["Point"][0]["coordinates"][0].gsub(/\s/, "").split(",")[0..1];
113
+
114
+ outPoints << outPoint
115
+ end
116
+
117
+ return outPoints
118
+ end
119
+
120
+ def generate_zones(zoneSet = "")
121
+ if(zoneSet.is_a? String)
122
+ zoneSet = @polygons[zoneSet];
123
+ end
124
+
125
+ zoneSet = [zoneSet].flatten;
126
+
127
+ outZones = Array.new();
128
+ zoneSet.each do |rawZone|
129
+ outZones << Zone.from_raw_zone(rawZone);
130
+ end
131
+
132
+ return outZones;
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,104 @@
1
+
2
+ require 'mqtt/sub_handler'
3
+
4
+ module LZRTag
5
+ module Player
6
+ # The base player class.
7
+ # This class is not instantiated by the user, but instead on a on-demand basis
8
+ # by the LZRTag::Handler::Base when a new PlayerID needs to be registered. The player classes
9
+ # process and send MQTT data, handle events, and keep track of per-player infos like life,
10
+ # damage, ammo, team, etc. etc.
11
+ class Base
12
+ attr_reader :handler
13
+ # @return [String] The player's DeviceID, which is derived from the ESP's MAC
14
+ attr_reader :DeviceID
15
+
16
+ # @return [String] Name of the player, set externally
17
+ attr_reader :name
18
+
19
+ # @return [String] status-string of the player. Should be "OK"
20
+ attr_reader :status
21
+
22
+ # @return [Integer] 0..255, shot ID of the player
23
+ attr_reader :id
24
+ # @return [Hash<Time>] Hash of the last few recorded shot times,
25
+ # used for hit arbitration
26
+ attr_accessor :hitIDTimetable
27
+
28
+ def initialize(deviceID, handler)
29
+ @handler = handler;
30
+ @mqtt = handler.mqtt;
31
+
32
+ @DeviceID = deviceID;
33
+
34
+ @status = "";
35
+ @name = "";
36
+
37
+ @hitIDTimetable = Hash.new(Time.new(0));
38
+ end
39
+
40
+ def _pub_to(key, data, retain: false)
41
+ @mqtt.publish_to("Lasertag/Players/#{@DeviceID}/#{key}", data, retain: retain);
42
+ end
43
+ private :_pub_to
44
+
45
+ # @private
46
+ def on_mqtt_data(data, topic)
47
+ case topic[1..topic.length].join("/")
48
+ when "Connection"
49
+ return if @status == data;
50
+ oldStatus = @status;
51
+ @status = data;
52
+ if(@status == "OK")
53
+ @handler.send_event(:playerConnected, self);
54
+ elsif(oldStatus == "OK")
55
+ @handler.send_event(:playerDisconnected, self);
56
+ end
57
+ when "CFG/Name"
58
+ @name = data;
59
+ end
60
+ end
61
+
62
+ # @return [Boolean] Whether this player is connected
63
+ def connected?()
64
+ return @status == "OK"
65
+ end
66
+
67
+ # Set the Shot ID of the player.
68
+ # @note Do not call this function yourself - the Handler must
69
+ # assign unique IDs to ensure proper game functionality!
70
+ # @private
71
+ def id=(n)
72
+ return if @id == n;
73
+
74
+ if(!n.nil?)
75
+ raise ArgumentError, "ID must be integer or nil!" unless n.is_a? Integer;
76
+ raise ArgumentError, "ID out of range (0<ID<256)" unless n < 256 and n > 0;
77
+
78
+ @id = n;
79
+ else
80
+ @id = nil;
81
+ end
82
+
83
+ _pub_to("CFG/ID", @id, retain: true);
84
+ end
85
+
86
+ # Trigger a clear of all topics
87
+ # @note Do not call this function yourself, except when deregistering a player!
88
+ # @private
89
+ def clear_all_topics()
90
+ self.id = nil;
91
+ end
92
+
93
+ def inspect()
94
+ iString = "#<Player:#{@deviceID}##{@id ? @id : "OFFLINE"}, Team=#{@team}";
95
+ iString += ", DEAD" if @dead
96
+ iString += ", Battery=#{@battery.round(2)}"
97
+ iString += ", Ping=#{@ping.ceil}ms>";
98
+
99
+ return iString;
100
+ end
101
+ alias to_s inspect
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,96 @@
1
+
2
+ require_relative 'hardware_player.rb'
3
+
4
+ module LZRTag
5
+ module Player
6
+ # This class extends the pure hardware class, adding various hooks
7
+ # that can be used to send effects and other events to the weapon.
8
+ # These do not change the game itself, but instead just look and feel good!
9
+ class Effects < Hardware
10
+ # Heartbeat status of the player
11
+ # The "heartbeat" is a regular vibration pattern on the weapon,
12
+ # which can be used to indicate things like low life or other tense events.
13
+ # Set it to true/false.
14
+ attr_reader :heartbeat
15
+
16
+ # Mark a player in a given color
17
+ # This function can be used to "mark" a player, sending flashes of
18
+ # light across their LEDs in a given color. This can either be
19
+ # false, to turn the marking off, or 0..7 to set it to a team color.
20
+ # Alternatively, any RGB Number (0x?? ?? ??) can be used for arbitrary marking color
21
+ attr_reader :marked
22
+
23
+ def initialize(*data)
24
+ super(*data);
25
+
26
+ @marked = false;
27
+ end
28
+
29
+ # Vibrate the weapon for a number of seconds
30
+ # @param duration [Numeric] Number (in s) to vibrate for.
31
+ def vibrate(duration)
32
+ raise ArgumentError, "Vibration-duration out of range (between 0 and 65.536)" unless duration.is_a? Numeric and duration <= 65.536 and duration >= 0
33
+ _pub_to("CFG/Vibrate", duration);
34
+ end
35
+
36
+ def heartbeat=(data)
37
+ return if (@heartbeat == data);
38
+
39
+ @heartbeat = data;
40
+ _pub_to("CFG/Heartbeat", @heartbeat ? "1" : "0", retain: true);
41
+ end
42
+
43
+ def marked=(data)
44
+ return if (@marked == data);
45
+
46
+ @marked = data;
47
+ if data.is_a? Numeric
48
+ _pub_to("CFG/Marked", @marked.to_s, retain: true)
49
+ else
50
+ _pub_to("CFG/Marked", "0", retain: true)
51
+ end
52
+ end
53
+
54
+ # Make the weapon play a given note.
55
+ # This function can make the set play a note of given frequency, volume
56
+ # and duration.
57
+ # @param duration [Numeric] Length in seconds
58
+ # @param frequency [Numeric] Frequency of the note
59
+ # @param volume [Numeric] Volume (0..1) of the note
60
+ def noise(duration: 0.5, frequency: 440, volume: 0.5)
61
+ return false unless duration.is_a? Numeric and frequency.is_a? Integer
62
+ _pub_to("Sound/Note", [frequency, volume*20000, duration*1000].pack("L3"))
63
+ end
64
+
65
+ # Play a given sound file.
66
+ # Depending on the weapon, various sounds are available to be played,
67
+ # such as:
68
+ # - "GAME START"
69
+ # - "HIT"
70
+ # - "OWN DEATH"
71
+ # - "KILL SCORE"
72
+ # etc.
73
+ # This list may be expanded in the future
74
+ def sound(sName)
75
+ _pub_to("Sound/File", sName);
76
+ end
77
+
78
+ # Make the weapon display a hit.
79
+ # When a weapon is hit, it will flash bright white and vibrate
80
+ # for a short moment. The length can be specified.
81
+ # @param hitLength [Numeric,nil] Length (in s) of the hit.
82
+ def hit(hitLength = nil)
83
+ _pub_to("CFG/Hit", hitLength || 0.7)
84
+ end
85
+
86
+ # @private
87
+ def clear_all_topics()
88
+ super();
89
+
90
+ ["CFG/Heartbeat", "CFG/Marked"].each do |t|
91
+ _pub_to(t, "", retain: true)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,251 @@
1
+
2
+ require 'mqtt/mqtt_hash.rb'
3
+
4
+ require_relative 'base_player.rb'
5
+
6
+ module LZRTag
7
+ module Player
8
+ # Hardware-handling player class.
9
+ # This class extends the base player, adding more hardware-related
10
+ # functionality and interfaces, such as:
11
+ # - Team setting
12
+ # - Brightness setting
13
+ # - Note playing
14
+ # - Gyro and button readout
15
+ # - Ping and Battery reading
16
+ # etc.
17
+ class Hardware < Base
18
+ # The team (0..7) of this player.
19
+ # Setting it to a number will publish to /DeviceID/CFG/Team,
20
+ # changing the color of the weapon. Interpret it as binary string,
21
+ # with 1 being red, 2 green and 4 blue.
22
+ #
23
+ # Changes trigger the :playerTeamChanged event, with [player, oldTeam] data
24
+ attr_reader :team
25
+ # Current brightness of the weapon.
26
+ # @return [Symbol] Symbol describing the current brightness.
27
+ # Possible brightnesses are:
28
+ # - :idle (low, slow brightness, white with slight team hue)
29
+ # - :teamSelect (team-colored with rainbow overlay)
30
+ # - :dead (low brightness, team colored with white overlay)
31
+ # - :active (bright, flickering and in team color)
32
+ #
33
+ # A change will trigger the :playerBrightnessChanged event, with data [player, oldBrightness]
34
+ attr_reader :brightness
35
+
36
+ # Whether or not the player is currently dead.
37
+ # Set this to kill the player. Will trigger a :playerKilled or :playerRevived event,
38
+ # although kill_by is preferred to also specify which player killed.
39
+ attr_reader :dead
40
+ # Last time the death status changed (killed/revivied).
41
+ # Especially useful to determine when to revive a player
42
+ attr_reader :deathChangeTime
43
+
44
+ # Current ammo of the weapon.
45
+ # TODO one day this should be settable. Right now, it's just reading
46
+ attr_reader :ammo
47
+ # Maximum ammo the weapon can have with the currently equipped gun.
48
+ attr_reader :maxAmmo
49
+ # Number of the current gun.
50
+ # The application can freely choose which gun profile the set is using,
51
+ # which influences shot speeds, sounds, reloading, etc.
52
+ attr_reader :gunNo
53
+
54
+ # Returns the gyro pose of the set.
55
+ # This is either:
56
+ # - :active
57
+ # - :laidDown
58
+ # - :pointsUp
59
+ # - :pointsDown
60
+ # Changes are triggered by the set itself, if it has a gyro.
61
+ # The :poseChanged event is sent on change with [player, newPose] data
62
+ attr_reader :gyroPose
63
+
64
+ attr_reader :battery, :ping, :heap
65
+
66
+ def self.getBrightnessKeys()
67
+ return [:idle, :teamSelect, :dead, :active]
68
+ end
69
+
70
+ def initialize(*data)
71
+ super(*data);
72
+
73
+ @team = 0;
74
+ @brightness = :idle;
75
+
76
+ @dead = false;
77
+ @deathChangeTime = Time.now();
78
+
79
+ @ammo = 0;
80
+ @maxAmmo = 0;
81
+ @gunNo = 0;
82
+
83
+ @gyroPose = :unknown;
84
+
85
+ @position = {x: 0, y: 0}
86
+ @zoneIDs = Hash.new();
87
+
88
+ @battery = 0; @ping = 0; @heap = 0;
89
+
90
+ @BrightnessMap = self.class.getBrightnessKeys();
91
+
92
+ # These values are configured for a DPS ~1, equal to all weapons
93
+ # Including reload timings and other penalties
94
+ @GunDamageMultipliers = [
95
+ 0.9138,
96
+ 1.85,
97
+ 0.6166,
98
+ ];
99
+ end
100
+
101
+ # @private
102
+ # This function processes incoming MQTT data.
103
+ # The user must not call this, since it is handled by the
104
+ # LZRTag base handler
105
+ def on_mqtt_data(data, topic)
106
+ case topic[1..topic.length].join("/")
107
+ when "HW/Ping"
108
+ if(data.size == 3*4)
109
+ parsedData = data.unpack("L<*");
110
+
111
+ @battery = parsedData[0].to_f/1000;
112
+ @ping = parsedData[2].to_f;
113
+ end
114
+ when "CFG/Dead"
115
+ dead = (data == "1")
116
+ return if @dead == dead;
117
+ @dead = dead;
118
+
119
+ @deathChangeTime = Time.now();
120
+
121
+ @handler.send_event(@dead ? :playerKilled : :playerRevived, self);
122
+ when "Stats/Ammo"
123
+ return if(data.size != 8)
124
+
125
+ outData = data.unpack("L<*");
126
+ @ammo = outData[0];
127
+ @maxAmmo = outData[1];
128
+ when "Position"
129
+ begin
130
+ @position = JSON.parse(data, symbolize_names: true);
131
+ rescue
132
+ end
133
+ when "HW/NSwitch"
134
+ @handler.send_event(:navSwitchPressed, self, data.to_i)
135
+ when "HW/Gyro"
136
+ @gyroPose = data.to_sym
137
+ @handler.send_event(:poseChanged, self, @gyroPose);
138
+ when "ZoneUpdate"
139
+ begin
140
+ data = JSON.parse(data, symbolize_names: true);
141
+ rescue JSON::ParserError
142
+ return;
143
+ end
144
+
145
+ @zoneIDs = data[:data];
146
+ if(data[:entered])
147
+ @handler.send_event(:playerEnteredZone, self, data[:entered])
148
+ end
149
+ if(data[:exited])
150
+ @handler.send_event(:playerExitedZone, self, data[:exited])
151
+ end
152
+ else
153
+ super(data, topic);
154
+ end
155
+ end
156
+
157
+ def team=(n)
158
+ n = n.to_i;
159
+ raise ArgumentError, "Team out of range (must be between 0 and 7)" unless n <= 7 and n >= 0;
160
+
161
+ return if @team == n;
162
+ oldT = @team;
163
+ @team = n;
164
+
165
+ _pub_to "CFG/Team", @team, retain: true;
166
+ @handler.send_event :playerTeamChanged, self, oldT;
167
+
168
+ @team;
169
+ end
170
+ def brightness=(n)
171
+ raise ArgumentError, "Brightness must be a valid symbol!" unless @BrightnessMap.include? n;
172
+
173
+ return if @brightness == n;
174
+ oldB = @brightness;
175
+ @brightness = n;
176
+
177
+ n = @BrightnessMap.find_index(n)
178
+
179
+ _pub_to "CFG/Brightness", n, retain: true;
180
+ @handler.send_event :playerBrightnessChanged, self, oldB;
181
+
182
+ @brightness;
183
+ end
184
+
185
+ def _set_dead(d, player = nil)
186
+ dead = (d ? true : false);
187
+ return if @dead == dead;
188
+ @dead = dead;
189
+
190
+ @deathChangeTime = Time.now();
191
+
192
+ _pub_to "CFG/Dead", @dead ? "1" : "0", retain: true;
193
+ @handler.send_event(@dead ? :playerKilled : :playerRevived, self, player);
194
+ end
195
+ def dead=(d)
196
+ _set_dead(d);
197
+ end
198
+ def kill_by(player)
199
+ return if @dead;
200
+ _set_dead(true, player);
201
+ end
202
+ def revive_by(player)
203
+ return unless @dead
204
+ _set_dead(false, player)
205
+ end
206
+
207
+ def ammo=(n)
208
+ unless (n.is_a?(Integer) and (n >= 0))
209
+ raise ArgumentError, "Ammo amount needs to be a positive number!"
210
+ end
211
+
212
+ @ammo = n;
213
+
214
+ _pub_to("Stats/Ammo/Set", n);
215
+ end
216
+
217
+ def gunNo=(n)
218
+ unless (n.is_a?(Integer) and (n >= 0))
219
+ raise ArgumentError, "Gun ID needs to be a positive integer!"
220
+ end
221
+
222
+ return if(@gunNo == n)
223
+
224
+ oldGun = @gunNo;
225
+ @gunNo = n;
226
+ @handler.send_event(:playerGunChanged, self, n, oldGun);
227
+
228
+ _pub_to("CFG/GunNo", n, retain: true);
229
+ end
230
+
231
+ # Return the averaged damage the player's gun should do.
232
+ # This function is very useful to calculate the damage a player did
233
+ # per shot. The returned number tries to average damage to "1 DPS" for
234
+ # all weapons regardless of speed etc., which the application can
235
+ # multiply for a given total damage, creating a more balanced game.
236
+ def gunDamage(number = nil)
237
+ number ||= @gunNo
238
+
239
+ return @GunDamageMultipliers[number-1] || 1;
240
+ end
241
+
242
+ def clear_all_topics()
243
+ super();
244
+
245
+ [ "CFG/Dead", "CFG/GunNo", "CFG/Brightness", "CFG/Team"].each do |t|
246
+ _pub_to(t, "", retain: true);
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end