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