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