feat(settings/user/snake): display score in top bar, and implement intro and outro

This commit is contained in:
2026-02-06 15:38:41 +01:00
parent 7baf63f30a
commit 6db8c9e230
3 changed files with 99 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import * as THREE from "three";
import { useIntervalFn, useLocalStorage } from "@vueuse/core";
import gsap from "gsap";
const store = useSettingsStore();
const achievements = useAchievementsStore();
@@ -12,15 +13,84 @@ const assets = atlas.images.settings.bottomScreen;
const highScore = useLocalStorage("snake_high_score", 0);
const handleCancel = () => {
const BOARD_SLIDE_OFFSET = 96;
const BOARD_SLIDE_DURATION = 0.25;
const TEXT_FADE_DURATION = 0.15;
const SCORE_OFFSET = -20;
const SCORE_DURATION = 0.15;
const intro = reactive({
boardOffsetY: BOARD_SLIDE_OFFSET,
boardOpacity: 0,
textOpacity: 0,
scoreOffsetY: SCORE_OFFSET,
});
const animateIntro = async () => {
await gsap
.timeline()
.to(
intro,
{ boardOffsetY: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
0,
)
.to(
intro,
{ boardOpacity: 1, duration: BOARD_SLIDE_DURATION, ease: "none" },
0,
)
.to(
intro,
{ textOpacity: 1, duration: TEXT_FADE_DURATION, ease: "none" },
BOARD_SLIDE_DURATION,
)
.to(
intro,
{ scoreOffsetY: 0, duration: SCORE_DURATION, ease: "none" },
BOARD_SLIDE_DURATION + TEXT_FADE_DURATION,
);
};
const animateOutro = async () => {
await gsap
.timeline()
.to(
intro,
{
boardOffsetY: BOARD_SLIDE_OFFSET,
duration: BOARD_SLIDE_DURATION,
ease: "none",
},
0,
)
.to(
intro,
{ boardOpacity: 0, duration: BOARD_SLIDE_DURATION, ease: "none" },
0,
)
.to(
intro,
{ scoreOffsetY: SCORE_OFFSET, duration: SCORE_DURATION, ease: "none" },
0,
);
};
onMounted(() => {
animateIntro();
});
const handleCancel = async () => {
switch (state.value) {
case "alive": {
state.value = "pause";
confirmationModal.open({
text: $t("settings.user.snake.quitConfirmation"),
onConfirm: () => {},
onClosed: (choice) => {
if (choice === "confirm") store.closeSubMenu();
onClosed: async (choice) => {
if (choice === "confirm") {
await animateOutro();
store.closeSubMenu();
}
},
onCancel: () => {
state.value = "alive";
@@ -31,6 +101,7 @@ const handleCancel = () => {
case "dead":
case "waiting": {
await animateOutro();
store.closeSubMenu();
break;
}
@@ -65,9 +136,9 @@ const handleConfirm = () => {
};
const BOARD_X = 15;
const BOARD_Y = 48;
const BOARD_Y = 33;
const BOARD_WIDTH = 13;
const BOARD_HEIGHT = 7;
const BOARD_HEIGHT = 8;
const CELL_SIZE = 15;
const CELL_PADDING = 1;
@@ -154,34 +225,22 @@ const cellToBoardPos = (pos: THREE.Vector2) => ({
onRender((ctx) => {
assets.background.draw(ctx, 0, 0);
// score
assets.user.snakeScore.draw(ctx, 27, 32);
ctx.save();
ctx.globalAlpha = intro.boardOpacity;
ctx.translate(0, intro.boardOffsetY);
ctx.textBaseline = "top";
ctx.font = "10px NDS10";
ctx.fillStyle = "#282828";
fillTextHCentered(
ctx,
$t("settings.user.snake.score", { score }),
27,
36,
108,
);
fillTextHCentered(
ctx,
$t("settings.user.snake.best", { best: highScore.value }),
135,
36,
108,
);
// board
assets.user.snakeBoard.draw(ctx, 15, 48);
assets.user.snakeBoard.draw(ctx, BOARD_X, BOARD_Y);
if (state.value === "waiting") {
ctx.globalAlpha = intro.textOpacity;
ctx.fillStyle = "#fbfbfb";
const text = `\n\n\n ${$t("settings.user.snake.startPrompt")}`;
const text = $t("settings.user.snake.startPrompt");
let x = 15;
let y = 52;
let y = 37;
for (let i = 0; i < text.length; i += 1, x += 16) {
while (text[i] === "\n") {
x = 15;
@@ -216,8 +275,22 @@ onRender((ctx) => {
cellToBoardPos(position).y,
);
}
ctx.restore();
});
onRender((ctx) => {
ctx.translate(0, intro.scoreOffsetY);
drawButton(ctx, $t("settings.user.snake.score", { score }), 10, 2, 118);
drawButton(
ctx,
$t("settings.user.snake.best", { best: highScore.value }),
138,
2,
108,
);
}, 110);
useKeyDown((key) => {
if (state.value !== "alive") return;