tinks, model_viewer & action_nitro: print model loading errors and fix one
[nit.git] / contrib / tinks / src / client / client3d.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 # 3D client for Tinks!
16 module client3d is
17 app_name "Tinks! 3D"
18 app_namespace "org.nitlanguage.tinks3d"
19 app_version(1, 0, git_revision)
20
21 android_api_target 10
22 android_manifest """<uses-permission android:name="android.permission.INTERNET" />"""
23 end
24
25 import gamnit::depth
26 import gamnit::keys
27 import app::audio
28
29 import base
30
31 redef class App
32
33 # ---
34 # Config
35
36 # Maximum distance from the camera to hear events and display explosions
37 private var far_dist2 = 2000.0
38
39 # Approximate maximum distance from the camera to display features
40 private var features_radius = 24
41
42 # ---
43 # Models
44
45 # Models of rocks
46 var models_rock = new Array[Model].with_items(
47 new Model("models/Tall_Rock_1_01.obj"),
48 new Model("models/Tall_Rock_2_01.obj"),
49 new Model("models/Tall_Rock_3_01.obj"),
50 new Model("models/Tall_Rock_4_01.obj"))
51
52 # Models of trees
53 var models_tree = new Array[Model].with_items(
54 new Model("models/Oak_Dark_01.obj"),
55 new Model("models/Oak_Green_01.obj"),
56 new Model("models/Large_Oak_Dark_01.obj"),
57 new Model("models/Large_Oak_Green_01.obj"))
58
59 # Models of the debris left by a destroyed tank
60 var models_debris = new Array[Model].with_items(
61 new Model("models/debris0.obj"),
62 new Model("models/debris1.obj"))
63
64 # Model the health pickup
65 var model_health = new Model("models/health.obj")
66
67 # Model of the tank base (without the turret)
68 var model_tank_base = new Model("models/tank.obj")
69
70 # Model of the tank turret
71 var model_tank_turret = new Model("models/tank-turret.obj")
72
73 # Blast effect on the ground after an explosion
74 private var blast_texture = new Texture("textures/blast.png")
75 private var blast_material: TexturedMaterial do
76 var mat = new TexturedMaterial([1.0]*4, [0.0]*4, [0.0]*4)
77 mat.ambient_texture = blast_texture
78 return mat
79 end
80 private var blast_model = new LeafModel(new Plane, blast_material)
81
82 # ---
83 # Particle effects
84
85 # Explosion image for particle effect
86 private var texture_explosion = new Texture("particles/explosion00.png")
87
88 # Explosion particles
89 var explosion_system = new ParticleSystem(20, explosion_program, texture_explosion)
90
91 # Explosion image for particle effect
92 private var texture_smoke = new Texture("particles/blackSmoke12.png")
93
94 # Explosion particles
95 var smoke_system = new ParticleSystem(200, smoke_program, texture_smoke)
96
97 # ---
98 # Sounds
99
100 # Firing sound
101 var turret_fire = new Sound("sounds/turret_fire.wav")
102
103 # Turret is ready to fire sound
104 var turret_ready = new Sound("sounds/turret_ready.mp3")
105
106 # ---
107 # Scene objects
108
109 # Ground
110 var ground: Actor is noinit
111 private var ground_texture = new Texture("textures/fastgras01.png")
112
113 # All `Feature` with an associated model, to be drawn on screen
114 var features_in_sight = new Set[Feature]
115
116 redef fun on_create
117 do
118 super
119
120 # Show splash screen
121 var logo = new Texture("textures/splash.png")
122 show_splash_screen logo
123
124 # Load everything
125 for texture in all_root_textures do
126 texture.load
127 var error = texture.error
128 if error != null then print_error error
129 end
130 for model in models do
131 model.load
132 if model.errors.not_empty then print_error model.errors.join("\n")
133 end
134
135 # Modify all textures so they have a higher ambient color
136 for model in models do
137 for leaf in model.leaves do
138 var mat = leaf.material
139 if mat isa TexturedMaterial then
140 var mod = 0.25
141 mat.ambient_color[0] = mat.diffuse_color[0] * mod
142 mat.ambient_color[1] = mat.diffuse_color[1] * mod
143 mat.ambient_color[2] = mat.diffuse_color[2] * mod
144
145 var tex = mat.diffuse_texture
146 if tex != null then mat.ambient_texture = tex
147 end
148 end
149 end
150
151 # Setup ground
152 # TODO we may need to move this plane if the player goes far from the center
153 var ground_mesh = new Plane
154 ground_mesh.repeat_x = 1000.0
155 ground_mesh.repeat_y = 1000.0
156
157 var ground_material = new TexturedMaterial(
158 [0.0, 0.1, 0.0, 1.0], [0.4, 0.4, 0.4, 1.0], [0.0]*4)
159 ground_material.diffuse_texture = ground_texture
160
161 var ground_model = new LeafModel(ground_mesh, ground_material)
162 var ground = new Actor(ground_model, new Point3d[Float](0.0, 0.0, 0.0))
163 ground.scale = 5000.0
164
165 self.ground = ground
166 actors.add ground
167
168 # Config the view
169 world_camera.near = 0.1
170
171 # Sky color
172 glClearColor(100.0/256.0, 120.0/256.0, 224.0/256.0, 1.0)
173
174 # Move the sun a bit off right above
175 light.position.x = 1000.0
176 light.position.z = 500.0
177
178 # Register our two systems
179 particle_systems.add explosion_system
180 particle_systems.add smoke_system
181
182 # Connect to server (or launch one) and assign models to rules
183 var context = context
184 context.game.story.assign_models
185 end
186
187 redef fun update(dt)
188 do
189 # Game logic
190 var turn = context.do_turn
191 for event in turn.events do event.client_react
192
193 # Is the player alive?
194 var local_player = context.local_player
195 var local_tank = null
196 if local_player != null then local_tank = local_player.tank
197
198 if local_tank != null then
199 # Update camera position above the tank
200 var pos = local_tank.pos
201 world_camera.position.x = pos.x
202 world_camera.position.z = pos.y
203 world_camera.yaw = 1.5 * pi - local_tank.heading
204 world_camera.position.y = 1.8
205 end
206 end
207
208 redef fun accept_event(event)
209 do
210 # Let `pressed_keys` be populated first
211 var s = super
212
213 var local_player = context.local_player
214 var local_tank = null
215 if local_player != null then local_tank = local_player.tank
216
217 # Quit game?
218 if event isa QuitEvent or (event isa KeyEvent and event.name == "escape") then
219 exit 0
220 end
221
222 if event isa KeyEvent then
223 # Move tank?
224 var direction_change = ["w", "a", "s", "d"].has(event.name)
225 if direction_change and local_tank != null and local_player != null then
226 var forward = pressed_keys.has("w")
227 var backward = pressed_keys.has("s")
228 var left = pressed_keys.has("a")
229 var right = pressed_keys.has("d")
230
231 # Cancel contradictory commands
232 if forward and backward then
233 forward = false
234 backward = false
235 end
236
237 if left and right then
238 left = false
239 right = false
240 end
241
242 # Set movement and direction
243 var move = 0.0
244 if forward then
245 move = 0.5
246 else if backward then move = -0.5
247
248 var ori = 0.0
249 if left then
250 ori = -local_tank.rule.max_direction/2.0
251 else if right then ori = local_tank.rule.max_direction/2.0
252
253 # Activate to invert the orientation on reverse, (for @R4p4Ss)
254 #if backward then ori = -ori
255
256 # Bonus when only moving or only turning
257 if not forward and not backward then ori *= 2.0
258 if not left and not right then move *= 2.0
259
260 # Give order
261 local_player.orders.add new TankDirectionOrder(local_tank, ori, move)
262 return true
263 end
264
265 # Open fire?
266 if event.name == "space" and local_tank != null and event.is_down then
267 if local_player == null then return false
268
269 # Open fire
270 var heading = local_tank.heading
271 var dist = 200.0
272 var target = new Pos(local_tank.pos.x + dist*heading.cos, local_tank.pos.y + dist*heading.sin)
273 local_player.orders.add new AimAndFireOrder(local_tank, target)
274 end
275 end
276
277 # Open fire with a target?
278 if event isa PointerEvent and event.pressed and not event.is_move then
279 if local_player == null then return false
280
281 var display = display
282 if display == null then return false
283
284 if local_tank == null then
285 local_player.orders.add new SpawnTankOrder(local_player)
286 return true
287 end
288
289 # Compute approximate target
290 var dx = event.x / display.width.to_f
291 dx = dx * 2.0 - 1.0 # center of the screen
292 var fovx = display.aspect_ratio * world_camera.field_of_view_y
293 fovx *= 0.8
294 var heading = local_tank.heading + dx * fovx
295
296 var dy = event.y / display.height.to_f
297 dy = dy - 0.5
298 if dy >= 0.0 then
299 var ty = dy * world_camera.field_of_view_y
300 var dist = world_camera.position.y / ty.tan / 1.6
301 if dist > 200.0 then dist = 200.0
302
303 # Issue fire order
304 var target = new Pos(local_tank.pos.x + dist*heading.cos, local_tank.pos.y + dist*heading.sin)
305 local_player.orders.add new AimAndFireOrder(local_tank, target)
306 end
307 end
308
309 return s
310 end
311 end
312
313 # ---
314 # Story and rules (meta game objects)
315
316 redef class FeatureRule
317 # Models of different alternatives
318 var models: Array[Model] is noinit
319 end
320
321 redef class TankRule
322 # Models of the tank base
323 var base_model: Model is noinit
324
325 # Models of the turret
326 var turret_model: Model is noinit
327 end
328
329 redef class Story
330
331 # Assign models from `app` to the corresponding rules
332 fun assign_models
333 do
334 tree.models = app.models_tree
335 rock.models = app.models_rock
336 debris.models = app.models_debris
337
338 for tank in tanks do
339 tank.base_model = app.model_tank_base
340 tank.turret_model = app.model_tank_turret
341 end
342
343 health.models = [app.model_health]
344 end
345 end
346
347 # ---
348 # Game entities
349
350 redef class Feature
351 # Actor representing this feature, if in sight
352 var actor: nullable Actor = null
353
354 # Instantiate `actor` and add it to the 3D world
355 fun add_actor_to_scene
356 do
357 app.features_in_sight.add self
358
359 var actor = actor
360 if actor != null then
361 # Reuse existing actor
362 if not app.actors.has(actor) then app.actors.add actor
363 return
364 end
365
366 # Apply a random model and rotation to new features
367 actor = new Actor(rule.models.rand,
368 new Point3d[Float](pos.x, 0.0, pos.y))
369 actor.yaw = 2.0*pi.rand
370 actor.scale = 0.75
371
372 self.actor = actor
373 app.actors.add actor
374 end
375
376 # Remove `actor` from the `actors` list as it will net be used anymore
377 fun destroy_actor
378 do
379 var actor = actor
380 if actor != null then
381 app.actors.remove actor
382 self.actor = null
383 end
384 end
385 end
386
387 redef class Tank
388 # Actors representing this tank, both the base and the turret
389 var actors: Array[Actor] is lazy do
390 var actors = new Array[Actor]
391 var actor = new Actor(app.model_tank_base, new Point3d[Float](0.0, 0.0, 0.0))
392 app.actors.add actor
393 actors.add actor
394
395 var tank_turret = new Actor(app.model_tank_turret, new Point3d[Float](0.0, 0.0, 0.0))
396 app.actors.add tank_turret
397 actors.add tank_turret
398 return actors
399 end
400 end
401
402 # ---
403 # Events
404
405 redef class TEvent
406 private fun client_react do end
407 end
408
409 redef class FeatureChangeEvent
410 redef fun client_react
411 do
412 var old_feature = old_feature
413 if old_feature != null then old_feature.destroy_actor
414
415 var feature = feature
416 if feature != null then feature.add_actor_to_scene
417 end
418 end
419
420 redef class ExplosionEvent
421 redef fun client_react
422 do
423 for feature in destroyed_features do feature.destroy_actor
424
425 # Particles
426 app.explosion_system.add(new Point3d[Float](pos.x, 1.0, pos.y), 4096.0, 0.3)
427 for i in 8.times do
428 app.explosion_system.add(
429 new Point3d[Float](pos.x & 1.0, 1.0 & 1.0, pos.y & 1.0),
430 2048.0 & 1024.0, 0.3 & 0.1)
431 end
432
433 # Blast mark on the ground
434 var blast = new Actor(app.blast_model, new Point3d[Float](pos.x, 0.05 & 0.04, pos.y))
435 blast.scale = 3.0
436 blast.yaw = 2.0*pi.rand
437 app.actors.add blast
438
439 # Smoke
440 for s in 32.times do
441 var dt = 0.2 * s.to_f + 0.1.rand
442 app.smoke_system.add(
443 new Point3d[Float](pos.x & 0.2, 0.0, pos.y & 0.2),
444 1024.0 & 512.0, 10.0 & 4.0, dt)
445 end
446 end
447 end
448
449 redef class OpenFireEvent
450 redef fun client_react
451 do
452 if tank.pos.dist2_3d(app.world_camera.position) < app.far_dist2 then
453 # Within earshot
454 app.turret_fire.play
455
456 # Particle
457 var d = 1.7
458 var a = tank.turret.heading - 0.025 # Correct to center the art
459 var pos = new Point3d[Float](tank.pos.x + d*a.cos, 1.25, tank.pos.y + d*a.sin)
460 app.explosion_system.add(pos, 0.75*256.0, 0.15)
461 end
462 end
463 end
464
465 redef class TurretReadyEvent
466 redef fun client_react
467 do
468 if tank.pos.dist2_3d(app.world_camera.position) < app.far_dist2 then
469 # Within earshot
470 app.turret_ready.play
471 end
472 end
473 end
474
475 redef class TankMoveEvent
476 redef fun client_react
477 do
478 var pos = tank.pos
479 for actor in tank.actors do
480 actor.center.x = pos.x
481 actor.center.z = pos.y
482 end
483
484 tank.actors[0].yaw = -tank.heading + pi
485 tank.actors[1].yaw = -tank.turret.heading + pi
486
487 # Keep going only for the local tank
488 var local_player = app.context.local_player
489 if local_player != tank.player then return
490
491 var center = tank.pos
492 var d = app.features_radius
493 var l = center.x.to_i - d
494 var r = center.x.to_i + d
495 var t = center.y.to_i - d
496 var b = center.y.to_i + d
497
498 # Remove out of range features
499 for feature in app.features_in_sight.to_a do
500 var x = feature.pos.x.to_i
501 var y = feature.pos.y.to_i
502 if x < l or x > r or y < t or y > b then
503 var actor = feature.actor
504 app.actors.remove actor
505 app.features_in_sight.remove feature
506 end
507 end
508
509 # Add newly in range features
510 for x in [l..r[ do
511 for y in [t..b[ do
512 var feature = app.context.game.world[x, y]
513 if feature != null then
514 feature.add_actor_to_scene
515 end
516 end
517 end
518 end
519 end
520
521 redef class TankDeathEvent
522 redef fun client_react
523 do
524 for actor in tank.actors do app.actors.remove actor
525 tank.actors.clear
526 end
527 end
528
529 # ---
530 # Misc services
531
532 redef class Point[N]
533 # Square of the distance to 3D coordinates `other`
534 #
535 # Same as `dist2` but using `other.z` as the Y value
536 # to adapt from the flat plane on X/Y to X/Z.
537 private fun dist2_3d(other: Point3d[Numeric]): N
538 do
539 var dx = other.x.sub(x)
540 var dy = other.z.sub(y)
541 var s = (dx.mul(dx)).add(dy.mul(dy))
542 return x.value_of(s)
543 end
544 end
545
546 redef class Float
547 # Fuzzy value in `[self-variation..self+variation]`
548 fun &(variation: Float): Float do return self - variation + 2.0*variation.rand
549 end