From f9d93e67212b78a51c4698d54b949a803150d35d Mon Sep 17 00:00:00 2001 From: keguigong Date: Thu, 14 Dec 2023 22:18:01 +0800 Subject: [PATCH] Add "Press space to play" text when not activated --- game/HPBar.ts | 9 ++- game/Horizon.ts | 6 +- game/Mountain.ts | 6 +- game/PressToStart.ts | 145 ++++++++++++++++++++++++++++++++++++++++ game/Runner.ts | 69 +++++++++++-------- game/Trex.ts | 4 +- pages/index.tsx | 20 +++++- public/sprite-zh@1x.png | Bin 6328 -> 6852 bytes public/sprite-zh@2x.png | Bin 10310 -> 11067 bytes styles/globals.css | 67 ++++++++++++++++++- 10 files changed, 283 insertions(+), 43 deletions(-) create mode 100644 game/PressToStart.ts diff --git a/game/HPBar.ts b/game/HPBar.ts index 0003a39..ff1470a 100644 --- a/game/HPBar.ts +++ b/game/HPBar.ts @@ -16,6 +16,7 @@ export default class HPBar { flashIterations = 0 opacity = 0 + enterTimer = 0 constructor(canvas: HTMLCanvasElement, spritePos: Position, canvasWidth: number) { this.canvas = canvas @@ -26,7 +27,10 @@ export default class HPBar { update(deltaTime: number, hp: number) { let paint = true - if (this.opacity < 1) { + + if (this.enterTimer < HPBar.config.ENTER_DELAY) { + this.enterTimer += deltaTime + } else if (this.opacity < 1) { this.opacity = Math.min(this.opacity + HPBar.config.FADE_SPEED, 1) } @@ -119,7 +123,8 @@ export default class HPBar { FLASH_ITERATIONS: 3, // 闪动的次数 MAX_HP: 3, HP_UNIT: 1, - FADE_SPEED: 0.035 // 淡入淡出的速度 + FADE_SPEED: 0.035, // 淡入淡出的速度, + ENTER_DELAY: 1000 // 显示延时 } static dimensions = { diff --git a/game/Horizon.ts b/game/Horizon.ts index a68dd89..174d7bf 100644 --- a/game/Horizon.ts +++ b/game/Horizon.ts @@ -36,17 +36,17 @@ export default class Horizon { } private init() { - this.addCloud() + this.mountain = new Mountain(this.canvas, Runner.bdaySpriteDefinition.MOUNTAIN, this.dimensions.WIDTH) 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) + this.addCloud() } update(deltaTime: number, speed: number, hasObstacles?: boolean, showNightMode: boolean = false) { + this.mountain.update(deltaTime, speed) this.horizonLine.update(deltaTime, speed) this.nightMode.update(showNightMode) this.updateCloud(deltaTime, speed) - this.mountain.update() if (hasObstacles) { this.updateObstacles(deltaTime, speed) } diff --git a/game/Mountain.ts b/game/Mountain.ts index e2458a6..a959e20 100644 --- a/game/Mountain.ts +++ b/game/Mountain.ts @@ -56,8 +56,8 @@ export default class Mountain { this.ctx.restore() } - update() { - this.xPos = this.updateXPos(this.xPos, Mountain.config.MOUNTAIN_SPEED) + update(deltaTime: number, speed: number) { + this.xPos = this.updateXPos(this.xPos, Mountain.config.MOUNTAIN_SPEED * deltaTime) this.draw() } @@ -73,6 +73,6 @@ export default class Mountain { static config = { WIDTH: 413, HEIGHT: 92, - MOUNTAIN_SPEED: 0.1 + MOUNTAIN_SPEED: 0.025 } } diff --git a/game/PressToStart.ts b/game/PressToStart.ts new file mode 100644 index 0000000..e7d6ce2 --- /dev/null +++ b/game/PressToStart.ts @@ -0,0 +1,145 @@ +import Runner from "./Runner" +import { IS_HIDPI } from "./varibles" + +export default class PressToStart { + canvas!: HTMLCanvasElement + ctx!: CanvasRenderingContext2D + canvasDimensions!: Dimensions + textImgPos!: Position + + animTimer = 0 + opacity = 0 + currTextIndex = 1 + fadeIn = true + + constructor(canvas: HTMLCanvasElement, textImgPos: Position, dimensions: Dimensions) { + this.canvas = canvas + this.ctx = canvas.getContext("2d")! + this.canvasDimensions = dimensions + this.textImgPos = textImgPos + } + + drawText(dimensions: any) { + let centerX = this.canvasDimensions.WIDTH / 2 + + let textSourceX = dimensions.OFFSET_X + let textSourceY = dimensions.OFFSET_Y + let textSourceWidth = dimensions.TEXT_WIDTH + let textSourceHeight = dimensions.TEXT_HEIGHT + + const textTargetX = Math.round(centerX - dimensions.TEXT_WIDTH / 2) + const textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3) + const textTargetWidth = dimensions.TEXT_WIDTH + const textTargetHeight = dimensions.TEXT_HEIGHT + + if (IS_HIDPI) { + textSourceX *= 2 + textSourceY *= 2 + textSourceWidth *= 2 + textSourceHeight *= 2 + } + textSourceX += this.textImgPos.x + textSourceY += this.textImgPos.y + + this.ctx.save() + this.ctx.globalAlpha = this.opacity + this.ctx.drawImage( + Runner.imageSprite, + textSourceX, + textSourceY, + textSourceWidth, + textSourceHeight, + textTargetX, + textTargetY, + textTargetWidth, + textTargetHeight + ) + this.ctx.restore() + this.ctx.globalAlpha = 1 + } + + drawEmoji(text: string, fadeOut = false) { + let centerX = this.canvasDimensions.WIDTH / 2 + const textTargetY = 100 + this.ctx.font = "16px system-ui, sans-serif" + this.ctx.textAlign = "center" + this.ctx.textBaseline = "middle" + this.ctx.save() + if (fadeOut) { + this.ctx.globalAlpha = this.opacity + } + this.ctx.fillStyle = "#6c6c6c" + this.ctx.fillText(text, centerX, textTargetY) + this.ctx.restore() + this.ctx.globalAlpha = 1 + } + + /** + * Update animation frames. + */ + update(deltaTime: number, fadeOut = false) { + if (this.opacity === 0 && fadeOut) return + + if (this.opacity === 1) { + this.animTimer += deltaTime + if (this.animTimer > PressToStart.TEXT_PAUSE_DURATION) { + this.fadeIn = false + this.animTimer = 0 + } + } else if (this.opacity === 0 && !this.fadeIn) { + this.fadeIn = true + this.currTextIndex = this.currTextIndex + 1 < PressToStart.TEXT_LIST.length ? this.currTextIndex + 1 : 0 + } + + if (fadeOut && this.opacity > 0) { + this.fadeIn = false + } + + if (this.fadeIn) { + this.opacity = Math.min(1, this.opacity + PressToStart.FADE_SPEED * deltaTime) + } else { + this.opacity = Math.max(0, this.opacity - PressToStart.FADE_SPEED * deltaTime) + } + + let textDimension = PressToStart.TEXT_LIST[this.currTextIndex] + this.drawText(textDimension) + this.drawEmoji("Code with 🖐️ & ❤️", fadeOut) + } + + reset() { + this.animTimer = 0 + } + + static FADE_SPEED = 1 / 500 + static TEXT_PAUSE_DURATION = 875 + static TEXT_LIST = [ + { + REMARK: "jp", + OFFSET_X: 0, + OFFSET_Y: 66, + TEXT_WIDTH: 222, + TEXT_HEIGHT: 14 + }, + { + REMARK: "zh", + OFFSET_X: 0, + OFFSET_Y: 80, + TEXT_WIDTH: 126, + TEXT_HEIGHT: 14 + }, + { + REMARK: "en", + OFFSET_X: 240, + OFFSET_Y: 66, + TEXT_WIDTH: 264, + TEXT_HEIGHT: 14 + }, + { + REMARK: "en", + OFFSET_X: 240, + OFFSET_Y: 80, + TEXT_WIDTH: 278, + TEXT_HEIGHT: 14 + } + ] +} diff --git a/game/Runner.ts b/game/Runner.ts index 50b87f3..1c2b2f3 100644 --- a/game/Runner.ts +++ b/game/Runner.ts @@ -1,5 +1,6 @@ import DistanceMeter from "./DistanceMeter" import GameOverPanel from "./GameOverPanel" +import PressToStart from "./PressToStart" import HPBar from "./HPBar" import Horizon from "./Horizon" import Trex from "./Trex" @@ -11,16 +12,15 @@ const RESOURCE_POSTFIX = "offline-resources-" const BDAY_SPRITE_POSTFIX = "offline-bday-sprite-" export default class Runner { - spriteDef!: SpritePosDef + canvas!: HTMLCanvasElement // Canvas object + ctx!: CanvasRenderingContext2D // Canvas context + spriteDef!: SpritePosDef // Sprite definition outerContainerEl!: HTMLElement containerEl!: HTMLElement - config: ConfigDict = Runner.config - dimensions = Runner.defaultDimensions - - canvas!: HTMLCanvasElement - ctx!: CanvasRenderingContext2D + config: ConfigDict = Runner.config // Default configuration + dimensions = Runner.defaultDimensions // Canvas dimensions used for positioning and scaling time = Date.now() runningTime = 0 @@ -34,26 +34,27 @@ export default class Runner { inverTimer = 0 // Night mode start time invertTrigger = false updatePending = false - resizeTimerId_: NodeJS.Timer | null = null + resizeTimerId_: NodeJS.Timer | null = null // window resizing delay - raqId = 0 + raqId = 0 // requestAnimationFrame + tRex!: Trex // Paint objects horizon!: Horizon - playingIntro!: boolean - - msPerFrame = 1000 / FPS distanceMeter!: DistanceMeter + gameOverPanel!: GameOverPanel + pressToStart!: PressToStart + hpBar!: HPBar + + playingIntro!: boolean // Playing intro when first start + + msPerFrame = 1000 / FPS // ms per frame distanceRan = 0 highestScore = 0 - tRex!: Trex - gameOverPanel!: GameOverPanel - - audioContext!: AudioContext - soundFx: ConfigDict = {} + audioContext!: AudioContext // Collision sound + soundFx: ConfigDict = {} // Sound FX map - hpBar!: HPBar - hp = HPBar.config.MAX_HP + hp = HPBar.config.MAX_HP // HP value constructor(outerContainerId: string, optConfig?: ConfigDict) { this.outerContainerEl = document.querySelector(outerContainerId)! @@ -130,14 +131,12 @@ export default class Runner { Runner.updateCanvasScaling(this.canvas) this.outerContainerEl.appendChild(this.containerEl) - // Load background clas s Horizon + // this.tRex = new Trex(this.canvas, this.spriteDef.TREX) + this.tRex = new Trex(this.canvas, Runner.bdaySpriteDefinition.TREX) 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.pressToStart = new PressToStart(this.canvas, this.spriteDef.TEXT_SPRITE, this.dimensions) 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) this.startListening() this.update() @@ -258,7 +257,7 @@ export default class Runner { } this.collision = !!collision - hasObstacles && this.hpBar.update(deltaTime, this.hp) + this.hpBar.update(deltaTime, this.hp) let playAchievementSound = this.distanceMeter.update(deltaTime, Math.ceil(this.distanceRan)) @@ -292,6 +291,17 @@ export default class Runner { } } + if (!this.activated) { + this.ctx.clearRect(0, 0, this.dimensions.WIDTH, this.dimensions.HEIGHT - 80) + this.tRex.update(deltaTime) + this.pressToStart.update(deltaTime) + + this.scheduleNextUpdate() + return + } else { + this.pressToStart.update(deltaTime, true) + } + if (this.playing || (!this.activated && this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) { this.tRex.update(deltaTime) @@ -321,10 +331,11 @@ export default class Runner { const messageEl = document.querySelector("#main-message") as HTMLDivElement messageEl.style.opacity = "0" - let keyframes = `@-webkit-keyframes intro { - from { width: ${Trex.config.WIDTH}px } - to { width: ${this.dimensions.WIDTH}px } + - }` + // let keyframes = `@-webkit-keyframes intro { + // from { width: ${Trex.config.WIDTH}px } + // to { width: ${this.dimensions.WIDTH}px } + + // }` + let keyframes = `@-webkit-keyframes intro {}` document.styleSheets[0].insertRule(keyframes, 0) this.containerEl.style.webkitAnimation = "intro .4s ease-out 1 both" this.containerEl.style.width = this.dimensions.WIDTH + "px" @@ -714,7 +725,7 @@ export default class Runner { GAMEOVER_CLEAR_TIME: 1200, INITIAL_JUMP_VELOCITY: 12, INVERT_FADE_DURATION: 12000, - MAX_BLINK_COUNT: 3, + MAX_BLINK_COUNT: Infinity, MAX_CLOUDS: 6, MAX_OBSTACLE_LENGTH: 3, MAX_OBSTACLE_DUPLICATION: 2, diff --git a/game/Trex.ts b/game/Trex.ts index 3a4ede5..77efba0 100644 --- a/game/Trex.ts +++ b/game/Trex.ts @@ -282,7 +282,7 @@ export default class Trex { ] } - static BLINK_TIMING = 7000 + static BLINK_TIMING = 2000 static status = { CRASHED: "CRASHED", @@ -295,7 +295,7 @@ export default class Trex { static animFrames: ConfigDict = { WAITING: { frames: [44, 0], - msPerFrame: 1000 / 3 + msPerFrame: 1000 / 5 }, RUNNING: { frames: [88, 132], diff --git a/pages/index.tsx b/pages/index.tsx index 9746950..744bd10 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -27,6 +27,14 @@ export default function Home() {

Press space to play

+
@@ -35,8 +43,16 @@ export default function Home() {
offline-resources-1x offline-resources-2x - offline-bday-sprite-1x - offline-bday-sprite-2x + offline-bday-sprite-1x + offline-bday-sprite-2x