contrib: intro action_nitro, a game for the clibre gamejam 2016
[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 and scoring when applicable
231 #
232 # Calls `destroy` by default.
233 fun die(world: World)
234 do
235 is_alive = false
236 destroy world
237 end
238
239 # Destroy this objects and most references to it
240 fun destroy(world: World) do end
241
242 # ---
243 # Box services
244
245 redef fun top do return center.y + height / 2.0
246 redef fun bottom do return center.y - height / 2.0
247 redef fun left do return center.x - width / 2.0
248 redef fun right do return center.x + width / 2.0
249 end
250
251 # Something to stand on
252 abstract class Platform
253 super Body
254
255 redef fun mass do return 20.0
256
257 redef fun affected_by_gravity do return false
258
259 # Planes slow down when close to the player, `old_inertia` is the speed before it slowed down
260 private var old_inertia_y: nullable Float = null
261
262 # Enemy the spawned on the plane
263 var enemy: nullable WalkingEnemy = null is writable
264
265 redef fun die(world)
266 do
267 super
268 world.explode(center, width)
269 world.score += 1
270 if 100.0.rand > 50.0 then world.powerups.add(new Powerup(self.center, world))
271 end
272
273 redef fun destroy(world)
274 do
275 world.planes.remove self
276 super
277 end
278
279 # Distance to the player
280 fun player_dist(world: World): Float do
281 var p = world.player
282 if p == null then return 0.0
283 var px = p.center.x
284 var dst = center.x - px
285 return dst.abs
286 end
287
288 # Has this plane slowed down because it is close to the player?
289 private var slowed_down = false
290
291 # Has this plane already accelerated because it got far from the player?
292 private var accelerated = false
293
294 redef fun update(dt, world)
295 do
296 # High friction on the Y axis to stabilize after an `apply_force`
297 inertia.y *= 0.95
298
299 super
300
301 # Slow down if close to the player
302 var dst = player_dist(world)
303 if dst < 20.0 then
304 if not slowed_down then
305 old_inertia_y = inertia.y
306
307 var speed = 10.0 + 15.0.rand
308 if inertia.x < 0.0 then
309 inertia.x = -speed
310 else inertia.x = speed
311 inertia.y = 0.0
312 inertia.z = 0.0
313
314 slowed_down = true
315 if enemy != null then
316 enemy.inertia.x = inertia.x
317 enemy.inertia.y = inertia.y
318 enemy.inertia.z = inertia.z
319 end
320 end
321 else if dst > 30.0 and not accelerated then
322 var oi = old_inertia_y
323 if oi == null then return
324
325 inertia.y = oi
326 accelerated = true
327 end
328 end
329 end
330
331 # Airplane, the basic `Platform`
332 class Airplane
333 super Platform
334 end
335
336 # Helicopter, the player rotates on its blades
337 class Helicopter
338 super Platform
339 end
340
341 # Parachute to slow down the player
342 class Parachute
343 super Body
344
345 redef var affected_by_gravity = false
346
347 redef fun update(dt, world) do
348 super
349 inertia.x = 0.0
350 inertia.y = 0.0
351 inertia.z = 0.0
352 center.x = world.player.center.x
353 center.y = world.player.center.y + 5.0
354 end
355 end
356
357 # Human body
358 abstract class Human
359 super Body
360
361 # Input direction in `[-1.0 .. 1.0]`
362 var moving = 0.0 is writable
363
364 # `moving` speed when on a plane, applied directly to `center`
365 var walking_speed = 20.0
366
367 # `moving` speed when in freefall, applied to `inertia`
368 var freefall_accel = 150.0
369
370 # Acceleration on the X axis applied when jumping
371 var jump_accel = 24.0
372
373 # On which plane is standing `self`? if any.
374 var plane: nullable Platform = null
375
376 # Position in relation to `plane`
377 private var dx_to_plane = 0.0
378
379 # Equipped weapon
380 var weapon: Weapon
381
382 # Rotation status when on a copter bladers, used by `update`
383 private var ltr = false
384
385 # Is the parachute currently deployed?
386 var parachute_deployed = false
387
388 redef var affected_by_gravity = true
389
390 # Altitude (in meters)
391 fun altitude: Float do return center.y
392
393 # Apply a jump from input
394 fun jump
395 do
396 var plane = plane
397 if plane != null then
398 # On solid plane, jump
399 inertia.y += 120.0
400 inertia.x = plane.inertia.x + moving * jump_accel
401
402 self.plane = null
403 end
404 end
405
406 # Deploy parachute on input
407 fun parachute
408 do
409 var plane = plane
410 if plane == null and not parachute_deployed then
411 # Deploy parachute
412 parachute_deployed = true
413 inertia.y = -10.0
414
415 self.plane = null
416 end
417 end
418
419 redef fun update(dt, world)
420 do
421 if not is_alive then return
422
423 if altitude >= world.boss_altitude then
424 # In space
425 affected_by_gravity = false
426 inertia.y *= 0.99
427 super
428 return
429 end
430
431 # In atmosphere
432
433 # On a plane?
434 var on_plane = plane
435 if on_plane != null then
436 # Is it still alive?
437 if not on_plane.is_alive then
438 on_plane = null
439 self.plane = null
440 end
441 end
442
443 if on_plane != null then
444 # On a plane, applying special physics do not call super!
445
446 # Precise movements
447 center.x = on_plane.center.x + dx_to_plane + moving * walking_speed * dt
448 center.y = on_plane.top + height / 2.0
449 if plane isa Helicopter then
450 center.y = plane.top + height / 2.0 + 1.5
451 var left_blade = plane.center.x - 5.0
452 var right_blade = plane.center.x + 5.0
453 var px = center.x
454 var blade_speed = 0.5
455 if ltr then
456 if px >= right_blade then ltr = false
457 center.x = on_plane.center.x + dx_to_plane + moving * walking_speed * dt + blade_speed
458 else
459 if px <= left_blade then ltr = true
460 center.x = on_plane.center.x + dx_to_plane + moving * walking_speed * dt - blade_speed
461 end
462 end
463
464 # Detect fall
465 if not (plane.left < right and plane.right > left) then
466 self.plane = null
467 end
468 else
469 # Freefall
470
471 # Only influence the inertia
472 inertia.x += moving * freefall_accel * dt
473 inertia.x *= 0.99
474
475 var old_y = bottom
476 super
477 if parachute_deployed then
478 if inertia.y < -10.0 then inertia.y = -10.0
479 end
480
481 # Detect collision with planes
482 for plane in world.planes do # TODO optimize with quad tree
483 if plane.left < right and plane.right > left then
484 if old_y > plane.top and bottom <= plane.top then
485 if world.parachute != null then
486 world.parachute.destroy(world)
487 world.parachute = null
488 end
489 parachute_deployed = false
490 # Landed on a plane
491 plane.inertia.y += inertia.y / plane.mass
492
493 # Update self
494 self.plane = plane
495 inertia.x = 0.0
496 inertia.y = 0.0
497 center.y = plane.top + height / 2.0
498 if plane isa Helicopter then
499 center.y = plane.top + height / 2.0 + 4.0
500 end
501 break
502 end
503 end
504 end
505 end
506
507 on_plane = self.plane
508 if on_plane != null then
509 dx_to_plane = center.x - on_plane.center.x
510 end
511
512 # Die when hitting the ground
513 if bottom <= 0.0 then
514 die world
515 inertia.x = 0.0
516 inertia.y = 0.0
517 end
518
519 return
520 end
521
522 # Is the weapon ready to shoot?
523 fun can_shoot(world: World): Bool
524 do
525 return is_alive and world.t - weapon.last_shot >= weapon.cooldown
526 end
527
528 # Open fire at `angle`!
529 fun shoot(angle: Float, world: World)
530 do
531 if not can_shoot(world) then return
532
533 var x_inertia = angle.cos * weapon.power
534 var y_inertia = angle.sin * weapon.power
535 var new_center = new Point3d[Float](self.center.x, self.center.y, self.center.z - 0.2)
536
537 var bullet = register_bullet(new_center, angle, world)
538 bullet.inertia.x = x_inertia
539 bullet.inertia.y = y_inertia
540 weapon.last_shot = world.t
541 end
542
543 # Add a bullet, which type depends on `self`
544 protected fun register_bullet(new_center: Point3d[Float], angle: Float, world: World): Bullet
545 do
546 var bullet = new EnemyBullet(new_center, 2.0, 2.0, angle, self.weapon, world.t)
547 world.enemy_bullets.add(bullet)
548 return bullet
549 end
550 end
551
552 # Player character
553 class Player
554 super Human
555
556 # Basic starting weapon to which `self` reverts when out of bullets for powerup
557 var basic_weapon = new Ak47
558
559 redef fun shoot(angle, world)
560 do
561 super
562
563 # Consume limited bullets from powerups
564 if can_shoot(world) then
565 weapon.bullet_number -= 1
566 if weapon.bullet_number <= 0 then self.weapon = basic_weapon
567 end
568 end
569
570 redef fun register_bullet(new_center, angle, world)
571 do
572 var bullet = new PlayerBullet(new_center, 2.0, 2.0, angle, self.weapon, world.t)
573 world.player_bullets.add(bullet)
574 return bullet
575 end
576
577 redef fun update(dt, world)
578 do
579 super
580
581 # Catch powerups
582 for p in world.powerups.reverse_iterator do
583 if self.intersects(p) then
584 p.apply self
585 p.die world
586 end
587 end
588 end
589
590 redef fun max_health do return 200.0
591 end
592
593 # Enemy that can shoot
594 abstract class Enemy
595 super Human
596
597 redef fun max_health do return 20.0
598
599 redef fun die(world)
600 do
601 super
602 world.score += 1
603 if 100.0.rand > 90.0 then world.powerups.add(new Powerup(self.center, world))
604 end
605
606 redef fun destroy(world)
607 do
608 super
609 world.enemies.remove self
610 end
611 end
612
613 # Enemy walking on a platform
614 class WalkingEnemy
615 super Enemy
616 end
617
618 # Enemy with a jetpack
619 class JetpackEnemy
620 super Enemy
621
622 redef fun affected_by_gravity do return false
623 end
624
625 # The main boss, the ISS taken over by bad guys
626 class Boss
627 super Enemy
628
629 # TODO this should not subclass Human!
630
631 redef fun max_health do return 2000.0
632
633 redef fun affected_by_gravity do return false
634
635 redef fun die(world)
636 do
637 super
638 world.score += 999
639 end
640 end
641
642 # Bonus powerup
643 class Powerup
644 super Body
645
646 # Seconds to live
647 var lifespan = 10.0
648
649 # When has this powerup been created
650 var created = 0.0 is writable
651
652 redef fun affected_by_gravity do return false
653
654 new(center: Point3d[Float], world: World)
655 do
656 var v = 3.rand
657 var powerup: nullable Powerup = null
658 if v == 0 then powerup = new Ak47PU(center, 5.0, 5.0)
659 if v == 1 then powerup = new RocketLauncherPU(center, 5.0, 5.0)
660 if v == 2 then powerup = new Life(center, 5.0, 5.0)
661 assert powerup != null
662
663 powerup.inertia.y = -2.0
664 powerup.created = world.t
665 return powerup
666 end
667
668 # Apply this powerup to `player`
669 fun apply(player: Player) do end
670
671 redef fun update(dt, world)
672 do
673 super
674 if world.t - created > lifespan then die(world)
675 end
676
677 redef fun destroy(world)
678 do
679 super
680 world.powerups.remove(self)
681 end
682 end
683
684 # Weapon usable by a `Human` and `Boss`
685 abstract class Weapon
686
687 # Second at which the last shot was taken
688 var last_shot = 0.0
689
690 # Number of bullets in the chamber, the weapon is lost when it reaches 0
691 var bullet_number: Int is abstract
692
693 # Damage made by a single bullet
694 fun damage: Float is abstract
695
696 # Seconds between each shot
697 fun cooldown: Float is abstract
698
699 # Speed of the bullet when leaving the weapon
700 fun power: Float is abstract
701
702 # Seconds to live of the bullets
703 fun bullet_lifespan: Float is abstract
704 end
705
706 # Bullet fired by a `weapon`
707 abstract class Bullet
708 super Body
709
710 # Orientation
711 var angle: Float
712
713 # `Weapon` that fired `self`
714 var weapon: Weapon
715
716 # Second at which this bullet was fired
717 var creation_time: Float
718
719 redef fun affected_by_gravity do return false
720
721 redef fun update(dt, world)
722 do
723 super
724 if world.t - creation_time >= weapon.bullet_lifespan then destroy world
725 end
726
727 # Hit `body`
728 fun hit_enemy(body: Body, world: World)
729 do
730 body.hit(self.weapon.damage, world)
731 destroy world
732 end
733 end
734
735 # `Bullet` shot by the player
736 class PlayerBullet
737 super Bullet
738
739 redef fun update(dt, world)
740 do
741 super
742
743 for i in world.planes do if self.intersects(i) then hit_enemy(i, world)
744 for i in world.enemies do if self.intersects(i) then hit_enemy(i, world)
745 end
746
747 redef fun destroy(world) do
748 super
749 world.player_bullets.remove self
750 end
751 end
752
753 # `Bullet` shot by an enemy
754 class EnemyBullet
755 super Bullet
756
757 redef fun update(dt, world) do
758 super
759
760 var player = world.player
761 if player != null and self.intersects(player) then hit_enemy(player, world)
762 end
763
764 redef fun destroy(world) do
765 super
766 world.enemy_bullets.remove self
767 end
768 end
769
770 # Fast shooting weapon
771 class Ak47
772 super Weapon
773
774 redef var damage = 10.0
775
776 redef var cooldown = 0.1
777
778 redef var power = 70.0
779
780 redef var bullet_lifespan = 3.0
781
782 redef var bullet_number = 200
783 end
784
785 # Powerup to equip an `Ak47`
786 class Ak47PU
787 super Powerup
788
789 redef fun apply(player) do player.weapon = new Ak47
790 end
791
792 # Slow but powerful rocket launcher
793 class RocketLauncher
794 super Weapon
795
796 redef var damage = 500.0
797
798 redef var cooldown = 1.5
799
800 redef var power = 50.0
801
802 redef var bullet_lifespan = 5.0
803
804 redef var bullet_number = 20
805 end
806
807 # Powerup to equip a `RocketLauncher`
808 class RocketLauncherPU
809 super Powerup
810
811 redef fun apply(player) do player.weapon = new RocketLauncher
812 end
813
814 # Base weapon, a bit slow
815 class Pistol
816 super Weapon
817
818 redef var damage = 10.0
819
820 redef var cooldown = 0.3
821
822 redef var power = 70.0
823
824 redef var bullet_lifespan = 3.0
825
826 redef var bullet_number = 10000
827 end
828
829 # Healing powerup
830 class Life
831 super Powerup
832
833 redef fun apply(player) do player.health += 50.0
834 end
835
836 redef class Float
837 # Fuzzy value in `[self-variation..self+variation]`
838 fun &(variation: Float): Float do return self - variation + 2.0*variation.rand
839 end