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