f408ec806ffe0d3bf03e4ed820b96ff9468fd516
[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 15
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 model in models do model.load
126 for texture in all_root_textures do texture.load
127
128 # Modify all textures so they have a higher ambient color
129 for model in models do
130 for leaf in model.leaves do
131 var mat = leaf.material
132 if mat isa TexturedMaterial then
133 var mod = 0.25
134 mat.ambient_color[0] = mat.diffuse_color[0] * mod
135 mat.ambient_color[1] = mat.diffuse_color[1] * mod
136 mat.ambient_color[2] = mat.diffuse_color[2] * mod
137
138 var tex = mat.diffuse_texture
139 if tex != null then mat.ambient_texture = tex
140 end
141 end
142 end
143
144 # Setup ground
145 # TODO we may need to move this plane if the player goes far from the center
146 var ground_mesh = new Plane
147 ground_mesh.repeat_x = 1000.0
148 ground_mesh.repeat_y = 1000.0
149
150 var ground_material = new TexturedMaterial(
151 [0.0, 0.1, 0.0, 1.0], [0.4, 0.4, 0.4, 1.0], [0.0]*4)
152 ground_material.diffuse_texture = ground_texture
153
154 var ground_model = new LeafModel(ground_mesh, ground_material)
155 var ground = new Actor(ground_model, new Point3d[Float](0.0, 0.0, 0.0))
156 ground.scale = 5000.0
157
158 self.ground = ground
159 actors.add ground
160
161 # Config the view
162 world_camera.near = 0.1
163
164 # Sky color
165 glClearColor(100.0/256.0, 120.0/256.0, 224.0/256.0, 1.0)
166
167 # Move the sun a bit off right above
168 light.position.x = 1000.0
169 light.position.z = 500.0
170
171 # Register our two systems
172 particle_systems.add explosion_system
173 particle_systems.add smoke_system
174
175 # Connect to server (or launch one) and assign models to rules
176 var context = context
177 context.game.story.assign_models
178 end
179
180 redef fun update(dt)
181 do
182 # Game logic
183 var turn = context.do_turn
184 for event in turn.events do event.client_react
185
186 # Is the player alive?
187 var local_player = context.local_player
188 var local_tank = null
189 if local_player != null then local_tank = local_player.tank
190
191 if local_tank != null then
192 # Update camera position above the tank
193 var pos = local_tank.pos
194 world_camera.position.x = pos.x
195 world_camera.position.z = pos.y
196 world_camera.yaw = 1.5 * pi - local_tank.heading
197 world_camera.position.y = 1.8
198 end
199 end
200
201 redef fun accept_event(event)
202 do
203 # Let `pressed_keys` be populated first
204 var s = super
205
206 var local_player = context.local_player
207 var local_tank = null
208 if local_player != null then local_tank = local_player.tank
209
210 # Quit game?
211 if event isa QuitEvent or (event isa KeyEvent and event.name == "escape") then
212 exit 0
213 end
214
215 if event isa KeyEvent then
216 # Move tank?
217 var direction_change = ["w", "a", "s", "d"].has(event.name)
218 if direction_change and local_tank != null and local_player != null then
219 var forward = pressed_keys.has("w")
220 var backward = pressed_keys.has("s")
221 var left = pressed_keys.has("a")
222 var right = pressed_keys.has("d")
223
224 # Cancel contradictory commands
225 if forward and backward then
226 forward = false
227 backward = false
228 end
229
230 if left and right then
231 left = false
232 right = false
233 end
234
235 # Set movement and direction
236 var move = 0.0
237 if forward then
238 move = 0.5
239 else if backward then move = -0.5
240
241 var ori = 0.0
242 if left then
243 ori = -local_tank.rule.max_direction/2.0
244 else if right then ori = local_tank.rule.max_direction/2.0
245
246 # Activate to invert the orientation on reverse, (for @R4p4Ss)
247 #if backward then ori = -ori
248
249 # Bonus when only moving or only turning
250 if not forward and not backward then ori *= 2.0
251 if not left and not right then move *= 2.0
252
253 # Give order
254 local_player.orders.add new TankDirectionOrder(local_tank, ori, move)
255 return true
256 end
257
258 # Open fire?
259 if event.name == "space" and local_tank != null and event.is_down then
260 if local_player == null then return false
261
262 # Open fire
263 var heading = local_tank.heading
264 var dist = 200.0
265 var target = new Pos(local_tank.pos.x + dist*heading.cos, local_tank.pos.y + dist*heading.sin)
266 local_player.orders.add new AimAndFireOrder(local_tank, target)
267 end
268 end
269
270 # Open fire with a target?
271 if event isa PointerEvent and event.pressed and not event.is_move then
272 if local_player == null then return false
273
274 var display = display
275 if display == null then return false
276
277 if local_tank == null then
278 local_player.orders.add new SpawnTankOrder(local_player)
279 return true
280 end
281
282 # Compute approximate target
283 var dx = event.x / display.width.to_f
284 dx = dx * 2.0 - 1.0 # center of the screen
285 var fovx = display.aspect_ratio * world_camera.field_of_view_y
286 fovx *= 0.8
287 var heading = local_tank.heading + dx * fovx
288
289 var dy = event.y / display.height.to_f
290 dy = dy - 0.5
291 if dy >= 0.0 then
292 var ty = dy * world_camera.field_of_view_y
293 var dist = world_camera.position.y / ty.tan / 1.6
294 if dist > 200.0 then dist = 200.0
295
296 # Issue fire order
297 var target = new Pos(local_tank.pos.x + dist*heading.cos, local_tank.pos.y + dist*heading.sin)
298 local_player.orders.add new AimAndFireOrder(local_tank, target)
299 end
300 end
301
302 return s
303 end
304 end
305
306 # ---
307 # Story and rules (meta game objects)
308
309 redef class FeatureRule
310 # Models of different alternatives
311 var models: Array[Model] is noinit
312 end
313
314 redef class TankRule
315 # Models of the tank base
316 var base_model: Model is noinit
317
318 # Models of the turret
319 var turret_model: Model is noinit
320 end
321
322 redef class Story
323
324 # Assign models from `app` to the corresponding rules
325 fun assign_models
326 do
327 tree.models = app.models_tree
328 rock.models = app.models_rock
329 debris.models = app.models_debris
330
331 for tank in tanks do
332 tank.base_model = app.model_tank_base
333 tank.turret_model = app.model_tank_turret
334 end
335
336 health.models = [app.model_health]
337 end
338 end
339
340 # ---
341 # Game entities
342
343 redef class Feature
344 # Actor representing this feature, if in sight
345 var actor: nullable Actor = null
346
347 # Instantiate `actor` and add it to the 3D world
348 fun add_actor_to_scene
349 do
350 app.features_in_sight.add self
351
352 var actor = actor
353 if actor != null then
354 # Reuse existing actor
355 if not app.actors.has(actor) then app.actors.add actor
356 return
357 end
358
359 # Apply a random model and rotation to new features
360 actor = new Actor(rule.models.rand,
361 new Point3d[Float](pos.x, 0.0, pos.y))
362 actor.rotation = 2.0*pi.rand
363 actor.scale = 0.75
364
365 self.actor = actor
366 app.actors.add actor
367 end
368
369 # Remove `actor` from the `actors` list as it will net be used anymore
370 fun destroy_actor
371 do
372 var actor = actor
373 if actor != null then
374 app.actors.remove actor
375 self.actor = null
376 end
377 end
378 end
379
380 redef class Tank
381 # Actors representing this tank, both the base and the turret
382 var actors: Array[Actor] is lazy do
383 var actors = new Array[Actor]
384 var actor = new Actor(app.model_tank_base, new Point3d[Float](0.0, 0.0, 0.0))
385 app.actors.add actor
386 actors.add actor
387
388 var tank_turret = new Actor(app.model_tank_turret, new Point3d[Float](0.0, 0.0, 0.0))
389 app.actors.add tank_turret
390 actors.add tank_turret
391 return actors
392 end
393 end
394
395 # ---
396 # Events
397
398 redef class TEvent
399 private fun client_react do end
400 end
401
402 redef class FeatureChangeEvent
403 redef fun client_react
404 do
405 var old_feature = old_feature
406 if old_feature != null then old_feature.destroy_actor
407
408 var feature = feature
409 if feature != null then feature.add_actor_to_scene
410 end
411 end
412
413 redef class ExplosionEvent
414 redef fun client_react
415 do
416 for feature in destroyed_features do feature.destroy_actor
417
418 # Particles
419 app.explosion_system.add(new Point3d[Float](pos.x, 1.0, pos.y), 4096.0, 0.3)
420 for i in 8.times do
421 app.explosion_system.add(
422 new Point3d[Float](pos.x & 1.0, 1.0 & 1.0, pos.y & 1.0),
423 2048.0 & 1024.0, 0.3 & 0.1)
424 end
425
426 # Blast mark on the ground
427 var blast = new Actor(app.blast_model, new Point3d[Float](pos.x, 0.05 & 0.04, pos.y))
428 blast.scale = 3.0
429 blast.rotation = 2.0*pi.rand
430 app.actors.add blast
431
432 # Smoke
433 for s in 32.times do
434 var dt = 0.2 * s.to_f + 0.1.rand
435 app.smoke_system.add(
436 new Point3d[Float](pos.x & 0.2, 0.0, pos.y & 0.2),
437 1024.0 & 512.0, 10.0 & 4.0, dt)
438 end
439 end
440 end
441
442 redef class OpenFireEvent
443 redef fun client_react
444 do
445 if tank.pos.dist2_3d(app.world_camera.position) < app.far_dist2 then
446 # Within earshot
447 app.turret_fire.play
448
449 # Particle
450 var d = 1.7
451 var a = tank.turret.heading - 0.025 # Correct to center the art
452 var pos = new Point3d[Float](tank.pos.x + d*a.cos, 1.25, tank.pos.y + d*a.sin)
453 app.explosion_system.add(pos, 0.75*256.0, 0.15)
454 end
455 end
456 end
457
458 redef class TurretReadyEvent
459 redef fun client_react
460 do
461 if tank.pos.dist2_3d(app.world_camera.position) < app.far_dist2 then
462 # Within earshot
463 app.turret_ready.play
464 end
465 end
466 end
467
468 redef class TankMoveEvent
469 redef fun client_react
470 do
471 var pos = tank.pos
472 for actor in tank.actors do
473 actor.center.x = pos.x
474 actor.center.z = pos.y
475 end
476
477 tank.actors[0].rotation = tank.heading + pi
478 tank.actors[1].rotation = tank.turret.heading + pi
479
480 # Keep going only for the local tank
481 var local_player = app.context.local_player
482 if local_player != tank.player then return
483
484 var center = tank.pos
485 var d = app.features_radius
486 var l = center.x.to_i - d
487 var r = center.x.to_i + d
488 var t = center.y.to_i - d
489 var b = center.y.to_i + d
490
491 # Remove out of range features
492 for feature in app.features_in_sight.to_a do
493 var x = feature.pos.x.to_i
494 var y = feature.pos.y.to_i
495 if x < l or x > r or y < t or y > b then
496 var actor = feature.actor
497 app.actors.remove actor
498 app.features_in_sight.remove feature
499 end
500 end
501
502 # Add newly in range features
503 for x in [l..r[ do
504 for y in [t..b[ do
505 var feature = app.context.game.world[x, y]
506 if feature != null then
507 feature.add_actor_to_scene
508 end
509 end
510 end
511 end
512 end
513
514 redef class TankDeathEvent
515 redef fun client_react
516 do
517 for actor in tank.actors do app.actors.remove actor
518 tank.actors.clear
519 end
520 end
521
522 # ---
523 # Misc services
524
525 redef class Point[N]
526 # Square of the distance to 3D coordinates `other`
527 #
528 # Same as `dist2` but using `other.z` as the Y value
529 # to adapt from the flat plane on X/Y to X/Z.
530 private fun dist2_3d(other: Point3d[Numeric]): N
531 do
532 var dx = other.x.sub(x)
533 var dy = other.z.sub(y)
534 var s = (dx.mul(dx)).add(dy.mul(dy))
535 return x.value_of(s)
536 end
537 end
538
539 redef class Float
540 # Fuzzy value in `[self-variation..self+variation]`
541 fun &(variation: Float): Float do return self - variation + 2.0*variation.rand
542 end