From dad0cbc37b287e05ff02145dc5546715d9569d37 Mon Sep 17 00:00:00 2001 From: keguigong Date: Wed, 22 Nov 2023 15:35:33 +0800 Subject: [PATCH] Add HP bar and mountain --- game/DistanceMeter.ts | 4 +- game/HPBar.ts | 130 ++++++++++++++++++++++++++++++ game/Horizon.ts | 5 ++ game/Mountain.ts | 78 ++++++++++++++++++ game/Obstacle.ts | 26 +++--- game/Runner.ts | 55 +++++++++++-- game/Trex.ts | 4 +- game/collisionDetection.ts | 7 +- public/offline-bday-sprite@1x.png | Bin 2406 -> 3660 bytes public/offline-bday-sprite@2x.png | Bin 3256 -> 5645 bytes styles/globals.css | 1 + 11 files changed, 283 insertions(+), 27 deletions(-) create mode 100644 game/HPBar.ts create mode 100644 game/Mountain.ts diff --git a/game/DistanceMeter.ts b/game/DistanceMeter.ts index 822101d..6fca809 100644 --- a/game/DistanceMeter.ts +++ b/game/DistanceMeter.ts @@ -6,8 +6,8 @@ export default class DistanceMeter { ctx!: CanvasRenderingContext2D spritePos!: Position - x = 0 - y = 0 + x = 2 + y = 2 distance = 0 maxScore = 0 diff --git a/game/HPBar.ts b/game/HPBar.ts new file mode 100644 index 0000000..0003a39 --- /dev/null +++ b/game/HPBar.ts @@ -0,0 +1,130 @@ +import Runner from "./Runner" +import { IS_HIDPI } from "./varibles" + +export default class HPBar { + canvas!: HTMLCanvasElement + ctx!: CanvasRenderingContext2D + spritePos!: Position + canvasWidth = 0 + + x = 0 + y = 0 + + hp = 3 + hpChanged = false + flashTimer = 0 + flashIterations = 0 + + opacity = 0 + + constructor(canvas: HTMLCanvasElement, spritePos: Position, canvasWidth: number) { + this.canvas = canvas + this.ctx = canvas.getContext("2d")! + this.spritePos = spritePos + this.canvasWidth = canvasWidth + } + + update(deltaTime: number, hp: number) { + let paint = true + if (this.opacity < 1) { + this.opacity = Math.min(this.opacity + HPBar.config.FADE_SPEED, 1) + } + + if (!this.hpChanged) { + this.hpChanged = hp !== this.hp + + this.flashIterations = 0 + this.flashTimer = 0 + } else { + if (this.flashIterations <= HPBar.config.FLASH_ITERATIONS) { + this.flashTimer += deltaTime + + if (this.flashTimer < HPBar.config.FLASH_DURATION) { + paint = false + } else if (this.flashTimer > HPBar.config.FLASH_DURATION * 2) { + this.flashTimer = 0 + this.flashIterations++ + } + } else { + this.hpChanged = false + this.flashIterations = 0 + this.flashTimer = 0 + } + } + + this.hp = hp + + // Draw the digits if not flashing. + if (paint) { + for (let i = 0; i <= HPBar.config.MAX_HP - 1; i++) { + if (i <= this.hp - 1) { + this.draw(i, 0) + } else { + this.draw(i, 1) + } + } + } + } + + draw(targetPos: number, sourcePos: number) { + let sourceWidth = HPBar.dimensions.WIDTH + let sourceHeight = HPBar.dimensions.HEIGHT + let sourceX = HPBar.dimensions.WIDTH * sourcePos + let sourceY = 0 + + let targetX = targetPos * HPBar.dimensions.DEST_WIDTH + let targetY = this.y + let targetWidth = HPBar.dimensions.WIDTH + let targetHeight = HPBar.dimensions.HEIGHT + + if (IS_HIDPI) { + sourceWidth *= 2 + sourceHeight *= 2 + sourceX *= 2 + } + + sourceX += this.spritePos.x + sourceY += this.spritePos.y + + this.ctx.save() + this.ctx.globalAlpha = this.opacity + + this.ctx.drawImage( + Runner.imageBdaySprite, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + targetX, + targetY, + targetWidth, + targetHeight + ) + + this.ctx.restore() + this.ctx.globalAlpha = 1 + } + + reset() { + this.hp = HPBar.config.MAX_HP + this.hpChanged = false + this.flashTimer = 0 + this.flashIterations = 0 + + this.update(0, this.hp) + } + + static config = { + FLASH_DURATION: 1000 / 4, // 一闪的时间(一次闪动分别两闪:从有到无,从无到有) + FLASH_ITERATIONS: 3, // 闪动的次数 + MAX_HP: 3, + HP_UNIT: 1, + FADE_SPEED: 0.035 // 淡入淡出的速度 + } + + static dimensions = { + WIDTH: 28, + HEIGHT: 26, + DEST_WIDTH: 30 + } +} diff --git a/game/Horizon.ts b/game/Horizon.ts index b5d6a6a..0a0c2b2 100644 --- a/game/Horizon.ts +++ b/game/Horizon.ts @@ -1,5 +1,6 @@ import Cloud from "./Cloud" import HorizonLine from "./HorizonLine" +import Mountain from "./Mountain" import NightMode from "./NightMode" import Obstacle from "./Obstacle" import Runner from "./Runner" @@ -22,6 +23,7 @@ export default class Horizon { obstacleHistory: string[] = [] nightMode!: NightMode + mountain!: Mountain constructor(canvas: HTMLCanvasElement, spritePos: SpritePosDef, dimensions: Dimensions, gapCoeffient: number) { this.canvas = canvas @@ -37,12 +39,14 @@ export default class Horizon { this.addCloud() this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON) this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, this.dimensions.WIDTH) + this.mountain = new Mountain(this.canvas, Runner.bdaySpriteDefinition.MOUNTAIN, this.dimensions.WIDTH) } update(deltaTime: number, speed: number, hasObstacles?: boolean, showNightMode: boolean = false) { this.horizonLine.update(deltaTime, speed) this.nightMode.update(showNightMode) this.updateCloud(deltaTime, speed) + this.mountain.update() if (hasObstacles) { this.updateObstacles(deltaTime, speed) } @@ -150,5 +154,6 @@ export default class Horizon { this.obstacles = [] this.horizonLine.reset() this.nightMode.reset() + this.mountain.reset() } } diff --git a/game/Mountain.ts b/game/Mountain.ts new file mode 100644 index 0000000..e2458a6 --- /dev/null +++ b/game/Mountain.ts @@ -0,0 +1,78 @@ +import HorizonLine from "./HorizonLine" +import Runner from "./Runner" +import { IS_HIDPI } from "./varibles" + +export default class Mountain { + canvas!: HTMLCanvasElement + ctx!: CanvasRenderingContext2D + spritePos!: Position + containerWidth = 0 + + xPos = Runner.defaultDimensions.WIDTH + yPos = 0 + + constructor(canvas: HTMLCanvasElement, spritePos: Position, containerWidth: number) { + this.canvas = canvas + this.ctx = canvas.getContext("2d")! + this.spritePos = spritePos + this.containerWidth = containerWidth + + this.xPos = containerWidth + this.yPos = HorizonLine.dimensions.YPOS - Mountain.config.HEIGHT + + this.draw() + } + + updateXPos(currentPos: number, speed: number) { + if (currentPos < -Mountain.config.WIDTH) { + currentPos = this.containerWidth + } else { + currentPos -= speed + } + return currentPos + } + + draw() { + let sourceWidth = Mountain.config.WIDTH + let sourceHeight = Mountain.config.HEIGHT + if (IS_HIDPI) { + sourceWidth *= 2 + sourceHeight *= 2 + } + this.ctx.save() + + this.ctx.drawImage( + Runner.imageBdaySprite, + this.spritePos.x, + this.spritePos.y, + sourceWidth, + sourceHeight, + this.xPos, + this.yPos, + Mountain.config.WIDTH, + Mountain.config.HEIGHT + ) + + this.ctx.restore() + } + + update() { + this.xPos = this.updateXPos(this.xPos, Mountain.config.MOUNTAIN_SPEED) + this.draw() + } + + reset() { + this.xPos = this.containerWidth + this.draw() + } + + protected isVisible() { + return this.xPos + Mountain.config.WIDTH > 0 + } + + static config = { + WIDTH: 413, + HEIGHT: 92, + MOUNTAIN_SPEED: 0.1 + } +} diff --git a/game/Obstacle.ts b/game/Obstacle.ts index 078505b..d29636f 100644 --- a/game/Obstacle.ts +++ b/game/Obstacle.ts @@ -214,25 +214,25 @@ export default class Obstacle { frameRate: 1000 / 6, speedOffset: 0.8 }, - { - type: "BIRTHDAY_CAKE", - width: 33, - height: 40, - yPos: 90, - multipleSpeed: 999, - minGap: 100, - minSpeed: 0, - collisionBoxes: [new CollisionBox(13, 1, 6, 12), new CollisionBox(6, 13, 20, 4), new CollisionBox(3, 18, 27, 19)] - }, + // { + // type: "BIRTHDAY_CAKE", + // width: 33, + // height: 40, + // yPos: 90, + // multipleSpeed: 999, + // minGap: 100, + // minSpeed: 0, + // collisionBoxes: [new CollisionBox(13, 1, 6, 12), new CollisionBox(6, 13, 20, 4), new CollisionBox(3, 18, 27, 19)] + // } { type: "HP", - width: 32, - height: 30, + width: 28, + height: 26, yPos: [100, 75, 50], multipleSpeed: 999, minGap: 100, minSpeed: 0, - collisionBoxes: [new CollisionBox(0, 0, 32, 30)] + collisionBoxes: [new CollisionBox(0, 0, 26, 24)] } ] diff --git a/game/Runner.ts b/game/Runner.ts index 8b26bcf..a2f26bb 100644 --- a/game/Runner.ts +++ b/game/Runner.ts @@ -1,5 +1,6 @@ import DistanceMeter from "./DistanceMeter" import GameOverPanel from "./GameOverPanel" +import HPBar from "./HPBar" import Horizon from "./Horizon" import Trex from "./Trex" import { checkForCollision } from "./collisionDetection" @@ -51,6 +52,9 @@ export default class Runner { audioContext!: AudioContext soundFx: ConfigDict = {} + hpBar!: HPBar + hp = HPBar.config.MAX_HP + constructor(outerContainerId: string, optConfig?: ConfigDict) { this.outerContainerEl = document.querySelector(outerContainerId)! this.config = optConfig || Object.assign(Runner.config, Runner.normalConfig) @@ -126,10 +130,12 @@ export default class Runner { Runner.updateCanvasScaling(this.canvas) this.outerContainerEl.appendChild(this.containerEl) - // Load background class Horizon + // Load background clas s Horizon this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions, this.config.GAP_COEFFICIENT) this.distanceMeter = new DistanceMeter(this.canvas, this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH) + + this.hpBar = new HPBar(this.canvas, Runner.bdaySpriteDefinition.HP, this.dimensions.WIDTH) // this.tRex = new Trex(this.canvas, this.spriteDef.TREX) this.tRex = new Trex(this.canvas, Runner.bdaySpriteDefinition.TREX) @@ -192,6 +198,10 @@ export default class Runner { } } + collision = false + collisionDetection = true + collisionSilentTimer = 0 + update() { this.updatePending = false let now = Date.now() @@ -221,18 +231,35 @@ export default class Runner { } // Check for collisions. - let collision = hasObstacles && checkForCollision(this.horizon.obstacles[0], this.tRex) + this.collisionSilentTimer += deltaTime + const collision = hasObstacles && checkForCollision(this.horizon.obstacles[0], this.tRex) + let disableDetection = this.collisionSilentTimer < Runner.config.COLLISION_DETECTION_SILENT_DURATION if (!collision) { - this.distanceRan += (this.currentSpeed * deltaTime) / this.msPerFrame + this.distanceRan += (this.currentSpeed / this.msPerFrame) * deltaTime if (this.currentSpeed < this.config.MAX_SPEED) { this.currentSpeed += this.config.ACCELERATION } - } else { - this.gameOver() + } else if (!disableDetection) { + this.collisionSilentTimer = 0 + + if (this.collision !== !!collision) { + if (collision.type === "HP") { + this.hp = Math.min(this.hp + 1, HPBar.config.MAX_HP) + } else if (this.hp - 1 > 0) { + this.hp -= 1 + this.playSound(this.soundFx.HIT) + } else { + this.hp = 0 + this.gameOver() + } + } } + this.collision = !!collision + hasObstacles && this.hpBar.update(deltaTime, this.hp) + let playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan)) if (playAchievementSound) { @@ -334,6 +361,14 @@ export default class Runner { this.paused = false this.crashed = false this.gameOverPanel.reset() + + this.hpBar.reset() + this.hp = HPBar.config.MAX_HP + this.collision = false + this.collision = false + this.collisionDetection = true + this.collisionSilentTimer = 0 + this.distanceRan = 0 this.currentSpeed = this.config.SPEED this.time = Date.now() @@ -352,6 +387,7 @@ export default class Runner { this.crashed = true this.distanceMeter.achievement = false + this.hpBar.update(0, this.hp) this.tRex.update(100, Trex.status.CRASHED) if (!this.gameOverPanel) { @@ -679,7 +715,8 @@ export default class Runner { SPEED: 6, SPEED_DROP_COEFFICIENT: 3, ARCADE_MODE_INITIAL_TOP_POSITION: 35, - ARCADE_MODE_TOP_POSITION_PERCENT: 0.1 + ARCADE_MODE_TOP_POSITION_PERCENT: 0.1, + COLLISION_DETECTION_SILENT_DURATION: 825 } static normalConfig = { @@ -772,13 +809,15 @@ export default class Runner { TREX: { x: 0, y: 0 }, BIRTHDAY_CAKE: { x: 384, y: 23 }, BALLOON: { x: 417, y: 29 }, - HP: { x: 433, y: 33 } + HP: { x: 433, y: 37 }, + MOUNTAIN: { x: 0, y: 65 } }, HDPI: { TREX: { x: 0, y: 0 }, BIRTHDAY_CAKE: { x: 768, y: 46 }, BALLOON: { x: 834, y: 58 }, - HP: { x: 866, y: 66 } + HP: { x: 866, y: 74 }, + MOUNTAIN: { x: 0, y: 130 } } } } diff --git a/game/Trex.ts b/game/Trex.ts index 07abd06..3a4ede5 100644 --- a/game/Trex.ts +++ b/game/Trex.ts @@ -270,9 +270,9 @@ export default class Trex { // new CollisionBox(5, 30, 21, 4), // new CollisionBox(9, 34, 15, 4) // ], - DUCKING: [new CollisionBox(39, 19, 9, 16), new CollisionBox(1, 34, 55, 25)], + DUCKING: [new CollisionBox(39, 22, 9, 13), new CollisionBox(1, 34, 55, 25)], RUNNING: [ - new CollisionBox(25, 0, 9, 16), + new CollisionBox(25, 4, 9, 12), new CollisionBox(22, 16, 17, 16), new CollisionBox(1, 34, 30, 9), new CollisionBox(10, 51, 14, 8), diff --git a/game/collisionDetection.ts b/game/collisionDetection.ts index 3bbedc6..c86f784 100644 --- a/game/collisionDetection.ts +++ b/game/collisionDetection.ts @@ -29,7 +29,7 @@ export function checkForCollision(obstacle: Obstacle, tRex: Trex, optCanvasCtx?: } // Simple outer bounds check. - if (true) { + if (boxCompare(tRexBox, obstacleBox)) { const collisionBoxes = obstacle.collisionBoxes let tRexCollisionBoxes = tRex.ducking ? Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING @@ -47,7 +47,10 @@ export function checkForCollision(obstacle: Obstacle, tRex: Trex, optCanvasCtx?: } if (crashed) { - return [adjTrexBox, adjObstacleBox] + return { + type: obstacle.typeConfig.type, + collisionBoxes: [adjTrexBox, adjObstacleBox] + } } } } diff --git a/public/offline-bday-sprite@1x.png b/public/offline-bday-sprite@1x.png index dd33eef0b8aef6fe66dc11d4ff9b6a5a391040f6..05b5f2e84a77f46c0024ab0f8357262040101ecc 100644 GIT binary patch delta 3645 zcmV-D4#M%~63iSSiBL{Q4GJ0x0000DNk~Le000670001;2nGNE03RBucV3MGCfpGF4Kh;YUD0K|ui${0FL(NtGf+ z5Jf1`?1mC4;X_bH;v+Ok1DBk$J1bw`V|#4R*faL?NUMAI9-rf}&v@pY*JJytfeqYz zq5c1>FEj;em$O~QzP|(1>`M(Wf9U4s(7Mcekvi2LwR~ z1Ox;?F}j^s_xI8p)>7SEQ0hLt8Bs%BsHv=N0s^2Imm1Q(-y3GR=>%ORf2QLv1p&2X zI70dEKPvB3XHV2RodJF^#PzYvVYYFfU>#GFY)94 zDRU&oCKZ}=2n-Pv003v%f`VE{TD7Qc|EdoFcJWNZZP`R-0%T z8pYI&Esxcv##>R4;)Tx%=u9H$3@wI9E|XYI$L2Mr{) zTBbmTAg|i?ulm4Tx3U6Ho)B8Y(?a*+QZOPrvmZs>rvOUUP05$8w(?HV&JG+U7&vWb z*zOXx`Q~rWH&@^M z_ukXLfBvIoH!zyo$)A=L807Ix_W>|d&gotnUBzA>e?9lyvGOYKrFGU)-XJ`{Xz2(& z_CZK<*VC6l*)kN=M&bE@TTDlA3(_ zV9DqzMy^NUwkdBq%6r=yqqNkINsiIY#Ei}1$k73&)@T8f5_VP1_gbUC{-m5pPReVE3fii`ukF; z%iEVRNHu>Td~WLb2x~lzacmu_zl*u$SYsWjw>4=X&}Sq-`1s?Gn-sd+^4p*M;m$HY z_|^AjFFQ7uWH0M;^#K5IHhTIXcXXUOb*H?yDQ{in&3$FT+_?m6jmGClU8W;#>n%C5 ze*}H&STGT>d*wmjsQ9HWn%m-3dSJBzvGQS?$r+SWZ98bJp` zuYL9VUpxQ&=kNbqU+ z<&SIAKrS){$IBQj2@s~9BA9cB!oK%> z`lm0Se?I%2|29*E3_&+zP@1{h7i;xQ!Pp~kOY`QNZ=QeFQr_D3ulkUeM

;e<|4l zw>r+}X;5H77?@K4 zK=a^dA2jd({FNrB(>jKP&b}QS3u6=NE4@K|30QZ+?G0Hko&a3 z7Nh8?cc<)oz56+1r-F{Ot)*xOe+Ns0f!ycdx0ZVQWC{Q{X9?OP`BYhhOP~4R7x~VX z{rQi7J^%dncVB3h`0$;-M=kf>j~`#sH|6rfq0bqUYWw=u*Bcb18gZ=^sfN$}?6c3# z<$dFgH<~%RBf@+4?qw}+ZTt6VmW)ZGk0_a;Jp>zkV~=#JQ9*Ee_j z#?}`d6pTg1rOnI@BfP$hL22cU-M>LtuD$$enYA8I+E?DG=R(&~Tm2o&7?ga~!qg{J z&g}?GI6pr1cy0eaWxLnff7bRXgH)M<+>;llI!eZ1E|~&gk)^5apf6S4?L_+?ExU$P z+uEn3Z7r=c@6)X}b4S!;8G^n?8PClajE&f1*B85evFjTR3dYW9b#Fe?cMf;0N*&Jm zhPee@vE|Ku?l_pce}_gHIrsJ0Ozdu>);9uQt3ITvziZWpG2J->e>G+dg4%9e6{ByP zat9N8HTM?C%qLR-ERt$6TvuN(v1Jf(q;0Lu)Qmw$vxmP2V8re+A{Zr@*i z^RJus-(%M|J1B5F-MsqBTT1!IG8efT&s1uw4>jGccefI;%wZ5g>H#;Z@=L^Rs!o)6@TPR1E zYtVO&I9#nzN_n>~$C#<*Dy=?@m7|pU5atYwWej4M%OQ3&Tua@aqSi|auR8Z$XpiJm zWeqN62c~@I<`t|59s9nct}nVUW9PoQe+3*{F1Nm`4}y>>D5bom zlq0p;_U~T5hb`@KpYFY$ys$}V7Iy%kg;W~4*&oFAdpgk z3m!gv7=51Z`uf^&1nh#w3|c}kaR2`O==VH&^yumDmjZ;L07F0llthCB&AedXl6HU) z41H!;UT^>ae+4)N1Y4VOx&i>mpoD-R5EuZ!MoJ0@0)YVltgOU#ff};|ePj)Q+>AWfrP{LtePj&) zV1hkm2l~hw067?Kup4{L4)l>V0DudQkR9kFYXGETe}qBRj+PzhBWnP_p&dCp&_~t) zSi-1+>ZHyNgi-d`=be_TAtNDc0Kg%n&JM(Wp1UCeM_?pD6>`iSh#mRYk|DT6)&PKA z$uUZw@|k;>yC7=-K-EqiS-Ue!)j3Z&Wf0b1OJFcm&hGvMfGYHLUMV`~vCru1Jl6;e zmaJp!f4yV-766JeHkw`9F^^>)a;|$y8RxubsEpn1k~ILJXk(pJsw_d@XU24rYX%14 zU(9ktWgH818UQFo%&0X-Krm(fQ_2z3nYWi6D`)KJ|I#+a20DTR05&(JGrBYa9_lcs zjF{)FKc-_I3kYIAcj~(5ZeIfcYnwYdy)*(If9m|hYdHdfrGY`WoMS=ET7iM!0D#q` z>~vzX0XYMLn2vc$`hRSW!y17>SWg20xuhP2ranCoyPm1*oO9YCHv5pX^OiLLAd8$G z_8Ohn(h+*C3*b;RdN9s$0_>Vb4yJS50K1}5gXJ7Y zK-Dy2ke=fTsES4l@^hR4Mbk*Z`W$yaQ8Y@hJ;x!iDUA@6$8ia)N}~hibDRQMe`#c( zK8{>Kwop2i@(BkkuqJH|B|3CPbWj2%&=ygO z!&N>9A5b!F6k9q(g>#SrHPUvmf41FLG6xf|C)zaDu$zk2eQ1e;2G}bB0=ZOMnFIj< z0tC{krUD590ETHNS*q9yqjCc*qa9_cVk(Hr3y_y~mNA=E2$d6HTiRi|R;d8pig%*& z0hC3MK(a6VQpX>tSOE1gpgW$ZFL$WJ4^$L@{WAdEw<{e!04eBC>i2({6ZwWKhd%j7 P00000NkvXXu0mjft<3=- delta 2381 zcmV-T39|Og9Oe=siBL{Q4GJ0x0000DNk~Le0005e0000#2nGNE0KfY3-H{q02pL*fA;aH<8$-h>wj;?90!1OaH2}tc<2<-suS{|t$YCh zz=L)tD)t(6)Uw^Koqhjz4dSX!R49N_HW73%8oM!zxR@wB>YME{VXs8RCY_-9G0w|`h;qkriAc>9BNojF-w9jd3e^bz6-}!s( z+iGL%?Es({XF5=eG~{N2&fcSgPTtFBtvz}nwu!CwvF&jl6#)QcRG*!D@%BLnwRJ>=qDBGAQB;WV^)LCSYXdA3;2L(2311%7h?IVm0!r6)fx zt;I&E4*;+viaODup1d40J^2=IA3zT=L%H_nt;BdklZcpP1dtb; zC;$LJBE_7j)}Fi;w3RRQ0RWarQ;RJOHJqhmWVsvd7GrvImTw9zf7mOtZ)0e|US{6{ z)P><(004kQviDlu-uTt4SGy_PxN&3qTB;|Hz1VA`&0d!)SFTJNQMq~ZCQ24y1SnAe z0Dwe_DbcAtc$ZQ=dh9(0dymm(uP{}Cx@b_O?lPj?f>~2uwNY(iku)N?8z(qMTls3W zS7_aCZT3zDCj)cAe{e29B*qbGaz@(hO~GuyU~i|lWbADYHSKA;bgC&)6lhtrl`r)H zQrS_R);_we?`E_V`keQg)jykZ14QBuk+zODd&jV|nfpqdr^#pE#`bfu`TcCS_vWMH z_bXoS8Yc=60ZP9uYy~Qa8eLL@?y5dPC&4KlyBpaaxop0I0%NURxz5L!0-zMIs%AR)(#`X}ZT`Xm@R(L;*l0w(60M3+xRH zmZXYo2fFmUYGZaK%4ChQU@w4PM0KytNj`Y+pf|tg&Yf#6{>yMK0H|YVYdtw;cHmjU z#GBUgdNL!|e;U@!HIysP)8{NhpU=UG6FoHxY~;;I?J4_rI@4h)Xe)P$-1sBFTMU%D_M|#FxaC=Nl zs;~DR`+Qnz>Z(*el3_@lzFG?85V@-{Vv0e{&9z4h?VGn9s)ybFL|`S{dUw zhentB$|y7%?=h6x+R6rd z^RdC+9crk6+}obT;3|E$9Nm2PmGe|;DXcZ+)kjL6Lg(blQ%mts12ItmP=VV>zx7qP z^D~sff9E7ypmbhGJG5K>jQjlZ%Ux~UGW+(^pS)>5{`&n>$A;3|=eT8f%85!ZEL5?k z_TJs|Q)5in8+ShT_NPlFm%XvE(W&awJwG)@>dUvUymP4hxt<+fQ(hq*lqdkGBG>SH z%4D?K)XeLt&Ou0h4q>1BOqJT#&pBKjQlKAPe}3cCIen_LFP-7XTYv1F<0?|G6ZI8( zuA3^?EnP{l7km4TPbIg#K1G@<{wdeZ9Z#*XTTJLXa^H(oJ8FDK?!0DooIU~$_2I*Z zon}y?005w#7zG-`D<8eOef{hEKXjT?2c|g1v3w?jq8%N}I}>@(TGti2?w?7Ey|ZF{(87nti0M zgDdBhdd}CUNL|jjy|31LUD#U|?5#tof7!C7K#{Ibkxp@_A;;;G66Xw^^Z1t+x3359 zrVZ=HQeBCLwzO)E>({SuU$xk)t$e8uWnqgm(83rsT65_?p8^$5oG1#UEZAEX6lona zskEsxr`MF%5Mz~P3ig8XeGbxIY<`RbEv-n2i2?vXcli0;?#>sNx{{dh7{cCke_Q?^ za;$y)`txtw*XOfz^We_P6}(2}?$NtFrBb_dTu?3$KO;x0>6OCSM0RRA&k8z;3 zQq1L0`>uKTW&Y@eI5lymxfH3-e|ZXCCz>n9R9bPk*M0Kj$=2R$*RFLb)KVN;%Xe+| z)~XM})EH^)iA47OGi6;PZs^|Isnu**scYsza4 z+>S;rMH*6|-u*;3}i8}iu`+E-IsfBCjzZ%C20 zJgax?4mzohqh$S#W$f?Oem^GPweo5Y;pS4JrQ@urm-K3d9atIp3yE=3xvK!uJn>j>DZT$TE0WQ*pg)( zdUnoj1bhmwR(;4-e{0o;e-LiWQ==jkv@Ns|gD+2|o$pdfT@tV+C{X~IM6Ss%EO;tmR_cSqJVS~!HeCr~)s~jJJqE3hsJPF*+Db8(D(zFBWA1tD zaZ!{g000a+)ZEvviBFNno)=8lMG_jjkBx!VDoEe#TtSp*EB3ZxM{cy0?^5r>oGh_S zmtIdw#A>IMNe(4CBQ{U5_M$`q0ATTy>L~vYN|=VtKH%^b00000NkvXXu0mjfXGE=o diff --git a/public/offline-bday-sprite@2x.png b/public/offline-bday-sprite@2x.png index 8dcd1c26052c9c7ee88aeaa3f5ce9fb294dff4b9..a0e72a2312899745eb7143175f4723c41959cd56 100644 GIT binary patch literal 5645 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 bvm9);BR7Y#EhG-S)d4UID`@=%?_2)^?H5JX diff --git a/styles/globals.css b/styles/globals.css index 2825f30..8e05b5d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -5,6 +5,7 @@ html { body { font-family: system-ui, sans-serif; + margin: 0; --google-blue-100: rgb(210, 227, 252); --google-blue-300: rgb(138, 180, 248); --google-blue-600: rgb(26, 115, 232);