lzrtag-base 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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