e4366e62351e6c9f6d21962c19ed9ca022a45fbf
[nit.git] / contrib / action_nitro / src / action_nitro.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 module action_nitro
16
17 import gamnit::depth
18 import gamnit::keys
19 import gamnit::limit_fps
20
21 import game
22
23 import gen::texts
24 import gen::planes
25
26 redef class App
27
28 # Game world
29 var world: World = new World is lazy
30
31 # ---
32 # Game world assets
33
34 # Textures of the biplane, jet, helicopter, parachute and powerups
35 private var planes_sheet = new PlanesImages
36
37 # Animation for the player movement
38 private var player_textures: Array[Texture] =
39 [for f in [1..12] do new Texture("textures/player/frame_{f.pad(2)}.png")]
40
41 # Boss 3D model
42 private var iss_model = new Model("models/iss.obj")
43
44 # ---
45 # Ground textures
46
47 private var ground_texture = new Texture("textures/fastgras01.jpg")
48 private var tree_texture = new Texture("textures/Tree03.png")
49
50 # ---
51 # Blood splatter
52
53 private var splatter_texture = new Texture("textures/blood_splatter.png")
54 private var splatter_material: TexturedMaterial do
55 var mat = new TexturedMaterial([1.0]*4, [0.0]*4, [0.0]*4)
56 mat.ambient_texture = splatter_texture
57 return mat
58 end
59 private var splatter_model = new LeafModel(new Plane, splatter_material)
60
61 # ---
62 # Background
63
64 private var city_texture = new TextureAsset("textures/city_background_clean.png")
65
66 private var stars_texture = new Texture("textures/stars.jpg")
67 private var stars = new Sprite(stars_texture, new Point3d[Float](0.0, 1100.0, -600.0)) is lazy
68
69 # ---
70 # Particle effects
71
72 # Explosion particles
73 var explosions = new ParticleSystem(20, explosion_program,
74 new Texture("particles/explosion00.png"))
75
76 # Blood explosion particles
77 var blood = new ParticleSystem(20, explosion_program,
78 new Texture("particles/blood07.png"))
79
80 # Smoke for the background
81 var smoke = new ParticleSystem(500, smoke_program,
82 new Texture("particles/blackSmoke12.png"))
83
84 # Static clouds particles
85 var clouds = new ParticleSystem(1600, static_program,
86 new Texture("particles/whitePuff12.png"))
87
88 # ---
89 # Sound effects
90
91 # TODO
92 #private var fx_fire = new Sound("sounds/fire.mp3")
93
94 # ---
95 # UI
96 private var texts_sheet = new TextsImages
97
98 private var tutorial_wasd = new Sprite(app.texts_sheet.tutorial_wasd,
99 app.ui_camera.center.offset(0.0, -250.0, 0.0)) is lazy
100
101 private var tutorial_arrows = new Sprite(app.texts_sheet.tutorial_arrows,
102 app.ui_camera.center.offset(0.0, -350.0, 0.0)) is lazy
103
104 private var tutorial_chute = new Sprite(app.texts_sheet.tutorial_chute,
105 app.ui_camera.center.offset(0.0, -450.0, 0.0)) is lazy
106
107 private var tutorial_goal = new Sprite(app.texts_sheet.goal,
108 app.ui_camera.center.offset(0.0, 0.0, 0.0)) is lazy
109
110 private var outro_directed = new Sprite(app.texts_sheet.directed,
111 app.ui_camera.center.offset(0.0, 400.0, 0.0)) is lazy
112
113 private var outro_created = new Sprite(app.texts_sheet.created,
114 app.ui_camera.center.offset(0.0, -200.0, 0.0)) is lazy
115
116 # ---
117 # Counters for the UI
118
119 private var score_counter = new CounterSprites(texts_sheet.n,
120 new Point3d[Float](32.0, -64.0, 0.0))
121
122 private var altitude_counter = new CounterSprites(texts_sheet.n,
123 new Point3d[Float](1400.0, -64.0, 0.0))
124
125 # Did the player asked to skip the intro animation?
126 private var skip_intro = false
127
128 redef fun on_create
129 do
130 super
131
132 show_splash_screen new Texture("textures/splash.jpg")
133
134 # Load 3d models
135 iss_model.load
136
137 # Setup cameras
138 world_camera.reset_height 60.0
139 ui_camera.reset_height 1080.0
140
141 # Register particle systems
142 particle_systems.add explosions
143 particle_systems.add blood
144 particle_systems.add smoke
145 particle_systems.add clouds
146
147 # Stars background
148 sprites.add stars
149 stars.scale = 2.1
150
151 # City background
152 city_texture.pixelated = true
153 var city_sprite = new Sprite(city_texture, new Point3d[Float](0.0, 370.0, -600.0))
154 city_sprite.scale = 0.8
155 sprites.add city_sprite
156
157 # Setup ground
158 var ground_mesh = new Plane
159 ground_mesh.repeat_x = 100.0
160 ground_mesh.repeat_y = 100.0
161
162 var ground_material = new TexturedMaterial(
163 [0.0, 0.1, 0.0, 1.0], [0.4, 0.4, 0.4, 1.0], [0.0]*4)
164 ground_material.diffuse_texture = ground_texture
165
166 var ground_model = new LeafModel(ground_mesh, ground_material)
167 var ground = new Actor(ground_model, new Point3d[Float](0.0, 0.0, 0.0))
168 ground.scale = 5000.0
169 actors.add ground
170
171 # Trees
172 for i in 2000.times do
173 var s = 0.1 + 0.1.rand
174 var h = tree_texture.height * s
175 var sprite = new Sprite(tree_texture,
176 new Point3d[Float](0.0 & 1500.0, h/2.0 - 10.0*s, 10.0 - 609.0.rand))
177 sprite.static = true
178 sprite.scale = s
179 sprites.add sprite
180
181 var c = 1.0.rand
182 c *= 0.7
183 sprite.tint = [c, 1.0, c, 1.0]
184 end
185
186 # Clouds
187 var no_clouds_layer = 200.0
188 for i in [0 .. 32[ do
189 var zp = 1.0.rand
190 var x = 0.0 & 1000.0 * zp
191 var y = no_clouds_layer + (world.boss_altitude - no_clouds_layer*2.0).rand
192 var z = -500.0*zp - 10.0
193
194 var r = 50.0 & 1.0
195 for j in [0..32[ do
196 var a = 2.0*pi.rand
197 var rj = r.rand
198 clouds.add(new Point3d[Float](x+2.0*a.cos*rj, y+a.sin*rj, z & 1.0),
199 48000.0 & 16000.0, inf)
200 end
201 end
202
203 # Move the sun to best light the ISS
204 light.position.x = 2000.0
205 light.position.z = 4000.0
206
207 # Prepare for intro animation
208 ui_sprites.add tutorial_goal
209 world_camera.far = 1024.0
210 end
211
212 redef fun update(dt)
213 do
214 # Game logic
215 world.update dt
216
217 # Update background color
218 var player = world.player
219 var player_pos = if player != null then player.center else new Point3d[Float](0.0, 200.0, 0.0)
220 var altitude = player_pos.y
221 var p = altitude / world.boss_altitude
222 var ip = 1.0 - p
223 glClearColor(0.3*ip, 0.3*ip, ip, 1.0)
224 stars.alpha = (1.4*p-0.4).clamp(0.0, 1.0)
225
226 # Randomly add smoke
227 var poss = [
228 new Point3d[Float](291.0, 338.0, -601.0),
229 new Point3d[Float](-356.0, 422.0, -601.0)]
230
231 var r = 8.0
232 if 2.rand == 0 then
233 var pos = poss.rand
234 smoke.add(
235 new Point3d[Float](pos.x & r, pos.y & r, pos.z & r),
236 96000.0 & 16000.0, 10.0)
237 end
238
239 # Move camera
240 world_camera.position.x = player_pos.x
241 world_camera.position.y = player_pos.y + 5.0
242
243 # Cinematic?
244 var t = world.t
245 var intro_duration = 8.0
246 if t < intro_duration and not skip_intro then
247 var pitch = t/intro_duration
248 pitch = (pitch*pi).sin
249 world_camera.pitch = pitch
250 return
251 end
252
253 if world.player == null then
254 world_camera.pitch = 0.0
255 world_camera.far = 700.0
256
257 begin_play true
258 end
259
260 # Update counters
261 score_counter.value = world.score
262 var alt = 0
263 if world.player != null then alt = world.player.altitude.to_i
264 altitude_counter.value = alt
265
266 # General movement on the X axis
267 if player != null then
268 player.moving = 0.0
269 if pressed_keys.has("left") then player.moving -= 1.0
270 if pressed_keys.has("right") then player.moving += 1.0
271 player.sprite.as(PlayerSprite).update
272 end
273
274 # Try to fire as long as a key is pressed
275 if pressed_keys.not_empty then
276 var a = inf
277 if pressed_keys.has("a") then
278 if pressed_keys.has("w") then
279 a = 0.75 * pi
280 else if pressed_keys.has("s") then
281 a = 1.25 * pi
282 else
283 a = pi
284 end
285 else if pressed_keys.has("d") then
286 if pressed_keys.has("w") then
287 a = 0.25 * pi
288 else if pressed_keys.has("s") then
289 a = 1.75 * pi
290 else
291 a = 0.0
292 end
293 else if pressed_keys.has("w") then
294 a = 0.50 * pi
295 else if pressed_keys.has("s") then
296 a = 1.50 * pi
297 end
298
299 if a != inf and player != null then
300 player.shoot(a, world)
301 hide_tutorial_wasd
302 end
303 end
304
305 # Low-gravity controls
306 if player != null and player.is_alive and player.altitude >= world.boss_altitude then
307 var d = 50.0*dt
308 for key in pressed_keys do
309 if key == "up" then
310 player.inertia.y += d
311 else if key == "down" then
312 player.inertia.y -= d
313 else if key == "left" then
314 player.inertia.x -= d
315 else if key == "right" then
316 player.inertia.x += d
317 end
318 end
319 end
320
321 # Detect if game won
322 var won_at = won_at
323 if won_at == null then
324 var boss = world.boss
325 if boss != null and not boss.is_alive then
326 self.won_at = world.t
327 end
328 else
329 # Show outro
330 var t_since_won = world.t - won_at
331 if t_since_won > 1.0 and not ui_sprites.has(outro_directed) then ui_sprites.add outro_directed
332 if t_since_won > 2.0 and not ui_sprites.has(outro_created) then ui_sprites.add outro_created
333 end
334 end
335
336 # Begin playing, after intro if `initial`, otherwise after death
337 fun begin_play(initial: Bool)
338 do
339 ui_sprites.clear
340
341 world.spawn_player
342 world.planes.add new Airplane(new Point3d[Float](0.0, world.player.center.y - 10.0, 0.0), 16.0, 4.0)
343
344 if initial then
345 # Setup tutorial
346 ui_sprites.add_all([tutorial_wasd, tutorial_arrows, tutorial_chute])
347 end
348 end
349
350 # Seconds at which the game was won, using `world.t` as reference
351 private var won_at: nullable Float = null
352
353 # Remove the tutorial sprite about WASD from `ui_sprites`
354 private fun hide_tutorial_wasd do if ui_sprites.has(tutorial_wasd) then ui_sprites.remove(tutorial_wasd)
355
356 # Remove the tutorial sprite about arrows from `ui_sprites`
357 private fun hide_tutorial_arrows do if ui_sprites.has(tutorial_arrows) then ui_sprites.remove(tutorial_arrows)
358
359 # Remove the tutorial sprite about the parachute from `ui_sprites`
360 private fun hide_tutorial_chute do if ui_sprites.has(tutorial_chute) then ui_sprites.remove(tutorial_chute)
361
362 redef fun accept_event(event)
363 do
364 if super then return true
365
366 if event isa QuitEvent then
367 print perfs
368 exit 0
369 else if event isa KeyEvent then
370 if event.name == "escape" and event.is_down then
371 print perfs
372 exit 0
373 end
374
375 var player = world.player
376 if player != null and player.is_alive then
377
378 # Hide tutorial about arrows once they are used
379 var arrows = once ["left", "right"]
380 if arrows.has(event.name) then hide_tutorial_arrows
381
382 if player.altitude < world.boss_altitude then
383 if event.name == "space" and event.is_down and not player.parachute_deployed and player.plane == null then
384 player.parachute
385 if player.parachute_deployed then
386 var pc = player.center
387 world.parachute = new Parachute(new Point3d[Float](pc.x, pc.y + 5.0, pc.z-0.1), 8.0, 5.0)
388 end
389 hide_tutorial_chute
390 end
391
392 if (event.name == "space" or event.name == "up") and event.is_down then
393 player.jump
394 end
395
396 if event.name == "left" then
397 var mod = if event.is_down then -1.0 else 1.0
398 player.moving += mod
399 end
400
401 if event.name == "right" then
402 var mod = if event.is_down then 1.0 else -1.0
403 player.moving += mod
404 end
405
406 if player.moving == 0.0 then
407 player.sprite.as(PlayerSprite).stop_running
408 else player.sprite.as(PlayerSprite).start_running
409 end
410 end
411 end
412
413 # When player is dead, respawn on spacebar or pointer depressed
414 if (event isa KeyEvent and event.name == "space") or
415 (event isa PointerEvent and not event.is_move and event.depressed) then
416 var player = world.player
417 if player == null then
418 skip_intro = true
419 else if not player.is_alive then
420 begin_play false
421 end
422 end
423
424 return false
425 end
426 end
427
428 redef class Body
429 # Sprite representing this entity if there is no `actor`
430 fun sprite: Sprite is abstract
431
432 # 3D actor
433 fun actor: nullable Actor do return null
434
435 init
436 do
437 var actor = actor
438 if actor != null then
439 app.actors.add actor
440 else app.sprites.add sprite
441 end
442
443 redef fun destroy(world)
444 do
445 super
446
447 var actor = actor
448 if actor != null then
449 app.actors.remove actor
450 else app.sprites.remove sprite
451 end
452 end
453
454 redef class Human
455 redef fun die(world)
456 do
457 super
458
459 death_animation
460 end
461
462 # Show death animation (explosion)
463 fun death_animation
464 do
465 var force = 4.0
466 health = 0.0
467 for i in 32.times do
468 app.blood.add(
469 new Point3d[Float](center.x & force, center.y & force, center.z & force),
470 (2048.0 & 4096.0) * force, 0.3 & 0.1)
471 end
472 end
473 end
474
475 redef class Platform
476 init do sprite.scale = width/sprite.texture.width
477
478 redef fun update(dt, world)
479 do
480 super
481
482 if inertia.x < 0.0 then
483 sprite.invert_x = false
484 else if inertia.x > 0.0 then
485 sprite.invert_x = true
486 end
487 end
488 end
489
490 redef class Airplane
491 private fun texture: Texture do return if center.y < 600.0 then app.planes_sheet.biplane else app.planes_sheet.jet
492
493 redef var sprite = new Sprite(texture, center) is lazy
494 end
495
496 redef class Helicopter
497 redef var sprite = new Sprite(app.planes_sheet.helicopter, center) is lazy
498 end
499
500 redef class Boss
501 redef var actor is lazy do
502 var actor = new Actor(app.iss_model, center)
503 actor.rotation = pi/2.0
504 return actor
505 end
506
507 redef fun death_animation
508 do
509 var force = 64.0
510 app.explosions.add(center, 4096.0 * force, 0.3)
511 for i in (8.0*force).to_i.times do
512 app.explosions.add(
513 new Point3d[Float](center.x & force, center.y & force/8.0, center.z & force),
514 (2048.0 & 1024.0) * force, 0.3 + 5.0.rand, 5.0.rand)
515 end
516 end
517 end
518
519 redef class Enemy
520 redef var sprite = new Sprite(app.player_textures.rand, center) is lazy
521 init do sprite.scale = width/sprite.texture.width * 2.0
522 end
523
524 redef class Parachute
525 redef var sprite = new Sprite(app.planes_sheet.parachute, center) is lazy
526 init do sprite.scale = width / sprite.texture.width
527 end
528
529 redef class Player
530 redef var sprite = new PlayerSprite(app.player_textures[1], center, app.player_textures, 0.08) is lazy
531 init do sprite.scale = width/sprite.texture.width * 2.0
532
533 redef fun update(dt, world)
534 do
535 super
536 if moving > 0.0 then
537 sprite.invert_x = false
538 else if moving < 0.0 then
539 sprite.invert_x = true
540 end
541 end
542
543 redef fun die(world)
544 do
545 super
546
547 if center.y < 10.0 then
548 # Blood splatter on the ground
549 var splatter = new Actor(app.splatter_model,
550 new Point3d[Float](center.x, 0.05 & 0.04, center.y))
551 splatter.scale = 32.0
552 splatter.rotation = 2.0 * pi.rand
553 app.actors.add splatter
554 end
555
556 # Display respawn instructions
557 app.ui_sprites.add new Sprite(app.texts_sheet.respawn, app.ui_camera.center)
558 end
559 end
560
561 redef class Bullet
562 redef var sprite = new Sprite(weapon.bullet_texture, center) is lazy
563 init
564 do
565 sprite.scale = 0.03
566 sprite.rotation = angle
567 end
568 end
569
570 redef class Weapon
571 fun bullet_texture: Texture do return app.planes_sheet.bullet_ak
572 end
573
574 redef class Pistol
575 redef fun bullet_texture do return app.planes_sheet.bullet_pistol
576 end
577
578 redef class RocketLauncher
579 redef fun bullet_texture do return app.planes_sheet.bullet_rocket
580 end
581
582 redef class Powerup
583 # Scale so it looks like 5 world units wide, not matter the size of the texture
584 init do sprite.scale = 5.0/sprite.texture.width
585 end
586
587 redef class Ak47PU
588 redef var sprite = new Sprite(app.planes_sheet.ak, center) is lazy
589 end
590
591 redef class RocketLauncherPU
592 redef var sprite = new Sprite(app.planes_sheet.rocket, center) is lazy
593 end
594
595 redef class Life
596 redef var sprite = new Sprite(app.planes_sheet.health, center) is lazy
597 init do sprite.scale = 3.0/sprite.texture.height
598 end
599
600 redef class World
601
602 redef fun explode(center, force)
603 do
604 super
605
606 # Particles
607 app.explosions.add(center, 8192.0 * force, 0.3)
608 for i in (4.0*force).to_i.times do
609 app.explosions.add(
610 new Point3d[Float](center.x & force, center.y & force/2.0, center.z & force),
611 (4096.0 & 2048.0) * force, 0.3 & 0.3, 0.5.rand)
612 end
613 end
614 end
615
616 redef class Int
617 # Pad a number with `0`s on the left side to reach `size` digits
618 private fun pad(size: Int): String
619 do
620 var s = to_s
621 var d = size - s.length
622 if d > 0 then s = "0"*d + s
623 return s
624 end
625 end
626
627 # Special `Sprite` for the player character which is animated
628 class PlayerSprite
629 super Sprite
630
631 # Animation of the running character
632 var running_animation: Array[Texture]
633
634 # Seconds per frame of the animations
635 var time_per_frame: Float
636
637 # Currently playing animation
638 private var current_animation: nullable Array[Texture] = null
639
640 # Second at witch `current_animation` started
641 private var anim_ot = 0.0
642
643 # Start the running animation
644 fun start_running
645 do
646 anim_ot = app.world.t
647 current_animation = running_animation
648 end
649
650 # Stop the running animation
651 fun stop_running do current_animation = null
652
653 # Update `texture` from `current_animation`
654 fun update
655 do
656 var anim = current_animation
657 if anim != null then
658 var dt = app.world.t - anim_ot
659 var i = (dt / time_per_frame).to_i+2
660 texture = anim.modulo(i)
661 end
662 end
663 end
664
665 # Manager to display numbers in sprite
666 class CounterSprites
667
668 # TODO clean up and move up to lib
669
670 # Number textures, from 0 to 9
671 #
672 # Require: `textures.length == 10`
673 var textures: Array[Texture]
674
675 # Center of the first digit in UI coordinates
676 var anchor: Point3d[Float]
677
678 # Last set of sprites generated to display the `value=`
679 private var sprites = new Array[Sprite]
680
681 # Update the value displayed by the counter by inserting new sprites into `app.ui_sprites`
682 fun value=(value: Int)
683 do
684 # Clean up last used sprites
685 for s in sprites do if app.ui_sprites.has(s) then app.ui_sprites.remove s
686 sprites.clear
687
688 # Build new sprites
689 var s = value.to_s # TODO manipulate ints directly
690 var x = 0.0
691 for c in s do
692 var i = c.to_i
693 var tex = textures[i]
694
695 x += tex.width/2.0
696 sprites.add new Sprite(tex, new Point3d[Float](anchor.x + x, anchor.y, anchor.z))
697 x += tex.width/2.0
698 end
699
700 # Register sprites to be drawn by `app.ui_camera`
701 app.ui_sprites.add_all sprites
702 end
703 end
704
705 redef class SmokeProgram
706
707 # Redef source to get particles that move up faster
708 redef fun vertex_shader_core do return """
709 vec4 c = center;
710 c.y += dt * 20.0;
711 c.x += dt * dt * 2.0;
712
713 gl_Position = c * mvp;
714 gl_PointSize = scale / gl_Position.z * (pt+0.1);
715
716 if (pt < 0.1)
717 v_color.a = pt / 0.1;
718 else
719 v_color.a = 1.0 - pt*0.9;
720 """
721 end