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