--- /dev/null
+# 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