examples: update users of `mnit::mnit_fps`
[nit.git] / contrib / tinks / src / client / client.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 # Portable client
16 module client
17
18 import mnit
19 import mnit::opengles1
20 import performance_analysis
21
22 import game
23 import common
24
25 import assets
26 import context
27
28 # A position within the screen
29 class ScreenPos
30 super Point[Float]
31
32 # Convert to a game logic `Pos` by applying camera transformation
33 fun to_logic(camera: Camera): Pos do
34 return new Pos(x/camera.basic_zoom + camera.dx, y/camera.basic_zoom + camera.dy)
35 end
36 end
37
38 redef class Pos
39 # Convert to a `ScreenPos` by applying camera transformation
40 fun to_screen(camera: Camera): ScreenPos do
41 return new ScreenPos((x - camera.dx) * camera.basic_zoom, (y - camera.dy) * camera.basic_zoom)
42 end
43 end
44
45 # Camera managing the screen view on the world
46 class Camera
47 # Offset of the top left corner of the screen, X part
48 var dx = 0.0
49
50 # Offset of the top left corner of the screen, Y part
51 var dy = 0.0
52
53 # Basic zoom, the distance between 2 features
54 #
55 # In the world logic, the distance is of 1.
56 # This value depends on the size of the graphical assets.
57 #
58 # TODO make it a full zoom by scaling images too, if needed.
59 var basic_zoom = 32.0
60
61 # Center of the `display` as world `Pos`
62 fun center(display: Display): Pos
63 do
64 return (new ScreenPos(display.width.to_f * 0.5, display.height.to_f * 0.5)).to_logic(self)
65 end
66
67 # Center the `display` on the world `pos`
68 fun center_on(display: Display, pos: Pos)
69 do
70 self.dx = pos.x - display.width.to_f * 0.5 / basic_zoom
71 self.dy = pos.y - display.height.to_f * 0.5 / basic_zoom
72 end
73 end
74
75 redef class App
76
77 # Collection of assets
78 var assets = new Assets is lazy
79
80 redef fun on_create
81 do
82 super
83 maximum_fps = 60.0
84 assets.assign_images_to_story context.game.story
85 end
86
87 # Camera managing transformation between world and screen positions
88 var camera = new Camera
89
90 # Context of the game, either local or remote
91 var context: GameContext is lazy do
92
93 # Server info
94 var address = null
95 var port = default_listening_port
96
97 if args.not_empty then
98 # Use first argument as the server address
99 address = args[0]
100 if args.length > 1 then port = args[1].to_i
101 else
102 print "Looking for a server..."
103
104 var s = new UDPSocket
105 s.enable_broadcast = true
106 s.blocking = false
107 s.broadcast(discovery_port, "Server? {handshake_app_name}")
108 nanosleep(0, 100_000_000)
109
110 var ptr = new Ref[nullable SocketAddress](null)
111 var resp = s.recv_from(1024, ptr)
112 var src = ptr.item
113
114 if not resp.is_empty then
115 var words = resp.split(" ")
116 if words.length == 3 and words[0] == "Server!" and words[1] == handshake_app_name and words[2].is_numeric then
117 address = src.address
118 port = words[2].to_i
119 end
120 end
121 end
122
123 if address == null then
124 print "Launching a local server"
125
126 # No command line
127 return new LocalServerContext
128 else
129 print "Connecting to:{address}:{port}"
130 maximum_fps = 0.0
131
132 # Args are: tinks server_address {port}
133 #var address = "riph" # args[0]
134 #var port = sys.default_listening_port
135 if args.length > 1 then port = args[1].to_i
136
137 # Setup connection config
138 var server_config = new RemoteServerConfig(address, port)
139 var server = new RemoteServer(server_config)
140
141 # Connect then complete handshake
142 assert server.connect else print_error "Connection to server failed with {server.socket.last_error or else "none"}"
143 assert server.handshake else print_error "Handshake with server failed"
144
145 # Download and setup remote game
146 var context = new RemoteGameContext(server)
147 context.setup
148
149 return context
150 end
151 end
152
153 # `Tank` of the local player, if any
154 fun local_tank: nullable Tank
155 do
156 # FIXME use a ? to one line this
157 var local_player = context.local_player
158 if local_player == null then return null
159 return local_player.tank
160 end
161
162 # Square of the minimum distance from the tank for an object to be "far"
163 #
164 # This value influences which sounds are heard,
165 # the strength of vibrations and
166 # whether an arrow points to a far unit
167 private var far_dist2 = 2000.0
168
169 # Tank tracks tracks on the ground
170 #
171 # TODO use particles or at least optimize drawing
172 var tracks = new List[Couple[Pos, Float]]
173
174 redef fun frame_core(display)
175 do
176 var clock = new Clock
177
178 var turn = context.do_turn
179 sys.perfs["do_turn"].add clock.lapse
180
181 # Draw
182
183 # Update camera
184 if down_keys.has("left") then camera.dx -= 1.0
185 if down_keys.has("right") then camera.dx += 1.0
186 if down_keys.has("up") then camera.dy -= 1.0
187 if down_keys.has("down") then camera.dy += 1.0
188
189 var local_tank = local_tank
190 if local_tank != null then
191 var tank_speed = local_tank.direction_forwards*local_tank.rule.max_speed
192 tank_speed = tank_speed.min(0.5).max(-0.5)
193
194 var prop_pos = local_tank.pos + local_tank.heading.to_vector(tank_speed * 16.0)
195 var old_pos = camera.center(display)
196 var half = old_pos.lerp(prop_pos, 0.02)
197
198 camera.center_on(display, new Pos(half.x, half.y))
199 end
200
201 # Grass
202 display.clear(0.0, 0.45, 0.0)
203
204 # Past tank tracks
205 for track in tracks do
206 var pos = track.first.to_screen(camera)
207 display.blit_rotated(assets.drawing.track, pos.x, pos.y, track.second)
208 end
209
210 # Past blast sites
211 for blast in context.game.world.blast_sites do
212 var pos = blast.to_screen(camera)
213 display.blit_centered(assets.drawing.blast, pos.x, pos.y)
214 end
215
216 # Terrain features
217 var tl = (new ScreenPos(0.0, 0.0)).to_logic(camera)
218 var br = (new ScreenPos(display.width.to_f, display.height.to_f)).to_logic(camera)
219 for x in [tl.x.floor.to_i .. br.x.ceil.to_i] do
220 for y in [tl.y.floor.to_i .. br.y.ceil.to_i] do
221 var feature = context.game.world[x, y]
222 if feature != null then
223 var pos = feature.pos.to_screen(camera)
224 var image = feature.rule.images[feature.image_index]
225 display.blit_rotated(image, pos.x, pos.y, feature.angle)
226 end
227 end
228 end
229
230 # Tanks
231 for tank in context.game.tanks do
232 # Add random tracks
233 if (tank.direction_heading != 0.0 and 40.rand == 0) or
234 (tank.direction_forwards != 0.0 and 100.rand == 0) then
235
236 tracks.add new Couple[Pos, Float](tank.pos, tank.heading)
237 if tracks.length > 1000 then tracks.shift
238 end
239
240 # Get the player stencil
241 var player = tank.player
242 var stencil = null
243 if player != null then stencil = assets.drawing.stencils[player.stencil_index]
244
245 if camera.center(display).dist2(tank.pos) > far_dist2 then
246 var hw = (display.width/2).to_f
247 var hh = (display.height/2).to_f
248
249 var angle = camera.center(display).atan2(tank.pos)
250 var x = hw + angle.cos * (hw-128.0)
251 var y = hh + angle.sin * (hh-128.0)
252
253 var screen_pos = new ScreenPos(x, y)
254 display.blit_rotated(assets.drawing.arrow, screen_pos.x, screen_pos.y, angle)
255 if stencil != null then display.blit_rotated(stencil, screen_pos.x, screen_pos.y, angle)
256 continue
257 end
258
259 var screen_pos = tank.pos.to_screen(camera)
260
261 var damage = tank.rule.max_health - tank.health
262 damage = damage.max(0).min(tank.rule.base_images.length)
263
264 var base_image = tank.rule.base_images[damage]
265 display.blit_rotated(base_image, screen_pos.x, screen_pos.y, tank.heading)
266 if stencil != null then display.blit_rotated(stencil, screen_pos.x, screen_pos.y, tank.heading)
267 display.blit_rotated(tank.rule.turret_image, screen_pos.x, screen_pos.y, tank.turret.heading)
268
269 if debug then
270 var corners = tank.corners_at(new Couple[Pos, Float](tank.pos, tank.heading))
271 for c in corners do
272 var p = c.to_screen(camera)
273 display.blit_centered(assets.drawing.red_dot, p.x, p.y)
274 end
275 end
276 end
277
278 # Events
279 for event in turn.events do event.client_react(display, turn)
280
281 # Gather and show some performance stats!
282 sys.perfs["draw"].add clock.lapse
283 if context.game.tick % 300 == 5 then print sys.perfs
284 end
285
286 # Keys currently down
287 #
288 # TODO find a nice API and move up to mnit/gamnit
289 var down_keys = new HashSet[String]
290
291 redef fun input(ie)
292 do
293 var local_tank = local_tank
294 var local_player = context.local_player
295
296 # Quit?
297 if ie isa QuitEvent or
298 (ie isa KeyEvent and ie.name == "escape") then
299
300 quit = true
301 return true
302 end
303
304 # Spawn a new tank?
305 if local_tank == null and local_player != null then
306 if (ie isa KeyEvent and ie.name == "space") or
307 (ie isa PointerEvent and ie.depressed) then
308
309 local_player.orders.add new SpawnTankOrder(local_player)
310 return true
311 end
312 end
313
314 if ie isa KeyEvent then
315
316 # Update `down_keys`
317 var name = ie.name
318 if ie.is_down then
319 down_keys.add name
320 else if down_keys.has(name) then
321 down_keys.remove name
322 end
323
324 # wasd to move tank
325 var direction_change = ["w", "a", "s", "d"].has(ie.name)
326 if direction_change and local_tank != null and local_player != null then
327 var forward = down_keys.has("w")
328 var backward = down_keys.has("s")
329 var left = down_keys.has("a")
330 var right = down_keys.has("d")
331
332 # Cancel contradictory commands
333 if forward and backward then
334 forward = false
335 backward = false
336 end
337
338 if left and right then
339 left = false
340 right = false
341 end
342
343 # Set movement and direction
344 var move = 0.0
345 if forward then
346 move = 0.5
347 else if backward then move = -0.5
348
349 var ori = 0.0
350 if left then
351 ori = -local_tank.rule.max_direction/2.0
352 else if right then ori = local_tank.rule.max_direction/2.0
353
354 # Activate to invert the orientation on reverse, (for at @R4p4Ss)
355 #if backward then ori = -ori
356
357 # Bonus when only moving or only turning
358 if not forward and not backward then ori *= 2.0
359 if not left and not right then move *= 2.0
360
361 # Give order
362 local_player.orders.add new TankDirectionOrder(local_tank, ori, move)
363 return true
364 end
365 end
366
367 # On click (or tap), aim and fire
368 if ie isa PointerEvent then
369
370 if ie.pressed and local_tank != null and local_player != null then
371 var target = (new ScreenPos(ie.x, ie.y)).to_logic(camera)
372 local_player.orders.add new AimAndFireOrder(local_tank, target)
373 return true
374 end
375 end
376
377 return false
378 end
379 end
380
381 redef class TEvent
382 fun client_react(display: Display, turn: TTurn) do end
383 end
384
385 redef class ExplosionEvent
386 redef fun client_react(display, turn)
387 do
388 var pos = pos.to_screen(app.camera)
389 display.blit_centered(app.assets.drawing.explosion, pos.x, pos.y)
390 end
391 end
392
393 redef class OpenFireEvent
394 redef fun client_react(display, turn)
395 do
396 var screen_pos = tank.pos.to_screen(app.camera)
397 display.blit_rotated(app.assets.drawing.turret_firing, screen_pos.x, screen_pos.y, tank.turret.heading)
398
399 if tank.pos.dist2(app.camera.center(display)) < app.far_dist2 then
400 # Within earshot
401 app.assets.turret_fire.play
402 end
403 end
404 end
405
406 redef class TurretReadyEvent
407 redef fun client_react(display, turn)
408 do
409 if tank.pos.dist2(app.camera.center(display)) < app.far_dist2 then
410 # Within earshot
411 app.assets.turret_ready.play
412 end
413 end
414 end