contrib/tinks: do not publish TankMoveEvents for dead tanks
[nit.git] / contrib / tinks / src / game / tanks.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12
13 # Tank and tank turret related logic
14 module tanks is serialize
15
16 import world
17
18 redef class TGame
19 # All of the tanks in game
20 var tanks = new TankSet
21
22 redef fun do_turn
23 do
24 var turn = super
25 for tank in tanks do tank.do_turn turn
26 return turn
27 end
28 end
29
30 # Stats of a tank kind, its config should be move to the `Story` if we want more than 1
31 class TankRule
32 super Rule
33
34 # Width of this tank
35 var width: Float = 64.0/32.0
36
37 # Height of this tank
38 var length: Float = 100.0/32.0
39
40 # Maximum `health` this tank can normally have
41 var max_health = 4
42
43 # Maximum speed of this tank (in world coordinate units per seconds)
44 var max_speed = 2.0
45
46 # Turret turning speed (in radians per second)
47 var turret_turn_speed = 1.6
48
49 # Waiting time between shots can be fired
50 var turret_cooldown_time = 2.0
51
52 # Maximum `direction_heading` heading, may be double
53 var max_direction = 1.0
54 end
55
56 redef class Story
57 # The main (and only) tank configuration in this game
58 var tanks: Array[TankRule] = [new TankRule(self)]
59 end
60
61 # A tank!
62 class Tank
63 super TTurnable
64 super Ruled
65 redef type R: TankRule
66
67 # In world `Pos` of this entity
68 var pos: Pos
69
70 # Orientation of this entity
71 var heading: Float
72
73 # The turret mounted on this tank
74 var turret = new Turret(self)
75
76 # Commanded direction
77 var direction_heading = 0.0
78
79 # Commanded speed
80 var direction_forwards = 0.0
81
82 # Health of this tank, out of `rule.max_health`
83 var health: Int = rule.max_health is lazy
84
85 redef fun do_turn(turn)
86 do
87 var collisions = next_move_collisions(turn)
88
89 if collisions.is_empty then
90 var next = normal_next_pos(turn)
91 self.pos = next.first
92 self.heading = next.second
93 end
94
95 turret.do_turn turn
96
97 if health > 0 then
98 turn.add new TankMoveEvent(self, pos, direction_heading, direction_forwards, heading, turret.relative_heading)
99 end
100 end
101
102 # What would be the next position if not blocked by terrain features?
103 #
104 # Returns a couple of the new position and heading.
105 fun normal_next_pos(turn: TTurn): Couple[Pos, Float]
106 do
107 var heading = (heading + direction_heading * turn.dts).angle_normalize
108
109 var speed = direction_forwards * rule.max_speed * turn.dts
110 var pos = pos
111 if speed != 0.0 then
112 pos += heading.to_vector(speed)
113 end
114
115 return new Couple[Pos, Float](pos, heading)
116 end
117
118 # Damage this tank, server-side
119 fun hit(turn: TTurn)
120 do
121 var damage = 1
122 var health = health - damage
123
124 if health <= 0 then
125 destroy turn
126 else
127 turn.add new TankHealthChange(self, health)
128 end
129 end
130
131 # Destroy this tank, server-side
132 fun destroy(turn: TTurn)
133 do
134 turn.add new TankDeathEvent(self)
135 turn.game.world.explode(turn, pos, 3)
136 end
137
138 # Collisions on the next move
139 fun next_move_collisions(turn: TTurn): HashSet[Feature]
140 do
141 var next = normal_next_pos(turn)
142 var features = new HashSet[Feature]
143
144 # Use the lines between the corners to detect collisions
145 var corners = corners_at(next)
146 var prev_corner = corners.last
147 for corner in corners do
148 var feature = turn.game.world.first_collision(prev_corner, corner)
149 if feature != null then features.add feature
150 prev_corner = corner
151 end
152
153 return features
154 end
155
156 # Get the 4 corners at a `next` position
157 fun corners_at(next: Couple[Pos, Float]): Array[Pos]
158 do
159 var next_pos = next.first
160 var heading = next.second
161
162 var corners = new Array[Pos]
163
164 var hwy = rule.width/2.0 * (heading+pi/2.0).sin
165 var hwx = rule.width/2.0 * (heading+pi/2.0).cos
166 var hly = rule.length/2.0 * heading.sin
167 var hlx = rule.length/2.0 * heading.cos
168 corners.add new Pos(next_pos.x + hlx + hwx, next_pos.y + hly + hwy)
169 corners.add new Pos(next_pos.x + hlx - hwx, next_pos.y + hly - hwy)
170 corners.add new Pos(next_pos.x - hlx - hwx, next_pos.y - hly - hwy)
171 corners.add new Pos(next_pos.x - hlx + hwx, next_pos.y - hly + hwy)
172
173 return corners
174 end
175 end
176
177 # A tank turret
178 class Turret
179 super TTurnable
180
181 # The `Tank` on which is mounted this turret
182 var tank: Tank
183
184 # Orientation of this turret relative to the tank
185 var relative_heading = 0.0
186
187 # Absolute orientation of this turret
188 fun heading: Float do return (tank.heading+relative_heading).angle_normalize
189
190 # Current target to aim for and fire upon
191 var target: nullable Pos = null
192
193 # Seconds left before the turret can open fire again
194 var cooldown = 0.0
195
196 redef fun do_turn(turn)
197 do
198 if cooldown > 0.0 then
199 cooldown = cooldown - turn.dts
200
201 if cooldown <= 0.0 then
202 cooldown = 0.0
203
204 # Notify clients
205 turn.add new TurretReadyEvent(tank)
206 end
207 end
208
209 var target = target
210 if target != null then
211
212 var angle_to_target = tank.pos.atan2(target)
213 var d = (heading - angle_to_target).angle_normalize
214
215 var max_angle = tank.rule.turret_turn_speed * turn.dts
216 if d.abs < max_angle then
217 self.relative_heading = angle_to_target - tank.heading
218
219 if cooldown == 0.0 then
220 # On target, fire
221 fire turn
222 self.target = null
223 end
224 else
225 # Turn towards target
226 if d < 0.0 then
227 self.relative_heading += max_angle
228 else self.relative_heading -= max_angle
229 end
230 end
231 end
232
233 # Open fire!
234 fun fire(turn: TTurn)
235 do
236 var dst = target
237 assert dst != null
238
239 # Is there something between the tank and the target?
240 var hit = turn.game.world.first_collision(tank.pos, dst)
241 if hit != null then dst = hit.pos
242
243 # Events!
244 turn.add new OpenFireEvent(tank)
245 turn.game.world.explode(turn, dst, 2)
246
247 # The turret need time to reload, cooldown!
248 cooldown = tank.rule.turret_cooldown_time
249 end
250 end
251
252 redef class World
253 redef fun explode(turn, center, force)
254 do
255 super
256
257 for tank in game.tanks do
258 if tank.health == 0 then continue
259 if center.dist(tank.pos) <= force.to_f + 1.0 then
260 tank.hit turn
261 end
262 end
263 end
264 end
265
266 # A collection of `Tank` that could be optimized
267 class TankSet
268 super HashSet[Tank]
269 end
270
271 # A `tank` centric order
272 abstract class TankOrder
273 super TOrder
274
275 # The `Tank` at the center of this order
276 var tank: Tank
277 end
278
279 # A command to change the behavior of `tank`
280 class TankDirectionOrder
281 super TankOrder
282
283 # Desired direction, in [-1.0..1.0]
284 var direction_heading: Float
285
286 # Desired speed, in [-1.0..1.0]
287 var direction_forwards: Float
288
289 redef fun apply(game)
290 do
291 # TODO use events
292 var direction_heading = direction_heading
293 direction_heading = direction_heading.min(1.0).max(-1.0)
294 tank.direction_heading = direction_heading*tank.rule.max_direction
295
296 var direction_forwards = direction_forwards
297 direction_forwards = direction_forwards.min(1.0).max(-1.0)
298 tank.direction_forwards = direction_forwards*tank.rule.max_speed
299 end
300 end
301
302 # Order to aim and fire at `target`
303 class AimAndFireOrder
304 super TankOrder
305
306 # Target for the turret
307 var target: Pos
308
309 redef fun apply(game)
310 do
311 tank.turret.target = target
312 end
313 end
314
315 # A `tank` centric event
316 abstract class TankEvent
317 super TEvent
318
319 # The `Tank` at the center of this event
320 var tank: Tank
321 end
322
323 # `tank` opens fire
324 class OpenFireEvent
325 super TankEvent
326 end
327
328 # The turret of `tank` is ready to open fire
329 class TurretReadyEvent
330 super TankEvent
331 end
332
333 # `tank` has been destroyed
334 class TankDeathEvent
335 super TankEvent
336
337 redef fun apply(game)
338 do
339 tank.health = 0
340 game.tanks.remove tank
341 end
342 end
343
344 # The health of `tank` changes to `new_health`
345 class TankHealthChange
346 super TankEvent
347
348 # The new health for `tank`
349 var new_health: Int
350
351 redef fun apply(game)
352 do
353 tank.health = new_health
354 end
355 end
356
357 # A `tank` moved
358 #
359 # TODO this event is too big, divide in 2 or more and move more logic client-side
360 class TankMoveEvent
361 super TankEvent
362
363 # The position
364 var pos: Pos
365
366 # The direction of the "wheels"
367 var direction_heading: Float
368
369 # The speed
370 var direction_forwards: Float
371
372 # Orientation of the tank
373 var tank_heading: Float
374
375 # Orientation of the turret
376 var turret_heading: Float
377
378 redef fun apply(game)
379 do
380 tank.pos = pos
381 tank.direction_heading = direction_heading
382 tank.direction_forwards = direction_forwards
383 tank.heading = tank_heading
384 tank.turret.relative_heading = turret_heading
385 end
386 end