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,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