9d8296503cf3fabb6edcd83a9b137640907a360d
[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 turn.add new TankMoveEvent(self, pos, direction_heading, direction_forwards, heading, turret.relative_heading)
98 end
99
100 # What would be the next position if not blocked by terrain features?
101 #
102 # Returns a couple of the new position and heading.
103 fun normal_next_pos(turn: TTurn): Couple[Pos, Float]
104 do
105 var heading = (heading + direction_heading * turn.dts).angle_normalize
106
107 var speed = direction_forwards * rule.max_speed * turn.dts
108 var pos = pos
109 if speed != 0.0 then
110 pos += heading.to_vector(speed)
111 end
112
113 return new Couple[Pos, Float](pos, heading)
114 end
115
116 # Damage this tank, server-side
117 fun hit(turn: TTurn)
118 do
119 var damage = 1
120 var health = health - damage
121
122 if health <= 0 then
123 destroy turn
124 else
125 turn.add new TankHealthChange(self, health)
126 end
127 end
128
129 # Destroy this tank, server-side
130 fun destroy(turn: TTurn)
131 do
132 turn.add new TankDeathEvent(self)
133 turn.game.world.explode(turn, pos, 3)
134 end
135
136 # Collisions on the next move
137 fun next_move_collisions(turn: TTurn): HashSet[Feature]
138 do
139 var next = normal_next_pos(turn)
140 var features = new HashSet[Feature]
141
142 # Use the lines between the corners to detect collisions
143 var corners = corners_at(next)
144 var prev_corner = corners.last
145 for corner in corners do
146 var feature = turn.game.world.first_collision(prev_corner, corner)
147 if feature != null then features.add feature
148 prev_corner = corner
149 end
150
151 return features
152 end
153
154 # Get the 4 corners at a `next` position
155 fun corners_at(next: Couple[Pos, Float]): Array[Pos]
156 do
157 var next_pos = next.first
158 var heading = next.second
159
160 var corners = new Array[Pos]
161
162 var hwy = rule.width/2.0 * (heading+pi/2.0).sin
163 var hwx = rule.width/2.0 * (heading+pi/2.0).cos
164 var hly = rule.length/2.0 * heading.sin
165 var hlx = rule.length/2.0 * heading.cos
166 corners.add new Pos(next_pos.x + hlx + hwx, next_pos.y + hly + hwy)
167 corners.add new Pos(next_pos.x + hlx - hwx, next_pos.y + hly - hwy)
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
171 return corners
172 end
173 end
174
175 # A tank turret
176 class Turret
177 super TTurnable
178
179 # The `Tank` on which is mounted this turret
180 var tank: Tank
181
182 # Orientation of this turret relative to the tank
183 var relative_heading = 0.0
184
185 # Absolute orientation of this turret
186 fun heading: Float do return (tank.heading+relative_heading).angle_normalize
187
188 # Current target to aim for and fire upon
189 var target: nullable Pos = null
190
191 # Seconds left before the turret can open fire again
192 var cooldown = 0.0
193
194 redef fun do_turn(turn)
195 do
196 if cooldown > 0.0 then
197 cooldown = cooldown - turn.dts
198
199 if cooldown <= 0.0 then
200 cooldown = 0.0
201
202 # Notify clients
203 turn.add new TurretReadyEvent(tank)
204 end
205 end
206
207 var target = target
208 if target != null then
209
210 var angle_to_target = tank.pos.atan2(target)
211 var d = (heading - angle_to_target).angle_normalize
212
213 var max_angle = tank.rule.turret_turn_speed * turn.dts
214 if d.abs < max_angle then
215 self.relative_heading = angle_to_target - tank.heading
216
217 if cooldown == 0.0 then
218 # On target, fire
219 fire turn
220 self.target = null
221 end
222 else
223 # Turn towards target
224 if d < 0.0 then
225 self.relative_heading += max_angle
226 else self.relative_heading -= max_angle
227 end
228 end
229 end
230
231 # Open fire!
232 fun fire(turn: TTurn)
233 do
234 var dst = target
235 assert dst != null
236
237 # Is there something between the tank and the target?
238 var hit = turn.game.world.first_collision(tank.pos, dst)
239 if hit != null then dst = hit.pos
240
241 # Events!
242 turn.add new OpenFireEvent(tank)
243 turn.game.world.explode(turn, dst, 2)
244
245 # The turret need time to reload, cooldown!
246 cooldown = tank.rule.turret_cooldown_time
247 end
248 end
249
250 redef class World
251 redef fun explode(turn, center, force)
252 do
253 super
254
255 for tank in game.tanks do
256 if tank.health == 0 then continue
257 if center.dist(tank.pos) <= force.to_f + 1.0 then
258 tank.hit turn
259 end
260 end
261 end
262 end
263
264 # A collection of `Tank` that could be optimized
265 class TankSet
266 super HashSet[Tank]
267 end
268
269 # A `tank` centric order
270 abstract class TankOrder
271 super TOrder
272
273 # The `Tank` at the center of this order
274 var tank: Tank
275 end
276
277 # A command to change the behavior of `tank`
278 class TankDirectionOrder
279 super TankOrder
280
281 # Desired direction, in [-1.0..1.0]
282 var direction_heading: Float
283
284 # Desired speed, in [-1.0..1.0]
285 var direction_forwards: Float
286
287 redef fun apply(game)
288 do
289 # TODO use events
290 var direction_heading = direction_heading
291 direction_heading = direction_heading.min(1.0).max(-1.0)
292 tank.direction_heading = direction_heading*tank.rule.max_direction
293
294 var direction_forwards = direction_forwards
295 direction_forwards = direction_forwards.min(1.0).max(-1.0)
296 tank.direction_forwards = direction_forwards*tank.rule.max_speed
297 end
298 end
299
300 # Order to aim and fire at `target`
301 class AimAndFireOrder
302 super TankOrder
303
304 # Target for the turret
305 var target: Pos
306
307 redef fun apply(game)
308 do
309 tank.turret.target = target
310 end
311 end
312
313 # A `tank` centric event
314 abstract class TankEvent
315 super TEvent
316
317 # The `Tank` at the center of this event
318 var tank: Tank
319 end
320
321 # `tank` opens fire
322 class OpenFireEvent
323 super TankEvent
324 end
325
326 # The turret of `tank` is ready to open fire
327 class TurretReadyEvent
328 super TankEvent
329 end
330
331 # `tank` has been destroyed
332 class TankDeathEvent
333 super TankEvent
334
335 redef fun apply(game)
336 do
337 tank.health = 0
338 game.tanks.remove tank
339 end
340 end
341
342 # The health of `tank` changes to `new_health`
343 class TankHealthChange
344 super TankEvent
345
346 # The new health for `tank`
347 var new_health: Int
348
349 redef fun apply(game)
350 do
351 tank.health = new_health
352 end
353 end
354
355 # A `tank` moved
356 #
357 # TODO this event is too big, divide in 2 or more and move more logic client-side
358 class TankMoveEvent
359 super TankEvent
360
361 # The position
362 var pos: Pos
363
364 # The direction of the "wheels"
365 var direction_heading: Float
366
367 # The speed
368 var direction_forwards: Float
369
370 # Orientation of the tank
371 var tank_heading: Float
372
373 # Orientation of the turret
374 var turret_heading: Float
375
376 redef fun apply(game)
377 do
378 tank.pos = pos
379 tank.direction_heading = direction_heading
380 tank.direction_forwards = direction_forwards
381 tank.heading = tank_heading
382 tank.turret.relative_heading = turret_heading
383 end
384 end