bc434b72243752ae5eebdf281f61fafc201c413d
[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 base
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 # Square of the minimum distance from the tank for an object to be "far"
91 #
92 # This value influences which sounds are heard,
93 # the strength of vibrations and
94 # whether an arrow points to a far unit
95 private var far_dist2 = 2000.0
96
97 redef fun context
98 do
99 var s = super
100 if s isa RemoteGameContext then maximum_fps = 0.0
101 return s
102 end
103
104 # Tank tracks tracks on the ground
105 #
106 # TODO use particles or at least optimize drawing
107 var tracks = new List[Couple[Pos, Float]]
108
109 redef fun frame_core(display)
110 do
111 var clock = new Clock
112
113 var turn = context.do_turn
114 sys.perfs["do_turn"].add clock.lapse
115
116 # Draw
117
118 # Update camera
119 if down_keys.has("left") then camera.dx -= 1.0
120 if down_keys.has("right") then camera.dx += 1.0
121 if down_keys.has("up") then camera.dy -= 1.0
122 if down_keys.has("down") then camera.dy += 1.0
123
124 var local_tank = local_tank
125 if local_tank != null then
126 var tank_speed = local_tank.direction_forwards*local_tank.rule.max_speed
127 tank_speed = tank_speed.clamp(-0.5, 0.5)
128
129 var prop_pos = local_tank.pos + local_tank.heading.to_vector(tank_speed * 16.0)
130 var old_pos = camera.center(display)
131 var half = old_pos.lerp(prop_pos, 0.02)
132
133 camera.center_on(display, new Pos(half.x, half.y))
134 end
135
136 # Grass
137 display.clear(0.0, 0.45, 0.0)
138
139 # Past tank tracks
140 for track in tracks do
141 var pos = track.first.to_screen(camera)
142 display.blit_rotated(assets.drawing.track, pos.x, pos.y, track.second)
143 end
144
145 # Past blast sites
146 for blast in context.game.world.blast_sites do
147 var pos = blast.to_screen(camera)
148 display.blit_centered(assets.drawing.blast, pos.x, pos.y)
149 end
150
151 # Terrain features
152 var tl = (new ScreenPos(0.0, 0.0)).to_logic(camera)
153 var br = (new ScreenPos(display.width.to_f, display.height.to_f)).to_logic(camera)
154 for x in [tl.x.floor.to_i .. br.x.ceil.to_i] do
155 for y in [tl.y.floor.to_i .. br.y.ceil.to_i] do
156 var feature = context.game.world[x, y]
157 if feature != null then
158 var pos = feature.pos.to_screen(camera)
159 var image = feature.rule.images[feature.image_index]
160 display.blit_rotated(image, pos.x, pos.y, feature.angle)
161 end
162 end
163 end
164
165 # Tanks
166 for tank in context.game.tanks do
167 # Add random tracks
168 if (tank.direction_heading != 0.0 and 40.rand == 0) or
169 (tank.direction_forwards != 0.0 and 100.rand == 0) then
170
171 tracks.add new Couple[Pos, Float](tank.pos, tank.heading)
172 if tracks.length > 1000 then tracks.shift
173 end
174
175 # Get the player stencil
176 var player = tank.player
177 var stencil = null
178 if player != null then stencil = assets.drawing.stencils[player.stencil_index]
179
180 if camera.center(display).dist2(tank.pos) > far_dist2 then
181 var hw = (display.width/2).to_f
182 var hh = (display.height/2).to_f
183
184 var angle = camera.center(display).atan2(tank.pos)
185 var x = hw + angle.cos * (hw-128.0)
186 var y = hh + angle.sin * (hh-128.0)
187
188 var screen_pos = new ScreenPos(x, y)
189 display.blit_rotated(assets.drawing.arrow, screen_pos.x, screen_pos.y, angle)
190 if stencil != null then display.blit_rotated(stencil, screen_pos.x, screen_pos.y, angle)
191 continue
192 end
193
194 var screen_pos = tank.pos.to_screen(camera)
195
196 var damage = tank.rule.max_health - tank.health
197 damage = damage.clamp(0, tank.rule.base_images.length)
198
199 var base_image = tank.rule.base_images[damage]
200 display.blit_rotated(base_image, screen_pos.x, screen_pos.y, tank.heading)
201 if stencil != null then display.blit_rotated(stencil, screen_pos.x, screen_pos.y, tank.heading)
202 display.blit_rotated(tank.rule.turret_image, screen_pos.x, screen_pos.y, tank.turret.heading)
203
204 if debug then
205 var corners = tank.corners_at(new Couple[Pos, Float](tank.pos, tank.heading))
206 for c in corners do
207 var p = c.to_screen(camera)
208 display.blit_centered(assets.drawing.red_dot, p.x, p.y)
209 end
210 end
211 end
212
213 # Events
214 for event in turn.events do event.client_react(display, turn)
215
216 # Gather and show some performance stats!
217 sys.perfs["draw"].add clock.lapse
218 if context.game.tick % 300 == 5 then print sys.perfs
219 end
220
221 # Keys currently down
222 #
223 # TODO find a nice API and move up to mnit/gamnit
224 var down_keys = new HashSet[String]
225
226 redef fun input(ie)
227 do
228 var local_tank = local_tank
229 var local_player = context.local_player
230
231 # Quit?
232 if ie isa QuitEvent or
233 (ie isa KeyEvent and ie.name == "escape") then
234
235 quit = true
236 return true
237 end
238
239 # Spawn a new tank?
240 if local_tank == null and local_player != null then
241 if (ie isa KeyEvent and ie.name == "space") or
242 (ie isa PointerEvent and ie.depressed) then
243
244 local_player.orders.add new SpawnTankOrder(local_player)
245 return true
246 end
247 end
248
249 if ie isa KeyEvent then
250
251 # Update `down_keys`
252 var name = ie.name
253 if ie.is_down then
254 down_keys.add name
255 else if down_keys.has(name) then
256 down_keys.remove name
257 end
258
259 # wasd to move tank
260 var direction_change = ["w", "a", "s", "d"].has(ie.name)
261 if direction_change and local_tank != null and local_player != null then
262 var forward = down_keys.has("w")
263 var backward = down_keys.has("s")
264 var left = down_keys.has("a")
265 var right = down_keys.has("d")
266
267 # Cancel contradictory commands
268 if forward and backward then
269 forward = false
270 backward = false
271 end
272
273 if left and right then
274 left = false
275 right = false
276 end
277
278 # Set movement and direction
279 var move = 0.0
280 if forward then
281 move = 0.5
282 else if backward then move = -0.5
283
284 var ori = 0.0
285 if left then
286 ori = -local_tank.rule.max_direction/2.0
287 else if right then ori = local_tank.rule.max_direction/2.0
288
289 # Activate to invert the orientation on reverse, (for at @R4p4Ss)
290 #if backward then ori = -ori
291
292 # Bonus when only moving or only turning
293 if not forward and not backward then ori *= 2.0
294 if not left and not right then move *= 2.0
295
296 # Give order
297 local_player.orders.add new TankDirectionOrder(local_tank, ori, move)
298 return true
299 end
300 end
301
302 # On click (or tap), aim and fire
303 if ie isa PointerEvent then
304
305 if ie.pressed and local_tank != null and local_player != null then
306 var target = (new ScreenPos(ie.x, ie.y)).to_logic(camera)
307 local_player.orders.add new AimAndFireOrder(local_tank, target)
308 return true
309 end
310 end
311
312 return false
313 end
314 end
315
316 redef class TEvent
317 fun client_react(display: Display, turn: TTurn) do end
318 end
319
320 redef class ExplosionEvent
321 redef fun client_react(display, turn)
322 do
323 var pos = pos.to_screen(app.camera)
324 display.blit_centered(app.assets.drawing.explosion, pos.x, pos.y)
325 end
326 end
327
328 redef class OpenFireEvent
329 redef fun client_react(display, turn)
330 do
331 var screen_pos = tank.pos.to_screen(app.camera)
332 display.blit_rotated(app.assets.drawing.turret_firing, screen_pos.x, screen_pos.y, tank.turret.heading)
333
334 if tank.pos.dist2(app.camera.center(display)) < app.far_dist2 then
335 # Within earshot
336 app.assets.turret_fire.play
337 end
338 end
339 end
340
341 redef class TurretReadyEvent
342 redef fun client_react(display, turn)
343 do
344 if tank.pos.dist2(app.camera.center(display)) < app.far_dist2 then
345 # Within earshot
346 app.assets.turret_ready.play
347 end
348 end
349 end