9e437a18195a47057971ee866c42ddcb9c27ba94
[nit.git] / contrib / action_nitro / src / game / core.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 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Core game logic of Action Nitro
16 module core
17
18 import geometry
19
20 # Root game object of the whole game logic
21 #
22 # Used as a visitor in all methods modifying the state of the world.
23 class World
24
25 # The player, if the game has started
26 var player: nullable Player = null is writable
27
28 # All live platforms
29 var planes = new Array[Platform]
30
31 # All live enemies
32 var enemies = new Array[Enemy]
33
34 # All live bullets shot by `enemies`
35 var enemy_bullets = new Array[Bullet]
36
37 # All live bullets shot by `player`
38 var player_bullets = new Array[Bullet]
39
40 # All live powerups
41 var powerups = new Array[Powerup]
42
43 # Current open parachute, if any
44 var parachute: nullable Parachute = null is writable
45
46 # Altitude at which to enter space and trigger the boss
47 var boss_altitude = 1200.0
48
49 # Boss, the ISS occupied by bad guys
50 var boss: nullable Boss = null is writable
51
52 # Runtime of this game, in seconds
53 var t = 0.0
54
55 # Total score
56 var score = 0
57
58 # Approximate view of the camera, used to spawn stuff outside of the camera
59 fun camera_view: Box[Float]
60 do
61 # TODO update from client
62 var border = 100.0
63 var player = player
64 if player != null then
65 return new Box[Float](
66 player.center.x-border, player.center.x+border,
67 player.center.y+border, player.center.y-border)
68 else
69 return new Box[Float](
70 -border, border,
71 border, -border)
72 end
73 end
74
75 # Update the game logic for events over `dt` seconds
76 fun update(dt: Float)
77 do
78 t += dt
79
80 # Visit all other game logic objects
81 for plane in planes.reverse_iterator do plane.update(dt, self)
82 for enemy in enemies.reverse_iterator do enemy.update(dt, self)
83
84 var player = player
85 if player != null then player.update(dt, self)
86
87 for i in enemy_bullets.reverse_iterator do i.update(dt, self)
88 for i in player_bullets.reverse_iterator do i.update(dt, self)
89
90 for powerup in powerups.reverse_iterator do powerup.update(dt, self)
91 if parachute != null then parachute.update(dt, self)
92
93 # Check if the player has reached the boss
94 var cam = camera_view
95 if player != null and player.altitude >= boss_altitude and boss == null then
96 var w = 64.0
97 var boss = new Boss(new Point3d[Float](player.center.x, cam.top - 20.0, 0.0), w, 4.0, new Ak47)
98 self.boss = boss
99 enemies.add boss
100
101 for i in 6.times do
102 var e = new WalkingEnemy(new Point3d[Float](boss.center.x & (w/2.0), boss.center.y + 4.0, -1.0.rand), 4.0, 4.0, new Pistol)
103 enemies.add e
104 end
105 end
106 end
107
108 # Explosion at `center` of the given `force`
109 fun explode(center: Point3d[Float], force: Float)
110 do
111 var lists = [planes, enemies: Sequence[Body]]
112 var player = player
113 if player != null then lists.add([player])
114
115 for l in lists do
116 for body in l do
117 body.apply_force(center, force)
118 end
119 end
120 end
121
122 # Spawn or respawn the player
123 fun spawn_player
124 do
125 var old_player = player
126 var pos = null
127 if old_player != null then
128 # Respawn just above the death position
129 pos = old_player.center
130 pos.y += 50.0
131 end
132
133 if pos == null then
134 # If `dev` is passed as a command line option, spawn near space
135 var alt = 200.0
136 if args.has("dev") then alt = boss_altitude - 10.0
137
138 pos = new Point3d[Float](0.0, alt, 0.0)
139 end
140
141 player = new Player(pos, 4.0, 4.0, new Ak47)
142 end
143 end
144
145 # A physical object responding to hollywood physics
146 abstract class Body
147 super Boxed[Float]
148
149 # Position at the center of the body
150 var center: Point3d[Float]
151
152 # Inertia of this body
153 var inertia = new Point3d[Float](0.0, 0.0, 0.0) is writable
154
155 # Is this body still alive?
156 var is_alive = true
157
158 # Mass of this object, used by `apply_force`
159 fun mass: Float do return 1.0
160
161 # Width of this object, used to detect collisions
162 var width: Float
163
164 # Height of this object, used to detect collisions
165 var height: Float
166
167 # Current health level, starts at `max_health`
168 var health: Float = max_health
169
170 # Maximum health for this object
171 fun max_health: Float do return 100.0
172
173 # Is this object affected by gravity?
174 fun affected_by_gravity: Bool do return true
175
176 # Is this object dead? TODO merge with is_alive
177 fun dead: Bool do return health <= 0.0
178
179 # Apply game logic for the last `dt` seconds within `world`
180 fun update(dt: Float, world: World)
181 do
182 if affected_by_gravity then inertia.y -= 4.0
183
184 center.x += dt * inertia.x
185 center.y += dt * inertia.y
186 center.z += dt * inertia.z
187
188 # Hit the ground
189 if bottom <= 0.0 and affected_by_gravity then
190 center.y = height / 2.0
191 inertia.y = 0.0
192 end
193 end
194
195 # Apply a force, usually a result of `World::explode`
196 fun apply_force(origin: Point3d[Float], force: Float)
197 do
198 var dx = center.x - origin.x
199 var dy = center.y - origin.y
200
201 var d2 = dx*dx + dy*dy
202 var d = d2.sqrt
203 #TODO if d2 > ? then return
204
205 inertia.x += dx * force / d / mass * 4.0
206 inertia.y += dy * force / d / mass * 12.0
207
208 # Destabilize player
209 if self isa Player then self.plane = null
210 end
211
212 # Is this object out of `world.camera_view`?
213 fun out_of_screen(player: Player, world: World): Bool
214 do
215 var camera = world.camera_view
216 if right < camera.left - 20.0 then return true
217 if left > camera.right + 20.0 then return true
218 if top < camera.bottom - 20.0 then return true
219 if bottom > camera.top + 20.0 then return true
220 return false
221 end
222
223 # Apply `damage` to this object
224 fun hit(damage: Float, world: World)
225 do
226 self.health -= damage
227 if self.health <= 0.0 then die(world)
228 end
229
230 # Die in the game logic, with graphical animations
231 #
232 # Calls `destroy` by default.
233 fun die(world: World)
234 do
235 if not is_alive then return
236 is_alive = false
237 destroy world
238 end
239
240 # Destroy this objects and most references to it
241 protected fun destroy(world: World) do end
242
243 # ---
244 # Box services
245
246 redef fun top do return center.y + height / 2.0
247 redef fun bottom do return center.y - height / 2.0
248 redef fun left do return center.x - width / 2.0
249 redef fun right do return center.x + width / 2.0
250 end
251
252 # Something to stand on
253 abstract class Platform
254 super Body
255
256 redef fun mass do return 20.0
257
258 redef fun affected_by_gravity do return false
259
260 # Planes slow down when close to the player, `old_inertia` is the speed before it slowed down
261 private var old_inertia_y: nullable Float = null
262
263 # Enemy the spawned on the plane
264 var enemy: nullable WalkingEnemy = null is writable
265
266 redef fun die(world)
267 do
268 if not is_alive then return
269 super
270 world.explode(center, width)
271 world.score += 1
272 if 100.0.rand > 50.0 then world.powerups.add(new Powerup(self.center, world))
273 end
274
275 redef fun destroy(world)
276 do
277 world.planes.remove self
278 super
279 end
280
281 # Distance to the player
282 fun player_dist(world: World): Float do
283 var p = world.player
284 if p == null then return 0.0
285 var px = p.center.x
286 var dst = center.x - px
287 return dst.abs
288 end
289
290 # Has this plane slowed down because it is close to the player?
291 private var slowed_down = false
292
293 # Has this plane already accelerated because it got far from the player?
294 private var accelerated = false
295
296 redef fun update(dt, world)
297 do
298 # High friction on the Y axis to stabilize after an `apply_force`
299 inertia.y *= 0.95
300
301 super
302
303 # Slow down if close to the player
304 var dst = player_dist(world)
305 if dst < 20.0 then
306 if not slowed_down then
307 old_inertia_y = inertia.y
308
309 var speed = 10.0 + 15.0.rand
310 if inertia.x < 0.0 then
311 inertia.x = -speed
312 else inertia.x = speed
313 inertia.y = 0.0
314 inertia.z = 0.0
315
316 slowed_down = true
317 if enemy != null then
318 enemy.inertia.x = inertia.x
319 enemy.inertia.y = inertia.y
320 enemy.inertia.z = inertia.z
321 end
322 end
323 else if dst > 30.0 and not accelerated then
324 var oi = old_inertia_y
325 if oi == null then return
326
327 inertia.y = oi
328 accelerated = true
329 end
330 end
331 end
332
333 # Airplane, the basic `Platform`
334 class Airplane
335 super Platform
336 end
337
338 # Helicopter, the player rotates on its blades
339 class Helicopter
340 super Platform
341 end
342
343 # Parachute to slow down the player
344 class Parachute
345 super Body
346
347 redef var affected_by_gravity = false
348
349 redef fun update(dt, world) do
350 super
351 inertia.x = 0.0
352 inertia.y = 0.0
353 inertia.z = 0.0
354 center.x = world.player.center.x
355 center.y = world.player.center.y + 5.0
356 end
357 end
358
359 # Human body
360 abstract class Human
361 super Body
362
363 # Input direction in `[-1.0 .. 1.0]`
364 var moving = 0.0 is writable
365
366 # `moving` speed when on a plane, applied directly to `center`
367 var walking_speed = 20.0
368
369 # `moving` speed when in freefall, applied to `inertia`
370 var freefall_accel = 150.0
371
372 # Acceleration on the X axis applied when jumping
373 var jump_accel = 24.0
374
375 # On which plane is standing `self`? if any.
376 var plane: nullable Platform = null
377
378 # Position in relation to `plane`
379 private var dx_to_plane = 0.0
380
381 # Equipped weapon
382 var weapon: Weapon
383
384 # Rotation status when on a copter bladers, used by `update`
385 private var ltr = false
386
387 # Is the parachute currently deployed?
388 var parachute_deployed = false
389
390 redef var affected_by_gravity = true
391
392 # Altitude (in meters)
393 fun altitude: Float do return center.y
394
395 # Apply a jump from input
396 fun jump
397 do
398 var plane = plane
399 if plane != null then
400 # On solid plane, jump
401 inertia.y += 120.0
402 inertia.x = plane.inertia.x + moving * jump_accel
403
404 self.plane = null
405 end
406 end
407
408 # Deploy parachute on input
409 fun parachute
410 do
411 var plane = plane
412 if plane == null and not parachute_deployed then
413 # Deploy parachute
414 parachute_deployed = true
415 inertia.y = -10.0
416
417 self.plane = null
418 end
419 end
420
421 redef fun update(dt, world)
422 do
423 if not is_alive then return
424
425 if altitude >= world.boss_altitude then
426 # In space
427 affected_by_gravity = false
428 inertia.y *= 0.99
429 super
430 return
431 end
432
433 # In atmosphere
434
435 # On a plane?
436 var on_plane = plane
437 if on_plane != null then
438 # Is it still alive?
439 if not on_plane.is_alive then
440 on_plane = null
441 self.plane = null
442 end
443 end
444
445 if on_plane != null then
446 # On a plane, applying special physics do not call super!
447
448 # Precise movements
449 center.x = on_plane.center.x + dx_to_plane + moving * walking_speed * dt
450 center.y = on_plane.top + height / 2.0
451 if plane isa Helicopter then
452 center.y = plane.top + height / 2.0 + 1.5
453 var left_blade = plane.center.x - 5.0
454 var right_blade = plane.center.x + 5.0
455 var px = center.x
456 var blade_speed = 0.5
457 if ltr then
458 if px >= right_blade then ltr = false
459 center.x = on_plane.center.x + dx_to_plane + moving * walking_speed * dt + blade_speed
460 else
461 if px <= left_blade then ltr = true
462 center.x = on_plane.center.x + dx_to_plane + moving * walking_speed * dt - blade_speed
463 end
464 end
465
466 # Detect fall
467 if not (plane.left < right and plane.right > left) then
468 self.plane = null
469 end
470 else
471 # Freefall
472
473 # Only influence the inertia
474 inertia.x += moving * freefall_accel * dt
475 inertia.x *= 0.99
476
477 var old_y = bottom
478 super
479 if parachute_deployed then
480 if inertia.y < -10.0 then inertia.y = -10.0
481 end
482
483 # Detect collision with planes
484 for plane in world.planes do # TODO optimize with quad tree
485 if plane.left < right and plane.right > left then
486 if old_y > plane.top and bottom <= plane.top then
487 var parachute = world.parachute
488 if parachute != null then
489 parachute.die world
490 world.parachute = null
491 end
492 parachute_deployed = false
493 # Landed on a plane
494 plane.inertia.y += inertia.y / plane.mass
495
496 # Update self
497 self.plane = plane
498 inertia.x = 0.0
499 inertia.y = 0.0
500 center.y = plane.top + height / 2.0
501 if plane isa Helicopter then
502 center.y = plane.top + height / 2.0 + 4.0
503 end
504 break
505 end
506 end
507 end
508 end
509
510 on_plane = self.plane
511 if on_plane != null then
512 dx_to_plane = center.x - on_plane.center.x
513 end
514
515 # Die when hitting the ground
516 if bottom <= 0.0 then
517 die world
518 inertia.x = 0.0
519 inertia.y = 0.0
520 end
521
522 return
523 end
524
525 # Is the weapon ready to shoot?
526 fun can_shoot(world: World): Bool
527 do
528 return is_alive and world.t - weapon.last_shot >= weapon.cooldown
529 end
530
531 # Open fire at `angle`!
532 fun shoot(angle: Float, world: World)
533 do
534 if not can_shoot(world) then return
535
536 var x_inertia = angle.cos * weapon.power
537 var y_inertia = angle.sin * weapon.power
538 var new_center = new Point3d[Float](self.center.x, self.center.y, self.center.z - 0.2)
539
540 var bullet = register_bullet(new_center, angle, world)
541 bullet.inertia.x = x_inertia
542 bullet.inertia.y = y_inertia
543 weapon.last_shot = world.t
544 end
545
546 # Add a bullet, which type depends on `self`
547 protected fun register_bullet(new_center: Point3d[Float], angle: Float, world: World): Bullet
548 do
549 var bullet = new EnemyBullet(new_center, 2.0, 2.0, angle, self.weapon, world.t)
550 world.enemy_bullets.add(bullet)
551 return bullet
552 end
553 end
554
555 # Player character
556 class Player
557 super Human
558
559 # Basic starting weapon to which `self` reverts when out of bullets for powerup
560 var basic_weapon = new Ak47
561
562 redef fun shoot(angle, world)
563 do
564 super
565
566 # Consume limited bullets from powerups
567 if can_shoot(world) then
568 weapon.bullet_number -= 1
569 if weapon.bullet_number <= 0 then self.weapon = basic_weapon
570 end
571 end
572
573 redef fun register_bullet(new_center, angle, world)
574 do
575 var bullet = new PlayerBullet(new_center, 2.0, 2.0, angle, self.weapon, world.t)
576 world.player_bullets.add(bullet)
577 return bullet
578 end
579
580 redef fun update(dt, world)
581 do
582 super
583
584 # Catch powerups
585 for p in world.powerups.reverse_iterator do
586 if self.intersects(p) then
587 p.apply self
588 p.die world
589 end
590 end
591 end
592
593 redef fun max_health do return 200.0
594 end
595
596 # Enemy that can shoot
597 abstract class Enemy
598 super Human
599
600 redef fun max_health do return 20.0
601
602 redef fun die(world)
603 do
604 super
605 world.score += 1
606 if 100.0.rand > 90.0 then world.powerups.add(new Powerup(self.center, world))
607 end
608
609 redef fun destroy(world)
610 do
611 super
612 world.enemies.remove self
613 end
614 end
615
616 # Enemy walking on a platform
617 class WalkingEnemy
618 super Enemy
619 end
620
621 # Enemy with a jetpack
622 class JetpackEnemy
623 super Enemy
624
625 redef fun affected_by_gravity do return false
626 end
627
628 # The main boss, the ISS taken over by bad guys
629 class Boss
630 super Enemy
631
632 # TODO this should not subclass Human!
633
634 redef fun max_health do return 2000.0
635
636 redef fun affected_by_gravity do return false
637
638 redef fun die(world)
639 do
640 super
641 world.score += 999
642 end
643 end
644
645 # Bonus powerup
646 class Powerup
647 super Body
648
649 # Seconds to live
650 var lifespan = 10.0
651
652 # When has this powerup been created
653 var created = 0.0 is writable
654
655 redef fun affected_by_gravity do return false
656
657 new(center: Point3d[Float], world: World)
658 do
659 var v = 3.rand
660 var powerup: nullable Powerup = null
661 if v == 0 then powerup = new Ak47PU(center, 5.0, 5.0)
662 if v == 1 then powerup = new RocketLauncherPU(center, 5.0, 5.0)
663 if v == 2 then powerup = new Life(center, 5.0, 5.0)
664 assert powerup != null
665
666 powerup.inertia.y = -2.0
667 powerup.created = world.t
668 return powerup
669 end
670
671 # Apply this powerup to `player`
672 fun apply(player: Player) do end
673
674 redef fun update(dt, world)
675 do
676 super
677 if world.t - created > lifespan then die(world)
678 end
679
680 redef fun destroy(world)
681 do
682 super
683 world.powerups.remove(self)
684 end
685 end
686
687 # Weapon usable by a `Human` and `Boss`
688 abstract class Weapon
689
690 # Second at which the last shot was taken
691 var last_shot = 0.0
692
693 # Number of bullets in the chamber, the weapon is lost when it reaches 0
694 var bullet_number: Int is abstract
695
696 # Damage made by a single bullet
697 fun damage: Float is abstract
698
699 # Seconds between each shot
700 fun cooldown: Float is abstract
701
702 # Speed of the bullet when leaving the weapon
703 fun power: Float is abstract
704
705 # Seconds to live of the bullets
706 fun bullet_lifespan: Float is abstract
707 end
708
709 # Bullet fired by a `weapon`
710 abstract class Bullet
711 super Body
712
713 # Orientation
714 var angle: Float
715
716 # `Weapon` that fired `self`
717 var weapon: Weapon
718
719 # Second at which this bullet was fired
720 var creation_time: Float
721
722 redef fun affected_by_gravity do return false
723
724 redef fun update(dt, world)
725 do
726 super
727 if world.t - creation_time >= weapon.bullet_lifespan then die world
728 end
729
730 # Hit `body`
731 fun hit_enemy(body: Body, world: World)
732 do
733 body.hit(self.weapon.damage, world)
734 die world
735 end
736 end
737
738 # `Bullet` shot by the player
739 class PlayerBullet
740 super Bullet
741
742 redef fun update(dt, world)
743 do
744 super
745
746 for i in world.planes do if self.intersects(i) then hit_enemy(i, world)
747 for i in world.enemies do if self.intersects(i) then hit_enemy(i, world)
748 end
749
750 redef fun destroy(world) do
751 super
752 world.player_bullets.remove self
753 end
754 end
755
756 # `Bullet` shot by an enemy
757 class EnemyBullet
758 super Bullet
759
760 redef fun update(dt, world) do
761 super
762
763 var player = world.player
764 if player != null and self.intersects(player) then hit_enemy(player, world)
765 end
766
767 redef fun destroy(world) do
768 super
769 world.enemy_bullets.remove self
770 end
771 end
772
773 # Fast shooting weapon
774 class Ak47
775 super Weapon
776
777 redef var damage = 10.0
778
779 redef var cooldown = 0.1
780
781 redef var power = 70.0
782
783 redef var bullet_lifespan = 3.0
784
785 redef var bullet_number = 200
786 end
787
788 # Powerup to equip an `Ak47`
789 class Ak47PU
790 super Powerup
791
792 redef fun apply(player) do player.weapon = new Ak47
793 end
794
795 # Slow but powerful rocket launcher
796 class RocketLauncher
797 super Weapon
798
799 redef var damage = 500.0
800
801 redef var cooldown = 1.5
802
803 redef var power = 50.0
804
805 redef var bullet_lifespan = 5.0
806
807 redef var bullet_number = 20
808 end
809
810 # Powerup to equip a `RocketLauncher`
811 class RocketLauncherPU
812 super Powerup
813
814 redef fun apply(player) do player.weapon = new RocketLauncher
815 end
816
817 # Base weapon, a bit slow
818 class Pistol
819 super Weapon
820
821 redef var damage = 10.0
822
823 redef var cooldown = 0.3
824
825 redef var power = 70.0
826
827 redef var bullet_lifespan = 3.0
828
829 redef var bullet_number = 10000
830 end
831
832 # Healing powerup
833 class Life
834 super Powerup
835
836 redef fun apply(player) do player.health += 50.0
837 end
838
839 redef class Float
840 # Fuzzy value in `[self-variation..self+variation]`
841 fun &(variation: Float): Float do return self - variation + 2.0*variation.rand
842 end