example: add example/shoot/shot_logic.nit
authorJean Privat <jean@pryen.org>
Thu, 25 Oct 2012 15:18:09 +0000 (11:18 -0400)
committerJean Privat <jean@pryen.org>
Thu, 25 Oct 2012 15:53:39 +0000 (11:53 -0400)
Currently it can be used as a benchmark.

Signed-off-by: Jean Privat <jean@pryen.org>

examples/shoot/shoot_logic.nit [new file with mode: 0644]

diff --git a/examples/shoot/shoot_logic.nit b/examples/shoot/shoot_logic.nit
new file mode 100644 (file)
index 0000000..34f199b
--- /dev/null
@@ -0,0 +1,970 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Space shooter.
+# This program is a fun game but also a good example of the scene2d module
+module shoot_logic
+
+import scene2d
+
+# The ship of the player
+class Player
+       super Sprite
+
+       # Current forture of the player
+       var money: Int writable = 0
+
+       # Number of basic bullets fired together
+       var nbshoots: Int writable = 1
+
+       # Time bebore the player shoot again a basic bullet (cooldown)
+       # Shoot if 0
+       var shoot_ttl = 0
+
+       # Number of missiles
+       var nbmissiles: Int writable = 0
+
+       # Time bebore the player shoot again a missile (cooldown)
+       # Shoot if 0
+       var missile_ttl = 0
+
+       # Remainind time when the player is protected from impacts
+       var protected_ttl = 0
+
+       # The associated play scene
+       # (mainly used to registed shoots)
+       var scene: PlayScene
+
+       init(scene: PlayScene) do
+               self.scene = scene
+               self.width = 2400
+               self.height = 2400
+       end
+
+       redef fun update
+       do
+               super
+
+               # Out of screen?
+               if self.y < 0 then
+                       self.y = 0
+                       self.vy = 0
+               else if self.y > 60000 then
+                       self.y = 60000
+                       self.vy = 0
+               end
+               if self.x < 0 then
+                       self.x = 0
+                       self.vx = 0
+               else if self.x > 80000 then
+                       self.x = 80000
+                       self.vx = 0
+               end
+
+               # Update of the player protection if any
+               if protected_ttl > 0 then protected_ttl -= 1
+
+               # Need to shoot basic bullets?
+               if shoot_ttl > 0 then
+                       shoot_ttl -= 1
+               else
+                       shoot_ttl = 30
+                       for i in [0..nbshoots[ do
+                               var shoot = new Shoot
+                               shoot.x = x
+                               shoot.y = top
+                               shoot.vy = -500
+                               shoot.vx = (i - nbshoots / 2) * 100
+                               scene.player_shoots.add(shoot)
+                       end
+               end
+
+               # Need to shoot missiles?
+               if missile_ttl > 0 then
+                       missile_ttl -= 1
+               else if nbmissiles > 0 then
+                       missile_ttl = 500 / nbmissiles
+                       var shoot = new Missile
+                       shoot.x = x
+                       shoot.y = top
+                       shoot.vy = -300
+                       shoot.vx = 0
+                       scene.player_shoots.add(shoot)
+               end
+
+       end
+
+       # Time before the player is respawned by the scene
+       var respawn_ttl: Int = 0
+
+       fun hit
+       do
+               if self.protected_ttl > 0 then return
+               self.scene.explosion(self.x, self.y, 10)
+               self.exists = false
+
+               # Reset the position for respawn
+               self.x = 400 * 100
+               self.y = 500 * 100
+               self.vx = 0
+               self.vy = 0
+               self.respawn_ttl = 50
+       end
+
+end
+
+# Sprites that may be hit by the player.
+# Eq. enemies, bullets, loots, etc.
+class Hitable
+       super Sprite
+
+       # What do do when self is hit by the player.
+       # By defaut, do nothing
+       fun hit_by_player(player: Player) do end
+end
+
+# A bullet shooted by a ship
+class Shoot
+       super Hitable
+
+       # Was the shoot fired by an enemy.
+       # Since there is no frendly fire, it is important to distinguish ownership
+       var enemy: Bool = false
+
+       init do
+               self.width = 800
+               self.height = 800
+       end
+
+       redef fun update
+       do
+               super
+
+               # Out of screen ?
+               if self.y < -100 * 100 or self.y > 700 * 100 or self.x < -100 * 100 or self.x > 900 * 100 then
+                       self.exists = false
+               end
+       end
+
+       redef fun hit_by_player(player)
+       do
+               player.hit
+               self.exists = false
+       end
+end
+
+# A advanced bullet that aims a target (player or enemy)
+class Missile
+       super Shoot
+
+       # The target aquired by the missile
+       var target: nullable Sprite
+
+       # When ttl is 0 then the angle stay fixed
+       # The angle is updated toward the target if ttl>0
+       var ttl: Int = 200
+
+       redef fun update
+       do
+               super
+
+               # Do we still update the angle ?
+               if ttl <= 0 then return
+               ttl -= 1
+
+               # Do we have a target?
+               var target = self.target
+               if target == null or not target.exists then return
+
+               # Just update the angle
+               var angle = self.angle_to(target)
+               self.set_velocity(angle, 300)
+       end
+end
+
+# A enemy ship
+# Various enemies exists, each kind has its own subclass
+abstract class Enemy
+       super Hitable
+
+       # The scene of the ship
+       # Is used to store created bullets or to get info about the player
+       var scene: PlayScene
+
+       # Time bebore the enemy shoot again (cooldown)
+       # Shoot if 0
+       # The default value is used as a grace period to avoid a first shoot on
+       # the first update
+       var shoot_ttl = 50
+
+       init(scene: PlayScene)
+       do
+               self.width = 2400
+               self.height = 2400
+               self.scene = scene
+               scene.enemies.add(self)
+       end
+
+       redef fun update
+       do
+               super
+
+               # Out of screen ?
+               if self.y > 700 * 100 or self.x < -100 * 100 or self.x > 900 * 100 then
+                       # Note: no control on the top to let ennemies appear
+                       self.exists = false
+               end
+
+               # Need to shoot?
+               if shoot_ttl > 0 then
+                       shoot_ttl -= 1
+               else
+                       shoot
+               end
+       end
+
+       # Each enemy has its own kind of shoot strategy
+       # Note: is automatically called by update when shoot_ttl is expired
+       fun shoot do end
+
+       # Money given when the enemy is destroyed
+       fun loot: Int is abstract
+
+       # What to do when the enemy is hit by a player shoot (or by the player himself)?
+       # By default it kill the enemy in an explosion and generate a loot
+       fun hit
+       do
+               self.exists = false
+               scene.explosion(self.x, self.y, 5)
+               if 100.rand < 3 then
+                       var upmissile = new UpMissile
+                       upmissile.x = self.x
+                       upmissile.y = self.y
+                       upmissile.vx = 0
+                       upmissile.vy = 0
+                       scene.loots.add(upmissile)
+                       scene.hitables.add(new LootArea(upmissile, 2000))
+               else
+                       for i in [0..self.loot[ do
+                               var money = new Money
+                               money.x = self.x
+                               money.y = self.y
+                               money.set_velocity(100.rand.to_f*pi/50.0, (500+self.loot).rand)
+                               scene.loots.add(money)
+                               scene.hitables.add(new LootArea(money, 2000))
+                       end
+               end
+       end
+
+       redef fun hit_by_player(player)
+       do
+               player.hit
+               hit
+       end
+end
+
+# Basic enemy, do not shoot
+class Enemy0
+       super Enemy
+
+       redef fun loot do return 3
+end
+
+# Simple shooter of paris of basic bullets
+class Enemy1
+       super Enemy
+
+       redef fun shoot
+       do
+               # Next shoot
+               shoot_ttl = 50
+
+               # two bullets shoot each time
+               for dx in [-11, 11] do
+                       var shoot = new Shoot
+                       shoot.enemy = true
+                       shoot.x = self.x + dx * 100
+                       shoot.y = self.bottom
+                       shoot.vy = 500
+                       scene.enemy_shoots.add(shoot)
+               end
+       end
+
+       redef fun loot do return 5
+end
+
+# Enemy that shoot missiles
+class Enemy2
+       super Enemy
+
+       redef fun shoot
+       do
+               # Next shoot
+               shoot_ttl = 200
+
+               # The missile targets the player
+               var shoot = new Missile
+               shoot.enemy = true
+               shoot.x = self.x
+               shoot.y = self.bottom
+               shoot.vy = 500
+               shoot.target = scene.player
+               scene.enemy_shoots.add(shoot)
+       end
+
+       redef fun loot do return 10
+end
+
+# Enem that shoot rings of basic bullets
+class Enemy3
+       super Enemy
+
+       redef fun shoot
+       do
+               # Next shoot
+               shoot_ttl = 50
+
+               for i in [0..10[ do
+                       var shoot = new Shoot
+                       shoot.enemy = true
+                       shoot.x = self.x
+                       shoot.y = self.bottom
+                       shoot.set_velocity(pi/5.0*i.to_f, 500)
+                       scene.enemy_shoots.add(shoot)
+               end
+       end
+
+       redef fun loot do return 20
+end
+
+# Enemy with a turret that shoot burst of bullets toward the player
+class Enemy4
+       super Enemy
+
+       # The angle of the turret
+       var angle: Float = 0.0
+
+       redef fun update
+       do
+               super
+
+               # Rotate the turret toward the player
+               var target = scene.player
+               if target.exists then
+                       angle = self.angle_to(target)
+               end
+       end
+
+       # Shoots come in burst
+       var nbshoots: Int = 0
+
+       redef fun shoot
+       do
+               # Next shoot: is there still bullets in the burst?
+               if self.nbshoots < 10 then
+                       # Is ther
+                       self.nbshoots += 1
+                       shoot_ttl = 5
+               else
+                       self.nbshoots = 0
+                       shoot_ttl = 80
+               end
+
+               # Shoot with the turret angle
+               var shoot = new Shoot
+               shoot.enemy = true
+               shoot.x = self.x
+               shoot.y = self.y
+               shoot.set_velocity(angle, 500)
+               scene.enemy_shoots.add(shoot)
+       end
+
+       redef fun loot do return 20
+end
+
+# Enemy that rush directly on the player
+class EnemyKamikaze
+       super Enemy
+
+       redef fun update
+       do
+               super
+
+               # Try to target the player
+               var target = scene.player
+               if not target.exists then return
+
+               var angle = self.angle_to(target)
+               self.set_velocity(angle, 600)
+       end
+
+       redef fun loot do return 5
+end
+
+# The boss has two semi-independent arms
+class Boss
+       super Enemy
+
+       # Left arm
+       var left_part: BossPart
+
+       # Right arm
+       var right_part: BossPart
+
+       init(scene)
+       do
+               super
+               self.width = 128 * 100
+               self.height = 100 * 100
+               self.x = 400 * 100
+               self.y = -100 * 100
+               self.left_part = new BossPart(self, -48*100)
+               self.right_part = new BossPart(self, 48*100)
+       end
+
+       var flick_ttl: Int = 0
+
+       redef fun update
+       do
+               if flick_ttl > 0 then flick_ttl -= 1
+
+               # Path of the boss (down then left<->right)
+               if self.y < 20000 then
+                       self.vx = 0
+                       self.vy = 100
+               else if self.vx == 0 then
+                       self.vx = 100
+                       self.vy = 0
+               else if self.x > 700 * 100 and self.vx > 0 then
+                       self.vx = -self.vx
+               else if self.x < 100 * 100 and self.vx < 0 then
+                       self.vx = -self.vx
+               end
+
+               super
+       end
+
+       redef fun shoot
+       do
+               # Do not shoot if not ready
+               if self.vy != 0 then return
+
+               # Try to target the player
+               var target = scene.player
+               if not target.exists then return
+
+               # Next shoot: burst if no arms remains
+               if left_part.exists or right_part.exists then
+                       shoot_ttl = 60
+               else
+                       shoot_ttl = 20
+               end
+
+               # Shoot the player with a basic bullet
+               var shoot = new Shoot
+               shoot.enemy = true
+               shoot.x = self.x
+               shoot.y = self.bottom
+               var angle = shoot.angle_to(target)
+               shoot.set_velocity(angle, 500)
+               scene.enemy_shoots.add(shoot)
+       end
+
+       redef fun loot do return 100
+
+       var live: Int = 20
+
+       redef fun hit
+       do
+               # Protected while an arm remains
+               if left_part.exists or right_part.exists then return
+
+               if live > 0 then
+                       live -= 1
+                       flick_ttl = 2
+               else
+                       super
+                       scene.explosion(self.x, self.y, 30)
+               end
+       end
+end
+
+# An arm of a boss
+class BossPart
+       super Enemy
+
+       # The associated boss
+       var boss: Boss
+
+       # Relative x coordonate (center to center) of the arm
+       var relx: Int
+
+       # Relative y coordonate (center to center) of the arm
+       var rely: Int = 36 * 100
+
+       var live: Int = 10
+
+       init(boss: Boss, relx: Int)
+       do
+               self.boss = boss
+               self.relx = relx
+               super(boss.scene)
+               self.width = 32 * 100
+               self.height = 60 * 100
+
+               # Alternate the shoots of the arms
+               if relx > 0 then
+                       shoot_ttl += 300
+               end
+               self.x = boss.x + relx
+               self.y = boss.y + rely
+       end
+
+       redef fun update
+       do
+               self.x = boss.x + relx
+               self.y = boss.y + rely
+
+               super
+
+               if flick_ttl > 0 then flick_ttl -= 1
+       end
+
+       redef fun shoot
+       do
+               # Do not shoot if not ready
+               if self.boss.vy != 0 then return
+
+               # Next shoot
+               shoot_ttl = 600
+
+               # Shoot a missile that targets the player
+               var shoot = new Missile
+               shoot.enemy = true
+               shoot.x = self.x
+               shoot.y = self.bottom
+               shoot.vy = 500
+               shoot.target = scene.player
+               scene.enemy_shoots.add(shoot)
+       end
+
+       var flick_ttl: Int = 0
+
+       redef fun hit
+       do
+               if live > 0 then
+                       live -= 1
+                       flick_ttl = 2
+               else
+                       super
+               end
+       end
+
+       redef fun loot do return 10
+end
+
+# Whatever reward or bonus that can be picked by the player
+abstract class Loot
+       super Hitable
+
+       init
+       do
+               self.width = 400
+               self.height = 400
+       end
+
+       # Magnet effect: The loot will move to the target if set
+       # See LootArea for details
+       var target: nullable Sprite = null
+
+       redef fun update
+       do
+               super
+
+               # Out of screen ?
+               if self.y > 700 * 100 then
+                       self.exists = false
+               end
+
+               var target = self.target
+               if target == null then
+                       # Not magneted: deploy
+
+                       # Heavy fuild friction to stops the explosion
+                       # Loots are placed with a explosion, see `Enemy::hit'
+                       self.vx = self.vx*7/8
+                       self.vy = self.vy*7/8
+
+                       # Background scroling
+                       self.y += 50
+
+               else if target.exists then
+                       # Magneted: rush toward the target
+                       var angle = self.angle_to(target)
+                       self.set_velocity(angle, 800)
+
+               else
+                       # Magneted but dead target: reset the loot
+                       self.vx = 0
+                       self.vy = 0
+                       self.target = null
+               end
+       end
+end
+
+# Basic money loot
+class Money
+       super Loot
+
+       redef fun hit_by_player(player)
+       do
+               self.exists = false
+               player.money += 1
+               if player.money > 100 then
+                       player.money -= 100
+                       player.nbshoots += 1
+               end
+       end
+end
+
+# Increase the number of missiles
+class UpMissile
+       super Loot
+
+       redef fun hit_by_player(player)
+       do
+               self.exists = false
+               player.nbmissiles += 1
+       end
+end
+
+# A loot area is an invisible field used to implement the magnet effets of loots
+# The principle is:
+#  * the loot is an invisible sprite with a hitbox larger than the loot hitbox
+#  * the lootbox remains centered on the loot
+#  * when the player hit the lootarea, then the loot is set to target the player
+#  * when the player hit the loot, then the player gains effectively the loot
+class LootArea
+       super Hitable
+
+       # The associated loot
+       var loot: Loot
+
+       init(loot: Loot, radius: Int)
+       do
+               self.loot = loot
+               self.width = radius * 2 + loot.width
+               self.height = radius * 2 + loot.height
+       end
+
+       redef fun update
+       do
+               # position remains centered on the loot
+               self.x = loot.x
+               self.y = loot.y
+
+               # No area if no loot
+               if not loot.exists then self.exists = false
+
+               # the super is useless but it is a good practice to call it
+               super
+       end
+
+       redef fun hit_by_player(player)
+       do
+               # Kill the area
+               self.exists = false
+
+               # The loot now targets the player
+               loot.target = player
+       end
+end
+
+# A non interactive element of an explosion
+# A real explosion is made of many Explosion object
+# Use the `PlayScene::explosion` method to generate a full explosion
+class Explosion
+       super Sprite
+
+       # Time before the sprite vanishes
+       var ttl: Int = 10
+
+       redef fun update
+       do
+               # Heavy fuild friction to stops the explosion
+               self.vx = self.vx*7/8
+               self.vy = self.vy*7/8
+
+               # Background scrolling
+               self.y += 50
+
+               super
+
+               # Vanishes?
+               if ttl > 0 then
+                       ttl -= 1
+               else
+                       exists = false
+               end
+       end
+end
+
+# A star is a non-interactive background element
+# Stars are used to simulate a continuous global scroling
+class Star
+       super Sprite
+
+       init
+       do
+               # Randomely places stars on the plane
+               self.x = 800.rand * 100
+               self.y = 600.rand * 100
+               self.vy = 40.rand + 11
+       end
+
+       redef fun update
+       do
+               super
+
+               # Replace the star on the top
+               if self.y > 600 * 100 then
+                       self.y = 200.rand * -100
+                       self.x = 800.rand * 100
+                       self.vy = 40.rand + 11
+               end
+       end
+end
+
+redef class Scene
+       # When a scene need to be replaced, just assign the next_scene to a non null value
+       var next_scene: nullable Scene writable = null
+end
+
+# The main play state
+class PlayScene
+       super Scene
+
+       # The player ship
+       var player: Player
+
+       # Shoots of the player
+       var player_shoots = new LiveGroup[Shoot]
+
+       # Enemy ships
+       var enemies = new LiveGroup[Enemy]
+
+       # Soots of the enemy
+       var enemy_shoots = new LiveGroup[Shoot]
+
+       # Collectible loots
+       var loots = new LiveGroup[Loot]
+
+       # Non active stuff like explosions
+       var pasive_stuff = new LiveGroup[LiveObject]
+
+       # Background stuff like stars
+       var background = new LiveGroup[LiveObject]
+
+       # All other hitable sprites
+       var hitables = new LiveGroup[Hitable]
+
+       # All sprites
+       var sprites = new LiveGroup[LiveObject]
+
+       init
+       do
+               self.player = new Player(self)
+               player.x = 400 * 100
+               player.y = 500 * 100
+               self.sprites.add(background)
+               self.sprites.add(pasive_stuff)
+               self.sprites.add(loots)
+               self.sprites.add(player_shoots)
+               self.sprites.add(enemy_shoots)
+               self.sprites.add(enemies)
+               self.sprites.add(self.player)
+               self.sprites.add(hitables)
+
+               for i in [0..100[ do
+                       background.add(new Star)
+               end
+       end
+
+       # Generate an explosion
+       fun explosion(x, y: Int, radius: Int)
+       do
+               # Project explosion parts from the given position
+               # The strong friction and the short ttl of each part will achieve the effect
+               for i in [0..radius[ do
+                       var ex = new Explosion
+                       ex.x = x
+                       ex.y = y
+                       ex.set_velocity(100.rand.to_f*pi/50.0, (50*radius).rand)
+                       ex.ttl += radius.rand
+                       pasive_stuff.add(ex)
+               end
+       end
+
+       var enemy_remains: Int = 15
+       var boss_wait_ttl: Int = 0
+       var boss: nullable Boss
+
+       redef fun update
+       do
+               sprites.gc
+               sprites.update
+
+               if enemy_remains == 0 then
+                       if boss_wait_ttl > 0 then
+                               boss_wait_ttl -= 1
+                       else if boss == null then
+                               boss = new Boss(self)
+                               enemy_remains = 15
+                       else if not boss.exists then
+                               boss = null
+                       end
+               else if 100.rand < 1 then
+                       enemy_remains -= 1
+                       if enemy_remains == 0 then
+                               boss_wait_ttl = 500
+                       end
+                       var rnd = 100.rand
+                       var enemy: Enemy
+                       if rnd < 40 then
+                               enemy = new Enemy0(self)
+                       else if rnd < 60 then
+                               enemy = new Enemy1(self)
+                       else if rnd < 70 then
+                               enemy = new EnemyKamikaze(self)
+                       else if rnd < 90 then
+                               enemy = new Enemy2(self)
+                       else if rnd < 95 then
+                               enemy = new Enemy3(self)
+                       else
+                               enemy = new Enemy4(self)
+                       end
+                       enemy.x = 600.rand * 100 + 10000
+                       enemy.vy = 200.rand + 100
+                       if 10.rand < 3 then
+                               enemy.vx = 200.rand - 100
+                       end
+               end
+
+               for ps in player_shoots do
+                       if not ps.exists then continue
+                       var target: nullable Enemy = null
+                       var td = 100000 # big int
+                       for e in enemies do
+                               if not e.exists then continue
+                               if ps.overlaps(e) then
+                                       ps.exists = false
+                                       e.hit
+                               end
+                               var d = (e.x - ps.x).abs + (e.y - ps.y).abs
+                               if td > d then
+                                       target = e
+                                       td = d
+                               end
+                       end
+                       if ps isa Missile and (ps.target == null or not ps.target.exists) then
+                               ps.target = target
+                       end
+               end
+
+               for e in enemies do
+                       if not e.exists then continue
+                       if player.exists and player.overlaps(e) then
+                               e.hit_by_player(player)
+                       end
+               end
+               for s in enemy_shoots do
+                       if not s.exists then continue
+                       if player.exists and player.overlaps(s) then
+                               s.hit_by_player(player)
+                       end
+               end
+               for l in loots do
+                       if not l.exists then continue
+                       if player.exists and player.overlaps(l) then
+                               l.hit_by_player(player)
+                       end
+               end
+               for l in hitables do
+                       if not l.exists then continue
+                       if player.exists and player.overlaps(l) then
+                               l.hit_by_player(player)
+                       end
+               end
+               if not player.exists then
+                       if player.respawn_ttl > 0 then
+                               player.respawn_ttl -= 1
+                       else
+                               player.exists = true
+                               player.protected_ttl = 100
+                               self.sprites.add(self.player)
+                       end
+               end
+       end
+end
+
+###
+
+class MenuScene
+       super Scene
+
+       var sprites = new LiveGroup[LiveObject]
+
+       init
+       do
+               for i in [0..100[ do
+                       sprites.add(new Star)
+               end
+       end
+
+       var play: Bool writable = false
+       var ttl: Int = 50
+
+       redef fun update
+       do
+               sprites.update
+
+               if not play then return
+               if ttl > 0 then
+                       ttl -= 1
+                       return
+               end
+               next_scene = new PlayScene
+       end
+end
+
+fun headless_run
+do
+       print "Headless run"
+       # Only run the playscene
+       var scene = new PlayScene
+       # beefup the player
+       scene.player.nbshoots = 5
+       scene.player.nbmissiles = 5
+       # play
+       print "Play"
+       for i in [0..10[ do
+               for j in [0..10000[ do
+                       scene.update
+               end
+               print "{i}: money={scene.player.money} enemies={scene.enemies.length} shoots={scene.player_shoots.length}"
+       end
+       print "Game Over"
+end
+
+headless_run