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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e1aba4b3577f72ee4c4aca45caca65e157cfa522
4
+ data.tar.gz: 9582f9f9a6c478c97de4c837ad6eadfb2c44c451
5
+ SHA512:
6
+ metadata.gz: a2a6bcaeefeddece09a26ee91e9dd608aa5d8450dc4ff943b23d01ad995f07533cdcc278336b37e92873de1f9cc4ce468e66df5806225e01c8ce23acb18eb313
7
+ data.tar.gz: f5dea5415de79a54ab9c4d5bd7e81e774daae5498d4b6005c21a68c5d9ddba6f9ca7540b1159f78cffa786507d19d25f52a1d3153320e70c64234db833616f64
@@ -0,0 +1,98 @@
1
+
2
+ # LZRTag base library gem
3
+
4
+ This is the full documentation of the LZRTag ruby gem library.
5
+ It includes examples for the different elements of this code, and even a basic
6
+ deathmatch game to show off the mechanics more interactively.
7
+
8
+ ## Prerequisites
9
+ Let's first start off by installing and setting up everything that's needed
10
+ to run a LZRTag game. We'll assume you have at least one, preferrably as many as possible,
11
+ LZRTag hardware sets with the latest firmware - consult the [HackADay page](bit.ly/XASLZR) as well as the [GitHub Wiki page](https://github.com/XasWorks/LZRTag/wiki/Assembly) on soldering them up.
12
+ We will also need a WiFi network and a MQTT Server, either local or hosted, like
13
+ Flespi.io, with the LZRTag hardware set up to connect to the server. Successful
14
+ connection is indicated by a smooth, very slow waver of the blue connection LED
15
+ on the sets. If you want to double-check connection you can subscribe to the
16
+ Lasertag/# topic on your MQTT server and check if the sets are pinging properly.
17
+
18
+ Once we are sure that the sets are connected properly run `gem install lzrtag-base`.
19
+ The latest ruby is recommended for performance reasons, however, this code should work
20
+ with anything above 2.3.
21
+
22
+ ## Testing on IRB
23
+ As a first experience, I'd recommend playing with the interface class on the interactive
24
+ ruby console. It's a great way to see the immediate effects of various commands
25
+ on the set, and is kinda fun, too.
26
+
27
+ Let's first start up the console and require the library.
28
+ ```IRB
29
+ irb(main):001:0> require 'lzrtag.rb'
30
+ => true
31
+ irb(main):002:0>
32
+ ```
33
+ This will load all necessary files, and will also provide a few helper functions
34
+ to access the correct classes etc.
35
+ Next, let's establish an MQTT connection and start the handler.
36
+
37
+ ```IRB
38
+ irb(main):002:0> mqtt = MQTT::SubHandler.new('localhost'); # Or your address
39
+ MQTT: localhost trying reconnect...
40
+ MQTT: localhost connected!
41
+ irb(main):003:0> handler = LZRTag.Handler.new(mqtt);
42
+ I LZR::Handler init finished
43
+ ```
44
+
45
+ Awesome! The handler should now be connected to your server, and should have registered
46
+ new players. If you look at your MQTT Server, for example with MQTT-Spy, you
47
+ should see that any connected players will now have an ID.
48
+
49
+ Let's see what players the system has already registered:
50
+
51
+ ```irb
52
+ irb(main):004:0> handler.each do |player| puts player; end;
53
+ #<Player:DEVICE_ID#OFFLINE, Team=0, Battery=4.19, Ping=218ms>
54
+ ```
55
+
56
+ Players have a nice inspect function so they don't clutter the console too much when printed. Neat, huh?
57
+
58
+ Alright, we need to fetch one single player to play with. If you have a device ID
59
+ (their MAC, used on the MQTT network to identify them) you can use that, otherwise
60
+ use the ShotID (number, 0..255). Let's do that now - and while we're at it,
61
+ let's identify the player:
62
+
63
+ ```irb
64
+ irb(main):005:0> player = handler["BC.DD.C2.D0.63.F8"]
65
+ => #<Player:#OFFLINE, Team=0, Battery=4.19, Heap=0, Ping=218ms>
66
+ irb(main):006:0> player.hit
67
+ ```
68
+
69
+ You should have seen and heard one of the sets flashing up - that's the one we
70
+ just retrieved. We can also colour it green and make it a little brighter!
71
+
72
+ ```irb
73
+ irb(main):007:0> player.team = 2
74
+ => 2
75
+ irb(main):08:0> player.brightness = :active
76
+ => :active
77
+ ```
78
+
79
+ Alright, awesome - let's not go too deep into what you can do with the player.
80
+ That's what the Ruby documentation is for, after all.
81
+
82
+ ## And now?
83
+ Well, more complex behaviours can be added with so-called hooks. These are extensions
84
+ that perform automated tasks based on different events in the game, and use the
85
+ internal "event system".
86
+ There is a list of currently used events available, as well as documentation on
87
+ the hooks class. You can make your own, or use the standard hooks provided in this
88
+ library - it's up to you!
89
+
90
+ Since a game is way more than just behaviours though, there are also "Games".
91
+ Each game is a collection of hooks, which provide the behaviour of the game. However,
92
+ they also include a description of different phases - stuff like "initializing",
93
+ "playing", "selecting teams", etc.
94
+ The application can seamlessly switch between phases of a game, with each phase
95
+ defining which hooks to enable or disable and what to do every game tick.
96
+
97
+ It's best to look right into the documentation on those classes, since demonstrating
98
+ them bit by bit isn't really possible.
@@ -0,0 +1,117 @@
1
+
2
+
3
+ This file will document all known internal Lasertag events and their purpose.
4
+ Users may add their own events via the standard system by simply sending a new event -
5
+ however, only the following event symbols and payloads will be used by the provided software.
6
+
7
+
8
+ ## Game related events
9
+
10
+ ###### :gameTick
11
+ Triggered in regular intervals, default 0.1s
12
+ Payload is [deltaTime since last tick].
13
+ It is only triggered when a game is present.
14
+
15
+ *Game class alternative:* Describe per-tick code using the
16
+ "phase" DSL. It allows easy and clean specification of tick code.
17
+
18
+ ###### :gameStarting
19
+ Triggered when a new game is selected, either in Ruby or via MQTT.
20
+ Payload is [newGame]
21
+
22
+ ###### :gamePhaseEnds
23
+ Called immediately before the phase of a game is changed. Can be used to de-init some settings, or to finalize for example a player
24
+ team selection.
25
+ The payload is [oldPhase, nextPhase]
26
+
27
+ *Game class alternative:* Descibe per-tick code using the "phase_end" DSL.
28
+
29
+ ###### :gamePhaseStarts
30
+ Called immediately after the game's phase was changed to a new phase. Can, and should, be used to prepare a game's setting.
31
+ The payload is [newPhase, oldPhase]
32
+
33
+ *Game class alternative:* Descibe per-tick code using the "phase_prep" DSL.
34
+
35
+ ###### :playerEnteredGame
36
+ Called whenever a player is added to the list of game players.
37
+ Payload is [thePlayer]
38
+
39
+ ###### :playerLeftGame
40
+ Called whenever a player was removed from the list of game players.
41
+ Payload is [thePlayer]
42
+
43
+ ## Player config options
44
+ The following options will be triggered whenever some config of the player changes, and are mainly useful to trigger events or
45
+ keep track of the players etc.
46
+
47
+ ###### :playerRegistered
48
+ Triggered when a brand new player is found on the MQTT network.
49
+ This will effectively only happen at the start of the server, or when a new player is turned on.
50
+ Payload is [thePlayer]
51
+
52
+ ###### :playerConnected
53
+ Triggered whenever a player reconnects to the server, either when restarting the server or mid-game after a disconnect.
54
+ Payload is [thePlayer]
55
+
56
+ ###### :playerDisconnected
57
+ Triggered whenever a player looses connection to the MQTT server (detected via the LWT topic).
58
+ Payload is [thePlayer]
59
+
60
+ ## Hardware player signals
61
+ These signals are sent in response to various player actions, as well as most of Hardware Player's
62
+ configuration changes.
63
+
64
+ ###### :navSwitchPressed
65
+ Triggered when the player presses a navigation switch.
66
+ Payload is [thePlayer, direction (0..2)]
67
+
68
+ ###### :poseChanged
69
+ Triggered when the player changes the pose of the weapon. Possible poses are listed
70
+ under the Lasertag Hardware Player's "gyroPose" attribute
71
+ Payload is [thePlayer, poseSymbol]
72
+
73
+ ###### :playerTeamChanged
74
+ Triggered when the team of a player is changed.
75
+ Payload is [thePlayer, newTeam (0..7), oldTeam (0..7)]
76
+
77
+ ###### :playerBrightnessChanged
78
+ Triggered when the brightness of a player is changed. For a list of brightness
79
+ symbols, see Hardware Player's "brightness" attribute.
80
+ Payload is [thePlayer, newBrightness, oldBrightness]
81
+
82
+ ###### :playerGunChanged
83
+ Triggered when the gun number of the player is changed.
84
+ Payload is [thePlayer, newGun, oldGun]
85
+
86
+ *TODO:* The gun number should be replaced by an ID string, when the
87
+ gun configuration is moved to a Filesystem-based one (possibly by loading from JSON)
88
+
89
+ ## Player life signals
90
+ These signals are sent whenever the player's HP count or life status is changed,
91
+ and let the application handle things like scoring damage and kills, and triggering
92
+ things like the heartbeat.
93
+
94
+ ###### :playerRegenerated
95
+ Triggered whenever the player gains a bit of health, this can be by self-healing or
96
+ by a medic or similar providing health.
97
+ Payload is [thePlayer, deltaLife, sourcePlayer | nil]
98
+
99
+ ###### :playerFullyRegenerated
100
+ Triggered similarly to :playerRegenerated, but only whenever the player has fully
101
+ regenerated and has reached maxLife again.
102
+ Payload is [thePlayer, sourcePlayer | nil]
103
+
104
+ ###### :playerHurt
105
+ Triggered whenever a player is hurt after a hit arbitration
106
+ and damage calculation, or by other game events.
107
+ Payload is [thePlayer, deltaLife]
108
+
109
+ ###### :playerRevived
110
+ Triggered whenever a player was revivied, either by a timeout
111
+ or by regenerating enough health, or by another person.
112
+ Payload is [thePlayer, sourcePlayer]
113
+
114
+ ###### :playerKilled
115
+ Triggered whenever a player was killed, often by another player,
116
+ but can be by system damage (killzones etc.)
117
+ Payload is [thePlayer, sourcePlayer | nil]
@@ -0,0 +1,74 @@
1
+ # MQTT Branch setup description
2
+
3
+ ## Weapon Branches
4
+ The weapons are the main unit of the Lasertag system. As such, they use a specific
5
+ branch setup to minimize traffic to them, while making code maintaining easier.
6
+
7
+ The basic branch setup is:
8
+ > Lasertag/Players/**ID**/**Keys...**
9
+
10
+ - **ID** is the MAC of the Player, allowing unique identification of
11
+ each set out of the box.
12
+ - **Key** is a one- or two-layer key of the attribute to change.
13
+ Attributes are grouped, making it easier to subscribe to one group of attributes
14
+ without having to worry about the others.
15
+
16
+ In the following, the different groups of keys will be outlined, followed by
17
+ a description of each attribute per group.
18
+
19
+ ### Non-Grouped attributes
20
+
21
+ |Keys|Data|Descrition|Retained|
22
+ |--|--|--|--|
23
+ |Connection|String|LWT Topic of the sets. "OK" if connected.|Yes|
24
+
25
+ ### Config attributes
26
+
27
+ Any attributes under this category can be found under "/CFG/**KEY**".
28
+ They are slow-changing configuration parameters for the weapons.
29
+
30
+ |Key|Data|Descrition|Retained|
31
+ |--|--|--|--|
32
+ |Team|STR Number|Team number, 0 to 7|Yes|
33
+ |Brightness|STR Number|Brightness preset number. In Ruby, handled by symbols|Yes|
34
+ |Name|String|User-set name of the weapon|Yes|
35
+ |Dead|"1" or "0"|Sets the alive-ness of the gun.|Yes|
36
+ |Dead/Timed|STR Number|Number, in seconds, to kill the weapon for. Auto-revives|No|
37
+ |GunNo|STR Number|Gun preset number. 0 or "" disable shooting|Yes|
38
+ |Marked|STR Number|Color to mark the set with. 1-7 use team colors, custom colors can be used|Yes|
39
+ |Heartbeat|"1" or "0"|Enables/Disables heartbeat of the weapon|Yes|
40
+ |Hit|STR Number|Number, in seconds, to make the gun show that it's hit|No|
41
+ |Vibrate|STR Number|Number, in seconds, for which to make the gun vibrate|No|
42
+
43
+ ### Sound attributes
44
+
45
+ Any attributes under this category are found under "/Sound/**KEY**"
46
+ They all relate to noises the gun can make.
47
+
48
+ |Key|Data|Descrition|Retained|
49
+ |--|--|--|--|
50
+ |Note|uint32_t[3]|Raw note play data. Frequency(Hz), Volume (0..2^16), Duration(ms)|No|
51
+ |File|String|Name of a raw audio file to play.|No|
52
+
53
+ ### Hardware attributes
54
+
55
+ Any attributes under this category are found under "/HW/**KEY**"
56
+ They all relate to the hardware of the system, such as battery etc.
57
+
58
+ |Key|Data|Descrition|Retained|
59
+ |--|--|--|--|
60
+ |Ping|uint32_t[3]|Battery, free-heap and ping data|No|
61
+ |NSwitch|STR Number|Navigation switch press info (0-3)|No|
62
+ |Gyro|String|Clear string of the pose the gyroscope is reporting|Yes|
63
+
64
+ ### Statistics Attributes
65
+
66
+ Any attributes under this category are found under "/Stats/**KEY**"
67
+ They all relate to various game statistics that do not directly influence the hardware, but are still relevant to the game and how it plays out.
68
+
69
+
70
+ |Key|Data|Descrition|Retained|
71
+ |--|--|--|--|
72
+ |HP|STR Number|Float number, 0 to 100 (or more with shields)|Yes|
73
+ |Ammo|uint32_t[2]|Current and Max ammo of the weapon|Yes|
74
+ |Ammo/Set|STR Number|Give the player some ammo (*WIP*)|No|
@@ -0,0 +1,13 @@
1
+
2
+ module LZRTag
3
+ def self.Handler()
4
+ return LZRTag::Handler::Game;
5
+ end
6
+ end
7
+
8
+ require_relative 'lzrtag/handler/game_handler.rb'
9
+ require_relative 'lzrtag/hooks/standard_hooks.rb'
10
+
11
+ require_relative 'lzrtag/game/base_game.rb'
12
+
13
+ require_relative 'lzrtag/map/map_set.rb'
@@ -0,0 +1,220 @@
1
+
2
+ require_relative '../hooks/base_hook.rb'
3
+
4
+ # @author Xasin
5
+ module LZRTag
6
+ module Game
7
+ # The base game class.
8
+ # It implements a DSL that allows users to easily
9
+ # define their own games, and hooks in with the
10
+ # game event system
11
+ # @example
12
+ # class OwnGame < LZRTag::Game::Base
13
+ # def initialize(handler)
14
+ # super(handler);
15
+ # @tickTime = 1;
16
+ # end
17
+ #
18
+ # hook :dmgHook, LZRTag::Hook::Damager
19
+ #
20
+ # phase :running do |deltaTick|
21
+ # puts "I tick!"
22
+ # end
23
+ #
24
+ # on :playerRegistered do |newPlayer|
25
+ # puts "Hello new player!";
26
+ # end
27
+ # end
28
+ #
29
+ # handler.start_game(OwnGame)
30
+ # # Alternatively, register the game and let it be activated via MQTT
31
+ # handler.register_game("My Game", OwnGame);
32
+ class Base < Hook::Base
33
+ # Returns a list of all currently instantiated hooks
34
+ attr_reader :hookList
35
+ # Returns the current per-tick target time. Can, and should, be set during
36
+ # constructor to control the granularity of the game
37
+ attr_reader :tickTime
38
+
39
+ # Returns a list of the known phases of this game
40
+ attr_reader :phases
41
+
42
+ # @private
43
+ # This function is meant for the DSL,
44
+ # to allow adding to the class itself
45
+ def self.get_phase_map()
46
+ @globalPhaseMap ||= Hash.new();
47
+ return @globalPhaseMap;
48
+ end
49
+ # @private
50
+ # @see get_phase_map()
51
+ def self.get_phase_prep_map()
52
+ @globalPhasePrepMap ||= Hash.new();
53
+ return @globalPhasePrepMap;
54
+ end
55
+ # @private
56
+ # @see get_phase_map()
57
+ def self.get_phase_end_map()
58
+ @globalPhaseEndMap ||= Hash.new();
59
+ return @globalPhaseEndMap;
60
+ end
61
+ # @private
62
+ # @see get_phase_map()
63
+ def self.get_hooks()
64
+ @globalHookList ||= Hash.new();
65
+ return @globalHookList;
66
+ end
67
+
68
+ # This function returns a list of possible phases
69
+ # and their tick callbacks
70
+ def get_phase_map()
71
+ return @phaseMap
72
+ end
73
+
74
+ # Initializes a generic game handler.
75
+ # This function is usually not called by the user,
76
+ # but instead by the LZRTag::Handler::Game when
77
+ # starting a game.
78
+ # @param handler [LZRTag::Handler::Base] any valid
79
+ # LZRTag handler that the game shall follow
80
+ #
81
+ def initialize(handler)
82
+ super(handler)
83
+
84
+ @hookList = Array.new();
85
+ self.class.get_hooks().each do |hookID, hookData|
86
+ @hookList << hookData[0].new(@handler, **hookData[1])
87
+ end
88
+
89
+ @tickTime = 0.1;
90
+
91
+ @phaseMap = self.class.get_phase_map();
92
+ @phasePrepMap = self.class.get_phase_prep_map();
93
+ @phaseEndMap = self.class.get_phase_end_map();
94
+
95
+ @phases = [@phaseMap.keys, @phasePrepMap.keys].flatten.uniq
96
+
97
+ @phaseTime = 0;
98
+ @phaseLastTime = 0;
99
+ end
100
+
101
+ # @!group DSL functions
102
+
103
+ # DSL function to add a Hook type to this game.
104
+ # Any hook type added by this function will be instantiated when the game
105
+ # itself is instantiated, and will be linked in with the internal game
106
+ # signals.
107
+ # @param hookID [Symbol] The ID of the hook, used for later referencing
108
+ # @param hookType [Hook::Base] The class of the hook to instantiate
109
+ # @param hookOptions [Hash] A hash of options to pass to the constructor of the hook
110
+ def self.hook(hookID, hookType, hookOptions = {})
111
+ raise ArgumentError, "Hook ID needs to be a symbol!" unless hookID.is_a? Symbol
112
+ unless hookType.is_a? Class and hookType < LZRTag::Hook::Base
113
+ raise ArgumentError, "Hook needs to be a LZR::Hook!"
114
+ end
115
+ raise ArgumentError, "Hook options need to be a hash" unless hookOptions.is_a? Hash
116
+ get_hooks()[hookID] << [hookType, hookOptions];
117
+ end
118
+
119
+ # DSL function to provide a phase tick code to this game
120
+ # The block provided to this function will be executed every game tick,
121
+ # with the delta-time since last tick as parameter.
122
+ # @param phaseName [Symbol] The name of the phase during which to execute this code
123
+ # @yield [deltaTime] Calls this block every game-tick during the specified phase
124
+ def self.phase(phaseName, &block)
125
+ raise ArgumentError, "Block needs to be given!" unless block_given?
126
+
127
+ phaseName = [phaseName].flatten
128
+ phaseName.each do |evt|
129
+ unless (evt.is_a? Symbol)
130
+ raise ArgumentError, "Phase needs to be a symbol or array of symbols!"
131
+ end
132
+ self.get_phase_map()[evt] = block;
133
+ end
134
+ end
135
+
136
+ # DSL function to provide a callback immediately after switching to a new phase.
137
+ # The provided block will be called only once, right after a phase switch happened.
138
+ # As such, it can be used to prepare game timer settings, player classes, etc.
139
+ # @param phaseName [Symbol] The name of the phase before which to execute
140
+ # @yield [oldPhase] Calls the block right after a switch, with the old phase as parameter
141
+ def self.phase_prep(phaseName, &block)
142
+ raise ArgumentError, "Block needs to be given!" unless block_given?
143
+
144
+ phaseName = [phaseName].flatten
145
+ phaseName.each do |evt|
146
+ unless (evt.is_a? Symbol)
147
+ raise ArgumentError, "Phase needs to be a symbol or array of symbols!"
148
+ end
149
+ self.get_phase_prep_map()[evt] ||= Array.new()
150
+ self.get_phase_prep_map()[evt] << block;
151
+ end
152
+ end
153
+
154
+ # DSL function to provide a callback immediately before switching to a new phase.
155
+ # The provided block is guaranteed to execute before any phase and phase_prep blocks,
156
+ # giving the user an option to reset and clean up after themselves.
157
+ # @param phaseName [Symbol] Name of the phase after which to call this block
158
+ # @yield [newPhase] Calls the block right before the switch, with the upcoming phase as parameter
159
+ def self.phase_end(phaseName, &block)
160
+ raise ArgumentError, "Block needs to be given!" unless block_given?
161
+
162
+ phaseName = [phaseName].flatten
163
+ phaseName.each do |evt|
164
+ unless (evt.is_a? Symbol)
165
+ raise ArgumentError, "Phase needs to be a symbol or array of symbols!"
166
+ end
167
+ self.get_phase_end_map()[evt] ||= Array.new()
168
+ self.get_phase_end_map()[evt] << block;
169
+ end
170
+ end
171
+
172
+ # @!endgroup
173
+
174
+ # @private
175
+ def consume_event(evt, data)
176
+ super(evt, data);
177
+
178
+ case evt
179
+ when :gameTick
180
+ handle_game_tick(*data);
181
+ when :gamePhaseStarts
182
+ handle_phase_change();
183
+ when :gamePhaseEnds
184
+ if @phaseEndMap[data[0]]
185
+ @phaseEndMap[data[0]].each do |cb|
186
+ instance_exec(&cb);
187
+ end
188
+ end
189
+ end
190
+
191
+ @hookList.each do |hook|
192
+ hook.consume_event(evt, data);
193
+ end
194
+ end
195
+
196
+ # @private
197
+ def handle_phase_change
198
+ @phaseTime = 0;
199
+
200
+ return unless @phasePrepMap[@handler.gamePhase]
201
+ @phasePrepMap[@handler.gamePhase].each do |cb|
202
+ instance_exec(&cb);
203
+ end
204
+ end
205
+
206
+ # @private
207
+ def handle_game_tick(dT)
208
+ phase = @handler.gamePhase
209
+ return unless @phaseMap[phase];
210
+ return if phase == :idle;
211
+
212
+ @phaseLastTime = @phaseTime;
213
+ @phaseTime += dT;
214
+ @handler.mqtt.publish_to "Lasertag/Game/Timer", @phaseTime
215
+
216
+ instance_exec(dT, &@phaseMap[phase]);
217
+ end
218
+ end
219
+ end
220
+ end