-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathClientStack.lua
More file actions
495 lines (425 loc) · 20.1 KB
/
ClientStack.lua
File metadata and controls
495 lines (425 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
local class = require("common.lib.class")
local consts = require("client.src.consts")
local Signal = require("common.lib.signal")
local GraphicsUtil = require("client.src.graphics.graphics_util")
-- Draws an image at the given spot while scaling all coordinate and scale values with stack.gfxScale
local function drawGfxScaled(stack, img, x, y, rot, xScale, yScale)
xScale = xScale or 1
yScale = yScale or 1
GraphicsUtil.draw(img, x * stack.gfxScale, y * stack.gfxScale, rot, xScale * stack.gfxScale, yScale * stack.gfxScale)
end
---The base class for a client side wrapper around an engine stack
---Supports general properties for positioning and drawing
---@class ClientStack : Signal
---@field is_local boolean if the Stack gets its inputs live from the local client or not
---@field character Character the character to use for drawing and sounds
---@field theme table the theme to determine offsets via theme for multibar and other properties
---@field panels_dir string id of the panel set to use for metal garbage assets and panels
---@field baseWidth integer
---@field baseHeight integer
---@field gfxScale number scale factor for the entire Stack, default: 3
---@field panelOriginXOffset integer how far the panel origin is offset relative to frame at scale 1
---@field panelOriginYOffset integer how far the panel origin is offset relative to frame at scale 1
---@field canvas boolean if the stack is supposed to be drawn
---@field portraitFade number inverse opacity of the character portrait
---@field engine BaseStack the engine actually running the physics
---@field multiBarFrameCount integer at how many frames the BaseStack's multibar tops out
---@field player MatchParticipant
---@field healthQuad love.Quad
---@field multi_prestopQuad love.Quad
---@field multi_stopQuad love.Quad
---@field multi_shakeQuad love.Quad
---@field danger_music boolean
---@field garbageSource ClientStack The stack the garbage assets are used from
---@field assets IngameAssetPack
---@field renderIndex integer determines the position of the stack and how some elements are rendered
---@field player_number integer used for display ordering
---@class ClientStack
local ClientStack = class(
---@param self ClientStack
---@param args table
function(self, args)
self = self
assert(args.engine)
assert(args.characterId)
self.engine = args.engine
-- player number according to the multiplayer server, for game outcome reporting
self.player_number = args.player_number or args.engine.which
self.is_local = args.player and args.player.isLocal or args.engine.is_local
self.character = characters[args.characterId]
self.theme = args.theme or themes[config.theme]
self.panels_dir = args.panels_dir
if not self.panels_dir or not panels[self.panels_dir] then
self.panels_dir = config.panels
end
-- graphics
-- also relevant for the touch input controller method besides general drawing
self.baseWidth = 104
self.baseHeight = 204
self.panelOriginXOffset = 4
self.panelOriginYOffset = 4
self.gfxScale = 3
-- stacks no longer have a canvas but some functions bool check it to determine whether they should run or not
-- mostly for tests / not running extra in some scenarios; should be removed once they have been adjusted
self.canvas = true
self.portraitFade = config.portrait_darkness / 100 -- will be set back to 0 if count down happens
self.danger_music = false
Signal.turnIntoEmitter(self)
self:createSignal("dangerMusicChanged")
end)
-- Provides the X origin to draw an element of the stack
-- cameFromLegacyScoreOffset - set to true if this used to use the "score" position in legacy themes
function ClientStack:elementOriginX(cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
assert(cameFromLegacyScoreOffset ~= nil)
assert(legacyOffsetIsAlreadyScaled ~= nil)
local x = 546
if self.renderIndex == 2 then
x = 642
end
if cameFromLegacyScoreOffset == false or themes[config.theme]:offsetsAreFixed() then
x = self.origin_x
if legacyOffsetIsAlreadyScaled == false or themes[config.theme]:offsetsAreFixed() then
x = x * self.gfxScale
end
end
return x
end
-- Provides the Y origin to draw an element of the stack
-- cameFromLegacyScoreOffset - set to true if this used to use the "score" position in legacy themes
function ClientStack:elementOriginY(cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
assert(cameFromLegacyScoreOffset ~= nil)
assert(legacyOffsetIsAlreadyScaled ~= nil)
local y = 208
if cameFromLegacyScoreOffset == false or themes[config.theme]:offsetsAreFixed() then
y = self.panelOriginY
if legacyOffsetIsAlreadyScaled == false or themes[config.theme]:offsetsAreFixed() then
y = y * self.gfxScale
end
end
return y
end
-- Provides the X position to draw an element of the stack, shifted by the given offset and mirroring
-- themePositionOffset - the theme offset array
-- cameFromLegacyScoreOffset - set to true if this used to use the "score" position in legacy themes
-- legacyOffsetIsAlreadyScaled - set to true if the offset used to be already scaled in legacy themes
function ClientStack:elementOriginXWithOffset(themePositionOffset, cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
if legacyOffsetIsAlreadyScaled == nil then
legacyOffsetIsAlreadyScaled = false
end
local xOffset = themePositionOffset[1]
if cameFromLegacyScoreOffset == false or themes[config.theme]:offsetsAreFixed() then
xOffset = xOffset * self.mirror_x
end
if cameFromLegacyScoreOffset == false and themes[config.theme]:offsetsAreFixed() == false and legacyOffsetIsAlreadyScaled == false then
xOffset = xOffset * self.gfxScale
end
local x = self:elementOriginX(cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled) + xOffset
return x
end
-- Provides the Y position to draw an element of the stack, shifted by the given offset and mirroring
-- themePositionOffset - the theme offset array
-- cameFromLegacyScoreOffset - set to true if this used to use the "score" position in legacy themes
function ClientStack:elementOriginYWithOffset(themePositionOffset, cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
if legacyOffsetIsAlreadyScaled == nil then
legacyOffsetIsAlreadyScaled = false
end
local yOffset = themePositionOffset[2]
if cameFromLegacyScoreOffset == false and themes[config.theme]:offsetsAreFixed() == false and legacyOffsetIsAlreadyScaled == false then
yOffset = yOffset * self.gfxScale
end
local y = self:elementOriginY(cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled) + yOffset
return y
end
-- Provides the X position to draw a label of the stack, shifted by the given offset, mirroring and label width
-- themePositionOffset - the theme offset array
-- cameFromLegacyScoreOffset - set to true if this used to use the "score" position in legacy themes
-- width - width of the drawable
-- percentWidthShift - the percent of the width you want shifted left
function ClientStack:labelOriginXWithOffset(themePositionOffset, scale, cameFromLegacyScoreOffset, width, percentWidthShift, legacyOffsetIsAlreadyScaled)
local x = self:elementOriginXWithOffset(themePositionOffset, cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
if percentWidthShift > 0 then
x = x - math.floor((percentWidthShift * width * scale))
end
return x
end
function ClientStack:drawLabel(drawable, themePositionOffset, scale, cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
if cameFromLegacyScoreOffset == nil then
cameFromLegacyScoreOffset = false
end
local percentWidthShift = 0
-- If we are mirroring from the right, move the full width left
if cameFromLegacyScoreOffset == false or themes[config.theme]:offsetsAreFixed() then
if self.multiplication > 0 then
percentWidthShift = 1
end
end
local x = self:labelOriginXWithOffset(themePositionOffset, scale, cameFromLegacyScoreOffset, drawable:getWidth(), percentWidthShift, legacyOffsetIsAlreadyScaled)
local y = self:elementOriginYWithOffset(themePositionOffset, cameFromLegacyScoreOffset, legacyOffsetIsAlreadyScaled)
GraphicsUtil.draw(drawable, x, y, 0, scale, scale)
end
function ClientStack:drawBar(image, quad, themePositionOffset, height, yOffset, rotate, scale)
local imageWidth, imageHeight = image:getDimensions()
local barYScale = height / imageHeight
local quadY = 0
if barYScale < 1 then
barYScale = 1
quadY = imageHeight - height
end
local x = self:elementOriginXWithOffset(themePositionOffset, false)
local y = self:elementOriginYWithOffset(themePositionOffset, false)
quad:setViewport(0, quadY, imageWidth, imageHeight - quadY)
GraphicsUtil.drawQuad(image, quad, x, y - height - yOffset, rotate, scale, scale * barYScale, 0, 0, self.mirror_x)
end
function ClientStack:drawNumber(number, themePositionOffset, scale, cameFromLegacyScoreOffset)
if cameFromLegacyScoreOffset == nil then
cameFromLegacyScoreOffset = false
end
local x = self:elementOriginXWithOffset(themePositionOffset, cameFromLegacyScoreOffset)
local y = self:elementOriginYWithOffset(themePositionOffset, cameFromLegacyScoreOffset)
GraphicsUtil.drawPixelFont(number, self.assets.numberPixelFont, x, y, scale, scale, "center", 0)
end
---@param str string
---@param themePositionOffset number[]
---@param cameFromLegacyScoreOffset boolean
---@param fontSize FontSize
function ClientStack:drawString(str, themePositionOffset, cameFromLegacyScoreOffset, fontSize)
if cameFromLegacyScoreOffset == nil then
cameFromLegacyScoreOffset = false
end
local x = self:elementOriginXWithOffset(themePositionOffset, cameFromLegacyScoreOffset)
local y = self:elementOriginYWithOffset(themePositionOffset, cameFromLegacyScoreOffset)
local limit = love.graphics.getWidth() - x
local alignment = "left"
if themes[config.theme]:offsetsAreFixed() then
if self.renderIndex == 1 then
limit = x
x = 0
alignment = "right"
end
end
GraphicsUtil.printf(str, x, y, limit, alignment, nil, nil, fontSize)
end
-- Positions the stack draw position for the given player
function ClientStack:moveForRenderIndex(renderIndex)
self.renderIndex = renderIndex
-- Position of elements should ideally be on even coordinates to avoid non pixel alignment
if renderIndex == 1 then
self.mirror_x = 1
self.multiplication = 0
elseif renderIndex == 2 then
self.mirror_x = -1
self.multiplication = 1
end
local centerX = love.graphics.getWidth() / 2
local stackWidth = self:canvasWidth()
local innerStackXMovement = 100
local outerStackXMovement = stackWidth + innerStackXMovement
local outerEdgeScaled = centerX - (outerStackXMovement * self.mirror_x)
local frameOriginEdgeScaled = outerEdgeScaled
if self.mirror_x == -1 then
frameOriginEdgeScaled = outerEdgeScaled - stackWidth
end
self:moveToPosition(frameOriginEdgeScaled, self.baseWidth + self.panelOriginXOffset)
self.origin_x = (self.panelOriginXOffset * self.mirror_x) + (outerEdgeScaled / self.gfxScale) -- The outer X value of the frame
self:assignAssets(GAME.theme:getIngameAssetPack(self.renderIndex))
end
---@param x integer in screen coordinates
---@param y integer in screen coordinates
function ClientStack:moveToPosition(x, y)
self.frameOriginX = x / self.gfxScale
self.frameOriginY = y / self.gfxScale
self.panelOriginX = self.frameOriginX + self.panelOriginXOffset
self.panelOriginY = self.frameOriginY + self.panelOriginYOffset
self.origin_x = x / self.gfxScale
end
-- to be used in conjunction with resetDrawArea
-- sets the draw area for the Stack by defining an area outside of which all draws are cut off
-- and translating following draws to be relative to the top left origin of the area
function ClientStack:setDrawArea()
-- this used to be a canvas instead but turns out switching between canvases can be quite the overhead
love.graphics.setScissor(self.frameOriginX * self.gfxScale, self.frameOriginY * self.gfxScale, self.baseWidth * self.gfxScale, self.baseHeight * self.gfxScale)
love.graphics.push("transform")
love.graphics.translate(self.frameOriginX * self.gfxScale, self.frameOriginY * self.gfxScale)
end
-- to be used in conjunction with setDrawArea
-- resets the draw area and removes the translation
function ClientStack:resetDrawArea()
love.graphics.pop()
love.graphics.setScissor()
end
function ClientStack:drawCharacter()
-- Update portrait fade if needed
if self.engine.do_countdown then
-- self.portraitFade starts at 0 (no fade)
if self.engine.clock and self.engine.clock > 0 then
local desiredFade = config.portrait_darkness / 100
local startFrame = 50
local fadeDuration = 30
if self.engine.clock <= 50 then
self.portraitFade = 0
elseif self.engine.clock > 50 and self.engine.clock <= startFrame + fadeDuration then
local percent = (self.engine.clock - startFrame) / fadeDuration
self.portraitFade = desiredFade * percent
end
end
end
self.character:drawPortrait(self.renderIndex, self.panelOriginXOffset, self.panelOriginYOffset, self.portraitFade, self.gfxScale)
end
function ClientStack:drawFrame()
local frameImage = self.assets.frame
if frameImage then
local scaleX = self:canvasWidth() / frameImage:getWidth()
local scaleY = self:canvasHeight() / frameImage:getHeight()
GraphicsUtil.draw(frameImage, 0, 0, 0, scaleX, scaleY)
end
end
function ClientStack:drawWall(displacement, rowCount)
local wallImage = self.assets.wall
if wallImage then
local y = (4 - displacement + rowCount * 16) * self.gfxScale
local width = 96
local scaleX = width * self.gfxScale / wallImage:getWidth()
GraphicsUtil.draw(wallImage, 4 * self.gfxScale, y, 0, scaleX, scaleX)
end
end
function ClientStack:drawCountdown()
if self.engine.do_countdown and self.engine.countdown_timer and self.engine.countdown_timer > 0 then
local ready_x = 16
local initial_ready_y = 4
local ready_y_drop_speed = 6
local ready_y = initial_ready_y + (math.min(8, self.engine.clock) - 1) * ready_y_drop_speed
local countdown_x = 44
local countdown_y = 68
if self.engine.clock <= 8 then
drawGfxScaled(self, themes[config.theme].images.IMG_ready, ready_x, ready_y)
elseif self.engine.clock >= 9 and self.engine.countdown_timer and self.engine.countdown_timer > 0 then
if self.engine.countdown_timer >= 100 then
drawGfxScaled(self, themes[config.theme].images.IMG_ready, ready_x, ready_y)
end
local IMG_number_to_draw = themes[config.theme].images.IMG_numbers[math.ceil(self.engine.countdown_timer / 60)]
if IMG_number_to_draw then
drawGfxScaled(self, IMG_number_to_draw, countdown_x, countdown_y)
end
end
end
end
function ClientStack:canvasWidth()
return self.baseWidth * self.gfxScale
end
function ClientStack:canvasHeight()
return self.baseHeight * self.gfxScale
end
function ClientStack:drawAbsoluteMultibar(stop_time, shake_time, pre_stop_time)
local framePos = themes[config.theme].healthbar_frame_Pos
local barPos = themes[config.theme].multibar_Pos
local overtimePos = themes[config.theme].multibar_LeftoverTime_Pos
self:drawLabel(self.assets.multibar.frameAbsolute, framePos, themes[config.theme].healthbar_frame_Scale * (self.gfxScale / 3))
local multiBarFrameCount = self.multiBarFrameCount
local multiBarMaxHeight = 589 * (self.gfxScale / 3) * themes[config.theme].multibar_Scale
local bottomOffset = 0
local healthHeight = (self.engine.health / multiBarFrameCount) * multiBarMaxHeight
healthHeight = math.min(healthHeight, multiBarMaxHeight)
self:drawBar(self.assets.multibar.health, self.healthQuad, barPos, healthHeight, 0, 0, themes[config.theme].multibar_Scale)
bottomOffset = healthHeight
local stopHeight = 0
local preStopHeight = 0
if shake_time > 0 and shake_time > (stop_time + pre_stop_time) then
-- shake is only drawn if it is greater than prestop + stop
-- shake is always guaranteed to fit
local shakeHeight = (shake_time / multiBarFrameCount) * multiBarMaxHeight
self:drawBar(self.assets.multibar.shake, self.multi_shakeQuad, barPos, shakeHeight, bottomOffset, 0, themes[config.theme].multibar_Scale)
else
-- stop/prestop are only drawn if greater than shake
if stop_time > 0 then
stopHeight = math.min(stop_time, multiBarFrameCount - self.engine.health) / multiBarFrameCount * multiBarMaxHeight
self:drawBar(self.assets.multibar.stop, self.multi_stopQuad, barPos, stopHeight, bottomOffset, 0, themes[config.theme].multibar_Scale)
bottomOffset = bottomOffset + stopHeight
end
local totalInvincibility = self.engine.health + stop_time + pre_stop_time
local remainingSeconds = 0
if totalInvincibility > multiBarFrameCount then
-- total invincibility exceeds what the multibar can display -> fill only the remaining space with prestop
preStopHeight = (1 - (self.engine.health + stop_time) / multiBarFrameCount) * multiBarMaxHeight
remainingSeconds = (totalInvincibility - multiBarFrameCount) / 60
else
preStopHeight = pre_stop_time / multiBarFrameCount * multiBarMaxHeight
end
if pre_stop_time and pre_stop_time > 0 then
self:drawBar(self.assets.multibar.preStop, self.multi_prestopQuad, barPos, preStopHeight, bottomOffset, 0, themes[config.theme].multibar_Scale)
end
if remainingSeconds > 0 then
self:drawString(string.format("%." .. themes[config.theme].multibar_LeftoverTime_Decimals .. "f", remainingSeconds), overtimePos, false, "big")
end
end
end
function ClientStack:drawPlayerName()
local username = (self.player.name or "")
self:drawString(username, themes[config.theme].name_Pos, true, "big")
end
function ClientStack:drawWinCount()
self:drawLabel(self.assets.wins, themes[config.theme].winLabel_Pos, themes[config.theme].winLabel_Scale, true)
self:drawNumber(self.player:getWinCountForDisplay(), themes[config.theme].win_Pos, themes[config.theme].win_Scale, true)
end
function ClientStack.attackSoundInfoForMatch(isChainLink, chainSize, comboSize, metalCount)
if metalCount > 0 then
-- override SFX with shock sound
return {type = consts.ATTACK_TYPE.shock, size = metalCount}
elseif isChainLink then
return {type = consts.ATTACK_TYPE.chain, size = chainSize}
elseif comboSize > 3 then
return {type = consts.ATTACK_TYPE.combo, size = comboSize}
end
return nil
end
---@param doCountdown boolean if the stack should have a countdown at the beginning
function ClientStack:setCountdown(doCountdown)
self.engine:setCountdown(doCountdown)
end
function ClientStack:isCatchingUp()
return self.engine.play_to_end
end
---@param garbageSource ClientStack
function ClientStack:setGarbageSource(garbageSource)
self.garbageSource = garbageSource
end
function ClientStack:setMaxRunsPerFrame(maxRunsPerFrame)
self.engine:setMaxRunsPerFrame(maxRunsPerFrame)
end
---@return boolean
function ClientStack:game_ended()
return self.engine:game_ended()
end
---@param assetPack IngameAssetPack
function ClientStack:assignAssets(assetPack)
self.assets = assetPack
local width, height = self.assets.multibar.health:getDimensions()
self.healthQuad = GraphicsUtil:newRecycledQuad(0, 0, width, height, width, height)
width, height = self.assets.multibar.preStop:getDimensions()
self.multi_prestopQuad = GraphicsUtil:newRecycledQuad(0, 0, width, height, width, height)
width, height = self.assets.multibar.stop:getDimensions()
self.multi_stopQuad = GraphicsUtil:newRecycledQuad(0, 0, width, height, width, height)
width, height = self.assets.multibar.shake:getDimensions()
self.multi_shakeQuad = GraphicsUtil:newRecycledQuad(0, 0, width, height, width, height)
end
function ClientStack:deinit()
GraphicsUtil:releaseQuad(self.healthQuad)
GraphicsUtil:releaseQuad(self.multi_prestopQuad)
GraphicsUtil:releaseQuad(self.multi_stopQuad)
GraphicsUtil:releaseQuad(self.multi_shakeQuad)
end
---@alias GarbageTarget {frameOriginX: number, frameOriginY: number, mirror_x: integer, canvasWidth: number}
---@param garbageTarget GarbageTarget
function ClientStack:setGarbageTarget(garbageTarget)
self.garbageTarget = garbageTarget
end
--------------------------------
------ abstract functions ------
--------------------------------
function ClientStack:runGameOver()
error("did not implement runGameOver")
end
---@param matchEnded boolean?
function ClientStack:render(matchEnded)
error("did not implement render")
end
return ClientStack