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