+++ /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
-
- # Where the player is going
- var going_target = new GoingTarget
-
- # Activate the `going_target`
- fun goes_to(x,y: Int, speed: Int)
- do
- going_target.x = x
- going_target.y = y
- going_target.active = true
- var angle = angle_to(going_target)
- set_velocity(angle, speed)
- end
-
- # Current forture of the player
- var money: Int = 0 is writable
-
- # Number of basic bullets fired together
- var nbshoots: Int = 1 is writable
-
- # Time bebore the player shoot again a basic bullet (cooldown)
- # Shoot if 0
- var shoot_ttl = 0
-
- # Number of missiles
- var nbmissiles: Int = 0 is writable
-
- # 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 > scene.height then
- self.y = scene.height
- self.vy = 0
- end
- if self.x < 0 then
- self.x = 0
- self.vx = 0
- else if self.x > scene.width then
- self.x = scene.width
- 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(scene)
- 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(scene)
- 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 = scene.width / 2
- self.y = scene.height - 10000
- 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
-
-# Destination for the player (pointer position)
-class GoingTarget
- super Hitable
-
- # true in on move, false if player is at rest
- var active = false is writable
-
- init do
- self.width = 500
- self.height = 500
- end
-
- redef fun hit_by_player(player)
- do
- if not active then return
- active = false
- player.vx = 0
- player.vy = 0
- 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
-
- # The scene of the sprite
- # Is used with bound limits
- var scene: PlayScene
-
- init(scene: PlayScene)
- do
- self.scene = scene
- self.width = 800
- self.height = 800
- end
-
- redef fun update
- do
- super
-
- # Out of screen ?
- if self.y < -100 * 100 or self.y > scene.height + 10000 or self.x < -100 * 100 or self.x > scene.width + 10000 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 = null
-
- # 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 > scene.height + 10000 or self.x < -100 * 100 or self.x > scene.width + 10000 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(scene)
- 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(scene)
- 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
-
- redef init(scene)
- do
- self.width = 3600
- self.height = 3600
- end
-end
-
-# Simple shooter of pairs of basic bullets
-class Enemy1
- super Enemy
-
- redef init(scene)
- do
- self.width = 4400
- self.height = 4400
- end
-
- redef fun shoot
- do
- # Next shoot
- shoot_ttl = 50
-
- # two bullets shoot each time
- for dx in [-11, 11] do
- var shoot = new Shoot(scene)
- 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 init(scene)
- do
- self.width = 6000
- self.height = 6000
- end
-
- redef fun shoot
- do
- # Next shoot
- shoot_ttl = 200
-
- # The missile targets the player
- var shoot = new Missile(scene)
- 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
-
-# Enemy that shoot rings of basic bullets
-class Enemy3
- super Enemy
-
- redef init(scene)
- do
- self.width = 5800
- self.height = 5800
- end
-
- redef fun shoot
- do
- # Next shoot
- shoot_ttl = 50
-
- for i in [0..10[ do
- var shoot = new Shoot(scene)
- 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 init(scene)
- do
- self.width = 4200
- self.height = 4200
- end
-
- 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(scene)
- 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 init(scene)
- do
- self.width = 3200
- self.height = 3200
- end
-
- 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 = 140 * 100
- self.height = 96 * 100
- self.x = scene.width / 2
- self.y = -100 * 100
- self.left_part = new BossPart(self, -66*100)
- self.right_part = new BossPart(self, 66*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 > scene.width - 10000 and self.vx > 0 then
- self.vx = -self.vx
- else if self.x < 10000 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(scene)
- 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 = 38 * 100
- self.height = 48 * 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(scene)
- 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
-
- var scene: PlayScene
-
- init(scene: PlayScene)
- do
- self.scene = scene
- 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 > scene.height + 10000 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
-
- # The scene of the sprite
- # Is used with bound limits
- var scene: ShotScene
-
- init(scene: ShotScene)
- do
- self.scene = scene
- # Randomely places stars on the plane
- self.x = scene.width.rand
- self.y = scene.height.rand
- self.vy = 40.rand + 11
- end
-
- redef fun update
- do
- super
-
- # Replace the star on the top
- if self.y > scene.height then
- self.y = 200.rand * -100
- self.x = scene.width.rand
- self.vy = 40.rand + 11
- end
- end
-end
-
-class ShotScene
- super Scene
-
- # When a scene need to be replaced, just assign the next_scene to a non null value
- var next_scene: nullable ShotScene = null is writable
-
- # The width of the whole scene
- var width: Int is writable
-
- # The height of the whole scene
- var height: Int is writable
-
- init(w,h: Int)
- do
- width = w
- height = h
- end
-end
-
-# The main play state
-class PlayScene
- super ShotScene
-
- # 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(w,h)
- do
- super
- self.player = new Player(self)
- player.x = self.width / 2
- player.y = self.height - 10000
- 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(self))
- end
-
- hitables.add(player.going_target)
- 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 = (self.width - 20000).rand + 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 ShotScene
-
- var sprites = new LiveGroup[LiveObject]
-
- init(w,h)
- do
- super
- for i in [0..100[ do
- sprites.add(new Star(self))
- end
- end
-
- var play: Bool = false is writable
- 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(width,height)
- end
-end
-
-fun headless_run
-do
- srand_from 0
- print "Headless run"
- # Only run the playscene
- var scene = new PlayScene(80000,60000)
- # beefup the player
- scene.player.nbshoots = 5
- scene.player.nbmissiles = 5
- # play
- print "Play"
- var turns = 10
- if args.length > 0 then
- turns = args.first.to_i
- end
- for i in [0..turns[ 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