shoot: move game to a src sub-directory
[nit.git] / examples / shoot / src / shoot_logic.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 # Space shooter.
16 # This program is a fun game but also a good example of the scene2d module
17 module shoot_logic
18
19 import scene2d
20
21 # The ship of the player
22 class Player
23 super Sprite
24
25 # Current forture of the player
26 var money: Int writable = 0
27
28 # Number of basic bullets fired together
29 var nbshoots: Int writable = 1
30
31 # Time bebore the player shoot again a basic bullet (cooldown)
32 # Shoot if 0
33 var shoot_ttl = 0
34
35 # Number of missiles
36 var nbmissiles: Int writable = 0
37
38 # Time bebore the player shoot again a missile (cooldown)
39 # Shoot if 0
40 var missile_ttl = 0
41
42 # Remainind time when the player is protected from impacts
43 var protected_ttl = 0
44
45 # The associated play scene
46 # (mainly used to registed shoots)
47 var scene: PlayScene
48
49 init(scene: PlayScene) do
50 self.scene = scene
51 self.width = 2400
52 self.height = 2400
53 end
54
55 redef fun update
56 do
57 super
58
59 # Out of screen?
60 if self.y < 0 then
61 self.y = 0
62 self.vy = 0
63 else if self.y > 60000 then
64 self.y = 60000
65 self.vy = 0
66 end
67 if self.x < 0 then
68 self.x = 0
69 self.vx = 0
70 else if self.x > 80000 then
71 self.x = 80000
72 self.vx = 0
73 end
74
75 # Update of the player protection if any
76 if protected_ttl > 0 then protected_ttl -= 1
77
78 # Need to shoot basic bullets?
79 if shoot_ttl > 0 then
80 shoot_ttl -= 1
81 else
82 shoot_ttl = 30
83 for i in [0..nbshoots[ do
84 var shoot = new Shoot
85 shoot.x = x
86 shoot.y = top
87 shoot.vy = -500
88 shoot.vx = (i - nbshoots / 2) * 100
89 scene.player_shoots.add(shoot)
90 end
91 end
92
93 # Need to shoot missiles?
94 if missile_ttl > 0 then
95 missile_ttl -= 1
96 else if nbmissiles > 0 then
97 missile_ttl = 500 / nbmissiles
98 var shoot = new Missile
99 shoot.x = x
100 shoot.y = top
101 shoot.vy = -300
102 shoot.vx = 0
103 scene.player_shoots.add(shoot)
104 end
105
106 end
107
108 # Time before the player is respawned by the scene
109 var respawn_ttl: Int = 0
110
111 fun hit
112 do
113 if self.protected_ttl > 0 then return
114 self.scene.explosion(self.x, self.y, 10)
115 self.exists = false
116
117 # Reset the position for respawn
118 self.x = 400 * 100
119 self.y = 500 * 100
120 self.vx = 0
121 self.vy = 0
122 self.respawn_ttl = 50
123 end
124
125 end
126
127 # Sprites that may be hit by the player.
128 # Eq. enemies, bullets, loots, etc.
129 class Hitable
130 super Sprite
131
132 # What do do when self is hit by the player.
133 # By defaut, do nothing
134 fun hit_by_player(player: Player) do end
135 end
136
137 # A bullet shooted by a ship
138 class Shoot
139 super Hitable
140
141 # Was the shoot fired by an enemy.
142 # Since there is no frendly fire, it is important to distinguish ownership
143 var enemy: Bool = false
144
145 init do
146 self.width = 800
147 self.height = 800
148 end
149
150 redef fun update
151 do
152 super
153
154 # Out of screen ?
155 if self.y < -100 * 100 or self.y > 700 * 100 or self.x < -100 * 100 or self.x > 900 * 100 then
156 self.exists = false
157 end
158 end
159
160 redef fun hit_by_player(player)
161 do
162 player.hit
163 self.exists = false
164 end
165 end
166
167 # A advanced bullet that aims a target (player or enemy)
168 class Missile
169 super Shoot
170
171 # The target aquired by the missile
172 var target: nullable Sprite
173
174 # When ttl is 0 then the angle stay fixed
175 # The angle is updated toward the target if ttl>0
176 var ttl: Int = 200
177
178 redef fun update
179 do
180 super
181
182 # Do we still update the angle ?
183 if ttl <= 0 then return
184 ttl -= 1
185
186 # Do we have a target?
187 var target = self.target
188 if target == null or not target.exists then return
189
190 # Just update the angle
191 var angle = self.angle_to(target)
192 self.set_velocity(angle, 300)
193 end
194 end
195
196 # A enemy ship
197 # Various enemies exists, each kind has its own subclass
198 abstract class Enemy
199 super Hitable
200
201 # The scene of the ship
202 # Is used to store created bullets or to get info about the player
203 var scene: PlayScene
204
205 # Time bebore the enemy shoot again (cooldown)
206 # Shoot if 0
207 # The default value is used as a grace period to avoid a first shoot on
208 # the first update
209 var shoot_ttl = 50
210
211 init(scene: PlayScene)
212 do
213 self.width = 2400
214 self.height = 2400
215 self.scene = scene
216 scene.enemies.add(self)
217 end
218
219 redef fun update
220 do
221 super
222
223 # Out of screen ?
224 if self.y > 700 * 100 or self.x < -100 * 100 or self.x > 900 * 100 then
225 # Note: no control on the top to let ennemies appear
226 self.exists = false
227 end
228
229 # Need to shoot?
230 if shoot_ttl > 0 then
231 shoot_ttl -= 1
232 else
233 shoot
234 end
235 end
236
237 # Each enemy has its own kind of shoot strategy
238 # Note: is automatically called by update when shoot_ttl is expired
239 fun shoot do end
240
241 # Money given when the enemy is destroyed
242 fun loot: Int is abstract
243
244 # What to do when the enemy is hit by a player shoot (or by the player himself)?
245 # By default it kill the enemy in an explosion and generate a loot
246 fun hit
247 do
248 self.exists = false
249 scene.explosion(self.x, self.y, 5)
250 if 100.rand < 3 then
251 var upmissile = new UpMissile
252 upmissile.x = self.x
253 upmissile.y = self.y
254 upmissile.vx = 0
255 upmissile.vy = 0
256 scene.loots.add(upmissile)
257 scene.hitables.add(new LootArea(upmissile, 2000))
258 else
259 for i in [0..self.loot[ do
260 var money = new Money
261 money.x = self.x
262 money.y = self.y
263 money.set_velocity(100.rand.to_f*pi/50.0, (500+self.loot).rand)
264 scene.loots.add(money)
265 scene.hitables.add(new LootArea(money, 2000))
266 end
267 end
268 end
269
270 redef fun hit_by_player(player)
271 do
272 player.hit
273 hit
274 end
275 end
276
277 # Basic enemy, do not shoot
278 class Enemy0
279 super Enemy
280
281 redef fun loot do return 3
282 end
283
284 # Simple shooter of paris of basic bullets
285 class Enemy1
286 super Enemy
287
288 redef fun shoot
289 do
290 # Next shoot
291 shoot_ttl = 50
292
293 # two bullets shoot each time
294 for dx in [-11, 11] do
295 var shoot = new Shoot
296 shoot.enemy = true
297 shoot.x = self.x + dx * 100
298 shoot.y = self.bottom
299 shoot.vy = 500
300 scene.enemy_shoots.add(shoot)
301 end
302 end
303
304 redef fun loot do return 5
305 end
306
307 # Enemy that shoot missiles
308 class Enemy2
309 super Enemy
310
311 redef fun shoot
312 do
313 # Next shoot
314 shoot_ttl = 200
315
316 # The missile targets the player
317 var shoot = new Missile
318 shoot.enemy = true
319 shoot.x = self.x
320 shoot.y = self.bottom
321 shoot.vy = 500
322 shoot.target = scene.player
323 scene.enemy_shoots.add(shoot)
324 end
325
326 redef fun loot do return 10
327 end
328
329 # Enem that shoot rings of basic bullets
330 class Enemy3
331 super Enemy
332
333 redef fun shoot
334 do
335 # Next shoot
336 shoot_ttl = 50
337
338 for i in [0..10[ do
339 var shoot = new Shoot
340 shoot.enemy = true
341 shoot.x = self.x
342 shoot.y = self.bottom
343 shoot.set_velocity(pi/5.0*i.to_f, 500)
344 scene.enemy_shoots.add(shoot)
345 end
346 end
347
348 redef fun loot do return 20
349 end
350
351 # Enemy with a turret that shoot burst of bullets toward the player
352 class Enemy4
353 super Enemy
354
355 # The angle of the turret
356 var angle: Float = 0.0
357
358 redef fun update
359 do
360 super
361
362 # Rotate the turret toward the player
363 var target = scene.player
364 if target.exists then
365 angle = self.angle_to(target)
366 end
367 end
368
369 # Shoots come in burst
370 var nbshoots: Int = 0
371
372 redef fun shoot
373 do
374 # Next shoot: is there still bullets in the burst?
375 if self.nbshoots < 10 then
376 # Is ther
377 self.nbshoots += 1
378 shoot_ttl = 5
379 else
380 self.nbshoots = 0
381 shoot_ttl = 80
382 end
383
384 # Shoot with the turret angle
385 var shoot = new Shoot
386 shoot.enemy = true
387 shoot.x = self.x
388 shoot.y = self.y
389 shoot.set_velocity(angle, 500)
390 scene.enemy_shoots.add(shoot)
391 end
392
393 redef fun loot do return 20
394 end
395
396 # Enemy that rush directly on the player
397 class EnemyKamikaze
398 super Enemy
399
400 redef fun update
401 do
402 super
403
404 # Try to target the player
405 var target = scene.player
406 if not target.exists then return
407
408 var angle = self.angle_to(target)
409 self.set_velocity(angle, 600)
410 end
411
412 redef fun loot do return 5
413 end
414
415 # The boss has two semi-independent arms
416 class Boss
417 super Enemy
418
419 # Left arm
420 var left_part: BossPart
421
422 # Right arm
423 var right_part: BossPart
424
425 init(scene)
426 do
427 super
428 self.width = 128 * 100
429 self.height = 100 * 100
430 self.x = 400 * 100
431 self.y = -100 * 100
432 self.left_part = new BossPart(self, -48*100)
433 self.right_part = new BossPart(self, 48*100)
434 end
435
436 var flick_ttl: Int = 0
437
438 redef fun update
439 do
440 if flick_ttl > 0 then flick_ttl -= 1
441
442 # Path of the boss (down then left<->right)
443 if self.y < 20000 then
444 self.vx = 0
445 self.vy = 100
446 else if self.vx == 0 then
447 self.vx = 100
448 self.vy = 0
449 else if self.x > 700 * 100 and self.vx > 0 then
450 self.vx = -self.vx
451 else if self.x < 100 * 100 and self.vx < 0 then
452 self.vx = -self.vx
453 end
454
455 super
456 end
457
458 redef fun shoot
459 do
460 # Do not shoot if not ready
461 if self.vy != 0 then return
462
463 # Try to target the player
464 var target = scene.player
465 if not target.exists then return
466
467 # Next shoot: burst if no arms remains
468 if left_part.exists or right_part.exists then
469 shoot_ttl = 60
470 else
471 shoot_ttl = 20
472 end
473
474 # Shoot the player with a basic bullet
475 var shoot = new Shoot
476 shoot.enemy = true
477 shoot.x = self.x
478 shoot.y = self.bottom
479 var angle = shoot.angle_to(target)
480 shoot.set_velocity(angle, 500)
481 scene.enemy_shoots.add(shoot)
482 end
483
484 redef fun loot do return 100
485
486 var live: Int = 20
487
488 redef fun hit
489 do
490 # Protected while an arm remains
491 if left_part.exists or right_part.exists then return
492
493 if live > 0 then
494 live -= 1
495 flick_ttl = 2
496 else
497 super
498 scene.explosion(self.x, self.y, 30)
499 end
500 end
501 end
502
503 # An arm of a boss
504 class BossPart
505 super Enemy
506
507 # The associated boss
508 var boss: Boss
509
510 # Relative x coordonate (center to center) of the arm
511 var relx: Int
512
513 # Relative y coordonate (center to center) of the arm
514 var rely: Int = 36 * 100
515
516 var live: Int = 10
517
518 init(boss: Boss, relx: Int)
519 do
520 self.boss = boss
521 self.relx = relx
522 super(boss.scene)
523 self.width = 32 * 100
524 self.height = 60 * 100
525
526 # Alternate the shoots of the arms
527 if relx > 0 then
528 shoot_ttl += 300
529 end
530 self.x = boss.x + relx
531 self.y = boss.y + rely
532 end
533
534 redef fun update
535 do
536 self.x = boss.x + relx
537 self.y = boss.y + rely
538
539 super
540
541 if flick_ttl > 0 then flick_ttl -= 1
542 end
543
544 redef fun shoot
545 do
546 # Do not shoot if not ready
547 if self.boss.vy != 0 then return
548
549 # Next shoot
550 shoot_ttl = 600
551
552 # Shoot a missile that targets the player
553 var shoot = new Missile
554 shoot.enemy = true
555 shoot.x = self.x
556 shoot.y = self.bottom
557 shoot.vy = 500
558 shoot.target = scene.player
559 scene.enemy_shoots.add(shoot)
560 end
561
562 var flick_ttl: Int = 0
563
564 redef fun hit
565 do
566 if live > 0 then
567 live -= 1
568 flick_ttl = 2
569 else
570 super
571 end
572 end
573
574 redef fun loot do return 10
575 end
576
577 # Whatever reward or bonus that can be picked by the player
578 abstract class Loot
579 super Hitable
580
581 init
582 do
583 self.width = 400
584 self.height = 400
585 end
586
587 # Magnet effect: The loot will move to the target if set
588 # See LootArea for details
589 var target: nullable Sprite = null
590
591 redef fun update
592 do
593 super
594
595 # Out of screen ?
596 if self.y > 700 * 100 then
597 self.exists = false
598 end
599
600 var target = self.target
601 if target == null then
602 # Not magneted: deploy
603
604 # Heavy fuild friction to stops the explosion
605 # Loots are placed with a explosion, see `Enemy::hit'
606 self.vx = self.vx*7/8
607 self.vy = self.vy*7/8
608
609 # Background scroling
610 self.y += 50
611
612 else if target.exists then
613 # Magneted: rush toward the target
614 var angle = self.angle_to(target)
615 self.set_velocity(angle, 800)
616
617 else
618 # Magneted but dead target: reset the loot
619 self.vx = 0
620 self.vy = 0
621 self.target = null
622 end
623 end
624 end
625
626 # Basic money loot
627 class Money
628 super Loot
629
630 redef fun hit_by_player(player)
631 do
632 self.exists = false
633 player.money += 1
634 if player.money > 100 then
635 player.money -= 100
636 player.nbshoots += 1
637 end
638 end
639 end
640
641 # Increase the number of missiles
642 class UpMissile
643 super Loot
644
645 redef fun hit_by_player(player)
646 do
647 self.exists = false
648 player.nbmissiles += 1
649 end
650 end
651
652 # A loot area is an invisible field used to implement the magnet effets of loots
653 # The principle is:
654 # * the loot is an invisible sprite with a hitbox larger than the loot hitbox
655 # * the lootbox remains centered on the loot
656 # * when the player hit the lootarea, then the loot is set to target the player
657 # * when the player hit the loot, then the player gains effectively the loot
658 class LootArea
659 super Hitable
660
661 # The associated loot
662 var loot: Loot
663
664 init(loot: Loot, radius: Int)
665 do
666 self.loot = loot
667 self.width = radius * 2 + loot.width
668 self.height = radius * 2 + loot.height
669 end
670
671 redef fun update
672 do
673 # position remains centered on the loot
674 self.x = loot.x
675 self.y = loot.y
676
677 # No area if no loot
678 if not loot.exists then self.exists = false
679
680 # the super is useless but it is a good practice to call it
681 super
682 end
683
684 redef fun hit_by_player(player)
685 do
686 # Kill the area
687 self.exists = false
688
689 # The loot now targets the player
690 loot.target = player
691 end
692 end
693
694 # A non interactive element of an explosion
695 # A real explosion is made of many Explosion object
696 # Use the `PlayScene::explosion` method to generate a full explosion
697 class Explosion
698 super Sprite
699
700 # Time before the sprite vanishes
701 var ttl: Int = 10
702
703 redef fun update
704 do
705 # Heavy fuild friction to stops the explosion
706 self.vx = self.vx*7/8
707 self.vy = self.vy*7/8
708
709 # Background scrolling
710 self.y += 50
711
712 super
713
714 # Vanishes?
715 if ttl > 0 then
716 ttl -= 1
717 else
718 exists = false
719 end
720 end
721 end
722
723 # A star is a non-interactive background element
724 # Stars are used to simulate a continuous global scroling
725 class Star
726 super Sprite
727
728 init
729 do
730 # Randomely places stars on the plane
731 self.x = 800.rand * 100
732 self.y = 600.rand * 100
733 self.vy = 40.rand + 11
734 end
735
736 redef fun update
737 do
738 super
739
740 # Replace the star on the top
741 if self.y > 600 * 100 then
742 self.y = 200.rand * -100
743 self.x = 800.rand * 100
744 self.vy = 40.rand + 11
745 end
746 end
747 end
748
749 redef class Scene
750 # When a scene need to be replaced, just assign the next_scene to a non null value
751 var next_scene: nullable Scene writable = null
752 end
753
754 # The main play state
755 class PlayScene
756 super Scene
757
758 # The player ship
759 var player: Player
760
761 # Shoots of the player
762 var player_shoots = new LiveGroup[Shoot]
763
764 # Enemy ships
765 var enemies = new LiveGroup[Enemy]
766
767 # Soots of the enemy
768 var enemy_shoots = new LiveGroup[Shoot]
769
770 # Collectible loots
771 var loots = new LiveGroup[Loot]
772
773 # Non active stuff like explosions
774 var pasive_stuff = new LiveGroup[LiveObject]
775
776 # Background stuff like stars
777 var background = new LiveGroup[LiveObject]
778
779 # All other hitable sprites
780 var hitables = new LiveGroup[Hitable]
781
782 # All sprites
783 var sprites = new LiveGroup[LiveObject]
784
785 init
786 do
787 self.player = new Player(self)
788 player.x = 400 * 100
789 player.y = 500 * 100
790 self.sprites.add(background)
791 self.sprites.add(pasive_stuff)
792 self.sprites.add(loots)
793 self.sprites.add(player_shoots)
794 self.sprites.add(enemy_shoots)
795 self.sprites.add(enemies)
796 self.sprites.add(self.player)
797 self.sprites.add(hitables)
798
799 for i in [0..100[ do
800 background.add(new Star)
801 end
802 end
803
804 # Generate an explosion
805 fun explosion(x, y: Int, radius: Int)
806 do
807 # Project explosion parts from the given position
808 # The strong friction and the short ttl of each part will achieve the effect
809 for i in [0..radius[ do
810 var ex = new Explosion
811 ex.x = x
812 ex.y = y
813 ex.set_velocity(100.rand.to_f*pi/50.0, (50*radius).rand)
814 ex.ttl += radius.rand
815 pasive_stuff.add(ex)
816 end
817 end
818
819 var enemy_remains: Int = 15
820 var boss_wait_ttl: Int = 0
821 var boss: nullable Boss
822
823 redef fun update
824 do
825 sprites.gc
826 sprites.update
827
828 if enemy_remains == 0 then
829 if boss_wait_ttl > 0 then
830 boss_wait_ttl -= 1
831 else if boss == null then
832 boss = new Boss(self)
833 enemy_remains = 15
834 else if not boss.exists then
835 boss = null
836 end
837 else if 100.rand < 1 then
838 enemy_remains -= 1
839 if enemy_remains == 0 then
840 boss_wait_ttl = 500
841 end
842 var rnd = 100.rand
843 var enemy: Enemy
844 if rnd < 40 then
845 enemy = new Enemy0(self)
846 else if rnd < 60 then
847 enemy = new Enemy1(self)
848 else if rnd < 70 then
849 enemy = new EnemyKamikaze(self)
850 else if rnd < 90 then
851 enemy = new Enemy2(self)
852 else if rnd < 95 then
853 enemy = new Enemy3(self)
854 else
855 enemy = new Enemy4(self)
856 end
857 enemy.x = 600.rand * 100 + 10000
858 enemy.vy = 200.rand + 100
859 if 10.rand < 3 then
860 enemy.vx = 200.rand - 100
861 end
862 end
863
864 for ps in player_shoots do
865 if not ps.exists then continue
866 var target: nullable Enemy = null
867 var td = 100000 # big int
868 for e in enemies do
869 if not e.exists then continue
870 if ps.overlaps(e) then
871 ps.exists = false
872 e.hit
873 end
874 var d = (e.x - ps.x).abs + (e.y - ps.y).abs
875 if td > d then
876 target = e
877 td = d
878 end
879 end
880 if ps isa Missile and (ps.target == null or not ps.target.exists) then
881 ps.target = target
882 end
883 end
884
885 for e in enemies do
886 if not e.exists then continue
887 if player.exists and player.overlaps(e) then
888 e.hit_by_player(player)
889 end
890 end
891 for s in enemy_shoots do
892 if not s.exists then continue
893 if player.exists and player.overlaps(s) then
894 s.hit_by_player(player)
895 end
896 end
897 for l in loots do
898 if not l.exists then continue
899 if player.exists and player.overlaps(l) then
900 l.hit_by_player(player)
901 end
902 end
903 for l in hitables do
904 if not l.exists then continue
905 if player.exists and player.overlaps(l) then
906 l.hit_by_player(player)
907 end
908 end
909 if not player.exists then
910 if player.respawn_ttl > 0 then
911 player.respawn_ttl -= 1
912 else
913 player.exists = true
914 player.protected_ttl = 100
915 self.sprites.add(self.player)
916 end
917 end
918 end
919 end
920
921 ###
922
923 class MenuScene
924 super Scene
925
926 var sprites = new LiveGroup[LiveObject]
927
928 init
929 do
930 for i in [0..100[ do
931 sprites.add(new Star)
932 end
933 end
934
935 var play: Bool writable = false
936 var ttl: Int = 50
937
938 redef fun update
939 do
940 sprites.update
941
942 if not play then return
943 if ttl > 0 then
944 ttl -= 1
945 return
946 end
947 next_scene = new PlayScene
948 end
949 end
950
951 fun headless_run
952 do
953 print "Headless run"
954 # Only run the playscene
955 var scene = new PlayScene
956 # beefup the player
957 scene.player.nbshoots = 5
958 scene.player.nbmissiles = 5
959 # play
960 print "Play"
961 var turns = 10
962 if args.length > 0 then
963 turns = args.first.to_i
964 end
965 for i in [0..turns[ do
966 for j in [0..10000[ do
967 scene.update
968 end
969 print "{i}: money={scene.player.money} enemies={scene.enemies.length} shoots={scene.player_shoots.length}"
970 end
971 print "Game Over"
972 end
973
974 headless_run