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