X-Git-Url: http://nitlanguage.org diff --git a/contrib/action_nitro/src/action_nitro.nit b/contrib/action_nitro/src/action_nitro.nit new file mode 100644 index 0000000..17c7b05 --- /dev/null +++ b/contrib/action_nitro/src/action_nitro.nit @@ -0,0 +1,703 @@ +# 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. + +module action_nitro + +import gamnit::depth +import gamnit::keys +import gamnit::limit_fps + +import game + +import gen::texts +import gen::planes + +redef class App + + # Game world + var world: World = new World is lazy + + # --- + # Game world assets + + # Textures of the biplane, jet, helicopter, parachute and powerups + private var planes_sheet = new PlanesImages + + # Animation for the player movement + private var player_textures: Array[Texture] = + [for f in [1..12] do new Texture("textures/player/frame_{f.pad(2)}.png")] + + # Boss 3D model + private var iss_model = new Model("models/iss.obj") + + # --- + # Ground textures + + private var ground_texture = new Texture("textures/fastgras01.jpg") + private var tree_texture = new Texture("textures/Tree03.png") + + # --- + # Blood splatter + + private var splatter_texture = new Texture("textures/blood_splatter.png") + private var splatter_material: TexturedMaterial do + var mat = new TexturedMaterial([1.0]*4, [0.0]*4, [0.0]*4) + mat.ambient_texture = splatter_texture + return mat + end + private var splatter_model = new LeafModel(new Plane, splatter_material) + + # --- + # Background + + private var city_texture = new Texture("textures/city_background_clean.png") + + private var stars_texture = new Texture("textures/stars.jpg") + private var stars = new Sprite(stars_texture, new Point3d[Float](0.0, 1100.0, -600.0)) is lazy + + # --- + # Particle effects + + # Explosion particles + var explosions = new ParticleSystem(20, explosion_program, + new Texture("particles/explosion00.png")) + + # Blood explosion particles + var blood = new ParticleSystem(20, explosion_program, + new Texture("particles/blood07.png")) + + # Smoke for the background + var smoke = new ParticleSystem(500, smoke_program, + new Texture("particles/blackSmoke12.png")) + + # Static clouds particles + var clouds = new ParticleSystem(1600, static_program, + new Texture("particles/whitePuff12.png")) + + # --- + # Sound effects + + # TODO + #private var fx_fire = new Sound("sounds/fire.mp3") + + # --- + # UI + private var texts_sheet = new TextsImages + + private var tutorial_wasd = new Sprite(app.texts_sheet.tutorial_wasd, + app.ui_camera.center.offset(0.0, -250.0, 0.0)) is lazy + + private var tutorial_arrows = new Sprite(app.texts_sheet.tutorial_arrows, + app.ui_camera.center.offset(0.0, -350.0, 0.0)) is lazy + + private var tutorial_chute = new Sprite(app.texts_sheet.tutorial_chute, + app.ui_camera.center.offset(0.0, -450.0, 0.0)) is lazy + + private var tutorial_goal = new Sprite(app.texts_sheet.goal, + app.ui_camera.center.offset(0.0, 0.0, 0.0)) is lazy + + private var outro_directed = new Sprite(app.texts_sheet.directed, + app.ui_camera.center.offset(0.0, 400.0, 0.0)) is lazy + + private var outro_created = new Sprite(app.texts_sheet.created, + app.ui_camera.center.offset(0.0, -200.0, 0.0)) is lazy + + # --- + # Counters for the UI + + private var score_counter = new CounterSprites(texts_sheet.n, + new Point3d[Float](32.0, -64.0, 0.0)) + + private var altitude_counter = new CounterSprites(texts_sheet.n, + new Point3d[Float](1400.0, -64.0, 0.0)) + + redef fun on_create + do + super + + show_splash_screen new Texture("textures/splash.jpg") + + # Load 3d models + iss_model.load + + # Setup cameras + world_camera.reset_height 40.0 + ui_camera.reset_height 1080.0 + + # Register particle systems + particle_systems.add explosions + particle_systems.add blood + particle_systems.add smoke + particle_systems.add clouds + + # Stars background + sprites.add stars + stars.scale = 2.1 + + # City background + var city_sprite = new Sprite(city_texture, new Point3d[Float](0.0, 370.0, -600.0)) + city_sprite.scale = 0.8 + sprites.add city_sprite + + # Setup ground + var ground_mesh = new Plane + ground_mesh.repeat_x = 100.0 + ground_mesh.repeat_y = 100.0 + + var ground_material = new TexturedMaterial( + [0.0, 0.1, 0.0, 1.0], [0.4, 0.4, 0.4, 1.0], [0.0]*4) + ground_material.diffuse_texture = ground_texture + + var ground_model = new LeafModel(ground_mesh, ground_material) + var ground = new Actor(ground_model, new Point3d[Float](0.0, 0.0, 0.0)) + ground.scale = 5000.0 + actors.add ground + + # Trees + for i in 1000.times do + var s = 0.1 + 0.1.rand + var h = tree_texture.height * s + var sprite = new Sprite(tree_texture, + new Point3d[Float](0.0 & 1500.0, h/2.0 - 10.0*s, 10.0 - 609.0.rand)) + sprite.scale = s + sprites.add sprite + + var c = 1.0.rand + c *= 0.7 + sprite.tint = [c, 1.0, c, 1.0] + end + + # Clouds + var no_clouds_layer = 200.0 + for i in [0 .. 32[ do + var zp = 1.0.rand + var x = 0.0 & 1000.0 * zp + var y = no_clouds_layer + (world.boss_altitude - no_clouds_layer*2.0).rand + var z = -500.0*zp - 10.0 + + var r = 50.0 & 1.0 + for j in [0..32[ do + var a = 2.0*pi.rand + var rj = r.rand + clouds.add(new Point3d[Float](x+2.0*a.cos*rj, y+a.sin*rj, z & 1.0), + 48000.0 & 16000.0, inf) + end + end + + # Move the sun to best light the ISS + light.position.x = 2000.0 + light.position.z = 4000.0 + + # Prepare for intro animation + ui_sprites.add tutorial_goal + world_camera.far = 700.0 + end + + redef fun update(dt) + do + # Game logic + world.update dt + + # Update background color + var player = world.player + var player_pos = if player != null then player.center else new Point3d[Float](0.0, 200.0, 0.0) + var altitude = player_pos.y + var p = altitude / world.boss_altitude + var ip = 1.0 - p + glClearColor(0.3*ip, 0.3*ip, ip, 1.0) + stars.alpha = (1.4*p-0.4).min(1.0).max(0.0) + + # Randomly add smoke + var poss = [ + new Point3d[Float](291.0, 338.0, -601.0), + new Point3d[Float](-356.0, 422.0, -601.0)] + + var r = 8.0 + if 2.rand == 0 then + var pos = poss.rand + smoke.add( + new Point3d[Float](pos.x & r, pos.y & r, pos.z & r), + 96000.0 & 16000.0, 10.0) + end + + # Move camera + world_camera.position.x = player_pos.x + world_camera.position.y = player_pos.y + 5.0 + + # Cinematic? + var t = world.t + var intro_duration = 8.0 + if t < intro_duration then + var pitch = t/intro_duration + pitch = (pitch*pi).sin + world_camera.pitch = pitch + return + end + + if world.player == null then + # Game is starting! + world.spawn_player + world.planes.add new Airplane(new Point3d[Float](0.0, world.player.center.y - 10.0, 0.0), 16.0, 4.0) + + # Setup tutorial + ui_sprites.clear + ui_sprites.add_all([tutorial_wasd, tutorial_arrows, tutorial_chute]) + + world_camera.pitch = 0.0 + world_camera.far = 700.0 + end + + # Update counters + score_counter.value = world.score + var alt = 0 + if world.player != null then alt = world.player.altitude.to_i + altitude_counter.value = alt + + # General movement on the X axis + if player != null then + player.moving = 0.0 + if pressed_keys.has("left") then player.moving -= 1.0 + if pressed_keys.has("right") then player.moving += 1.0 + end + + # Try to fire as long as a key is pressed + if pressed_keys.not_empty then + var a = inf + if pressed_keys.has("a") then + if pressed_keys.has("w") then + a = 0.75 * pi + else if pressed_keys.has("s") then + a = 1.25 * pi + else + a = pi + end + else if pressed_keys.has("d") then + if pressed_keys.has("w") then + a = 0.25 * pi + else if pressed_keys.has("s") then + a = 1.75 * pi + else + a = 0.0 + end + else if pressed_keys.has("w") then + a = 0.50 * pi + else if pressed_keys.has("s") then + a = 1.50 * pi + end + + if a != inf and player != null then + player.shoot(a, world) + hide_tutorial_wasd + end + end + + # Low-gravity controls + if player != null and player.is_alive and player.altitude >= world.boss_altitude then + var d = 50.0*dt + for key in pressed_keys do + if key == "up" then + player.inertia.y += d + else if key == "down" then + player.inertia.y -= d + else if key == "left" then + player.inertia.x -= d + else if key == "right" then + player.inertia.x += d + end + end + end + + # Detect if game won + var won_at = won_at + if won_at == null then + var boss = world.boss + if boss != null and not boss.is_alive then + self.won_at = world.t + end + else + # Show outro + var t_since_won = world.t - won_at + if t_since_won > 1.0 and not ui_sprites.has(outro_directed) then ui_sprites.add outro_directed + if t_since_won > 2.0 and not ui_sprites.has(outro_created) then ui_sprites.add outro_created + end + end + + # Seconds at which the game was won, using `world.t` as reference + private var won_at: nullable Float = null + + # Remove the tutorial sprite about WASD from `ui_sprites` + private fun hide_tutorial_wasd do if ui_sprites.has(tutorial_wasd) then ui_sprites.remove(tutorial_wasd) + + # Remove the tutorial sprite about arrows from `ui_sprites` + private fun hide_tutorial_arrows do if ui_sprites.has(tutorial_arrows) then ui_sprites.remove(tutorial_arrows) + + # Remove the tutorial sprite about the parachute from `ui_sprites` + private fun hide_tutorial_chute do if ui_sprites.has(tutorial_chute) then ui_sprites.remove(tutorial_chute) + + redef fun accept_event(event) + do + var s = super + + if event isa QuitEvent then + exit 0 + else if event isa KeyEvent then + if event.name == "escape" and event.is_down then + exit 0 + end + + var player = world.player + if player != null and player.is_alive then + + # Hide tutorial about arrows once they are used + var arrows = once ["left", "right"] + if arrows.has(event.name) then hide_tutorial_arrows + + if player.altitude < world.boss_altitude then + if event.name == "space" and event.is_down and not player.parachute_deployed and player.plane == null then + player.parachute + if player.parachute_deployed then + var pc = player.center + world.parachute = new Parachute(new Point3d[Float](pc.x, pc.y + 5.0, pc.z-0.1), 8.0, 5.0) + end + hide_tutorial_chute + end + + if (event.name == "space" or event.name == "up") and event.is_down then + player.jump + end + + if event.name == "left" then + var mod = if event.is_down then -1.0 else 1.0 + player.moving += mod + end + + if event.name == "right" then + var mod = if event.is_down then 1.0 else -1.0 + player.moving += mod + end + + if player.moving == 0.0 then + player.sprite.as(PlayerSprite).stop_running + else player.sprite.as(PlayerSprite).start_running + end + end + + # When player is dead, respawn on spacebar + if player != null and not player.is_alive then + if event.name == "space" then + ui_sprites.clear + world.spawn_player + end + end + end + + return s + end +end + +redef class Body + # Sprite representing this entity if there is no `actor` + fun sprite: Sprite is abstract + + # 3D actor + fun actor: nullable Actor do return null + + init + do + var actor = actor + if actor != null then + app.actors.add actor + else app.sprites.add sprite + end + + redef fun destroy(world) + do + super + + var actor = actor + if actor != null then + app.actors.remove actor + else app.sprites.remove sprite + end +end + +redef class Human + redef fun die(world) + do + super + + death_animation + end + + # Show death animation (explosion) + fun death_animation + do + var force = 4.0 + health = 0.0 + for i in 32.times do + app.blood.add( + new Point3d[Float](center.x & force, center.y & force, center.z & force), + (2048.0 & 4096.0) * force, 0.3 & 0.1) + end + end +end + +redef class Platform + init do sprite.scale = width/sprite.texture.width + + redef fun update(dt, world) + do + super + + if inertia.x < 0.0 then + sprite.invert_x = false + else if inertia.x > 0.0 then + sprite.invert_x = true + end + end +end + +redef class Airplane + private fun texture: Texture do return if center.y < 600.0 then app.planes_sheet.biplane else app.planes_sheet.jet + + redef var sprite = new Sprite(texture, center) is lazy +end + +redef class Helicopter + redef var sprite = new Sprite(app.planes_sheet.helicopter, center) is lazy +end + +redef class Boss + redef var actor is lazy do + var actor = new Actor(app.iss_model, center) + actor.rotation = pi/2.0 + return actor + end + + redef fun death_animation + do + var force = 64.0 + app.explosions.add(center, 4096.0 * force, 0.3) + for i in (8.0*force).to_i.times do + app.explosions.add( + new Point3d[Float](center.x & force, center.y & force/8.0, center.z & force), + (2048.0 & 1024.0) * force, 0.3 + 5.0.rand, 5.0.rand) + end + end +end + +redef class Enemy + redef var sprite = new Sprite(app.player_textures.rand, center) is lazy + init do sprite.scale = width/sprite.texture.width * 2.0 +end + +redef class Parachute + redef var sprite = new Sprite(app.planes_sheet.parachute, center) is lazy + init do sprite.scale = width / sprite.texture.width +end + +redef class Player + redef var sprite = new PlayerSprite(app.player_textures[1], center, app.player_textures, 0.08) is lazy + init do sprite.scale = width/sprite.texture.width * 2.0 + + redef fun update(dt, world) + do + super + if moving > 0.0 then + sprite.invert_x = false + else if moving < 0.0 then + sprite.invert_x = true + end + end + + redef fun die(world) + do + super + + if center.y < 10.0 then + # Blood splatter on the ground + var splatter = new Actor(app.splatter_model, + new Point3d[Float](center.x, 0.05 & 0.04, center.y)) + splatter.scale = 32.0 + splatter.rotation = 2.0 * pi.rand + app.actors.add splatter + end + + # Display respawn instructions + app.ui_sprites.add new Sprite(app.texts_sheet.respawn, app.ui_camera.center) + end +end + +redef class Bullet + redef var sprite = new Sprite(weapon.bullet_texture, center) is lazy + init + do + sprite.scale = 0.03 + sprite.rotation = angle + end +end + +redef class Weapon + fun bullet_texture: Texture do return app.planes_sheet.bullet_ak +end + +redef class Pistol + redef fun bullet_texture do return app.planes_sheet.bullet_pistol +end + +redef class RocketLauncher + redef fun bullet_texture do return app.planes_sheet.bullet_rocket +end + +redef class Powerup + # Scale so it looks like 5 world units wide, not matter the size of the texture + init do sprite.scale = 5.0/sprite.texture.width +end + +redef class Ak47PU + redef var sprite = new Sprite(app.planes_sheet.ak, center) is lazy +end + +redef class RocketLauncherPU + redef var sprite = new Sprite(app.planes_sheet.rocket, center) is lazy +end + +redef class Life + redef var sprite = new Sprite(app.planes_sheet.health, center) is lazy + init do sprite.scale = 3.0/sprite.texture.height +end + +redef class World + + redef fun explode(center, force) + do + super + + # Particles + app.explosions.add(center, 8192.0 * force, 0.3) + for i in (4.0*force).to_i.times do + app.explosions.add( + new Point3d[Float](center.x & force, center.y & force/2.0, center.z & force), + (4096.0 & 2048.0) * force, 0.3 & 0.3, 0.5.rand) + end + end +end + +redef class Int + # Pad a number with `0`s on the left side to reach `size` digits + private fun pad(size: Int): String + do + var s = to_s + var d = size - s.length + if d > 0 then s = "0"*d + s + return s + end +end + +# Special `Sprite` for the player character which is animated +class PlayerSprite + super Sprite + + # Animation of the running character + var running_animation: Array[Texture] + + # Seconds per frame of the animations + var time_per_frame: Float + + # Currently playing animation + private var current_animation: nullable Array[Texture] = null + + # Second at witch `current_animation` started + private var anim_ot = 0.0 + + # Start the running animation + fun start_running + do + anim_ot = app.world.t + current_animation = running_animation + end + + # Stop the running animation + fun stop_running do current_animation = null + + redef fun texture + do + var anim = current_animation + if anim != null then + var dt = app.world.t - anim_ot + var i = (dt / time_per_frame).to_i+2 + return anim.modulo(i) + end + + return super + end +end + +# Manager to display numbers in sprite +class CounterSprites + + # TODO clean up and move up to lib + + # Number textures, from 0 to 9 + # + # Require: `textures.length == 10` + var textures: Array[Texture] + + # Center of the first digit in UI coordinates + var anchor: Point3d[Float] + + # Last set of sprites generated to display the `value=` + private var sprites = new Array[Sprite] + + # Update the value displayed by the counter by inserting new sprites into `app.ui_sprites` + fun value=(value: Int) + do + # Clean up last used sprites + for s in sprites do if app.ui_sprites.has(s) then app.ui_sprites.remove s + sprites.clear + + # Build new sprites + var s = value.to_s # TODO manipulate ints directly + var x = 0.0 + for c in s do + var i = c.to_i + var tex = textures[i] + + x += tex.width/2.0 + sprites.add new Sprite(tex, new Point3d[Float](anchor.x + x, anchor.y, anchor.z)) + x += tex.width/2.0 + end + + # Register sprites to be drawn by `app.ui_camera` + app.ui_sprites.add_all sprites + end +end + +redef class SmokeProgram + + # Redef source to get particles that move up faster + redef fun vertex_shader_core do return """ + vec4 c = center; + c.y += dt * 20.0; + c.x += dt * dt * 2.0; + + gl_Position = c * mvp; + gl_PointSize = scale / gl_Position.z * (pt+0.1); + + if (pt < 0.1) + v_color.a = pt / 0.1; + else + v_color.a = 1.0 - pt*0.9; + """ +end