feat(settings/user/message): implement
This commit is contained in:
@@ -8,6 +8,7 @@ import UserMenu from "./User/Menu.vue";
|
|||||||
import UserColor from "./User/Color.vue";
|
import UserColor from "./User/Color.vue";
|
||||||
import UserBirthday from "./User/Birthday.vue";
|
import UserBirthday from "./User/Birthday.vue";
|
||||||
import UserUserName from "./User/UserName.vue";
|
import UserUserName from "./User/UserName.vue";
|
||||||
|
import UserPersonalMessage from "./User/PersonalMessage.vue";
|
||||||
|
|
||||||
import ClockMenu from "./Clock/Menu.vue";
|
import ClockMenu from "./Clock/Menu.vue";
|
||||||
import ClockDate from "./Clock/Date.vue";
|
import ClockDate from "./Clock/Date.vue";
|
||||||
@@ -173,6 +174,7 @@ const viewComponents: Record<string, Component> = {
|
|||||||
userColor: UserColor,
|
userColor: UserColor,
|
||||||
userBirthday: UserBirthday,
|
userBirthday: UserBirthday,
|
||||||
userUserName: UserUserName,
|
userUserName: UserUserName,
|
||||||
|
userMessage: UserPersonalMessage,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { useIntervalFn, useLocalStorage } from "@vueuse/core";
|
||||||
|
|
||||||
|
const store = useSettingsStore();
|
||||||
|
const confirmationModal = useConfirmationModal();
|
||||||
|
|
||||||
|
const { onRender } = useScreen();
|
||||||
|
const { assets: atlas } = useAssets();
|
||||||
|
const assets = atlas.images.settings.bottomScreen;
|
||||||
|
|
||||||
|
const highScore = useLocalStorage("snake_high_score", 0);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
switch (state.value) {
|
||||||
|
case "alive": {
|
||||||
|
state.value = "pause";
|
||||||
|
// TODO: onClosed should as "choice" as a parameter, like "confirmed" or "canceled"
|
||||||
|
let quit = false;
|
||||||
|
confirmationModal.open({
|
||||||
|
text: $t("settings.user.personalMessage.quitConfirmation"),
|
||||||
|
onConfirm: () => {
|
||||||
|
quit = true;
|
||||||
|
},
|
||||||
|
onClosed: () => {
|
||||||
|
if (quit) store.closeSubMenu();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
state.value = "alive";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "dead":
|
||||||
|
case "waiting": {
|
||||||
|
store.closeSubMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
switch (state.value) {
|
||||||
|
case "alive": {
|
||||||
|
state.value = "pause";
|
||||||
|
confirmationModal.open({
|
||||||
|
text: $t("settings.user.personalMessage.restartConfirmation"),
|
||||||
|
onConfirm: () => {
|
||||||
|
spawn();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
state.value = "alive";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "waiting":
|
||||||
|
case "dead": {
|
||||||
|
spawn();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const BOARD_X = 15;
|
||||||
|
const BOARD_Y = 48;
|
||||||
|
const BOARD_WIDTH = 13;
|
||||||
|
const BOARD_HEIGHT = 7;
|
||||||
|
const CELL_SIZE = 15;
|
||||||
|
const CELL_PADDING = 1;
|
||||||
|
|
||||||
|
const position = new THREE.Vector2();
|
||||||
|
const direction = new THREE.Vector2();
|
||||||
|
const nextDirection = new THREE.Vector2();
|
||||||
|
const tail: THREE.Vector2[] = [];
|
||||||
|
const food = new THREE.Vector2();
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
const state = ref<"waiting" | "alive" | "pause" | "dead">("waiting");
|
||||||
|
|
||||||
|
const randomFoodPos = (): THREE.Vector2 => {
|
||||||
|
// can't spawn on the head or tail
|
||||||
|
const occupiedPositions = [position, ...tail];
|
||||||
|
const emptyPositions: THREE.Vector2[] = [];
|
||||||
|
|
||||||
|
for (let x = 0; x < BOARD_WIDTH; x++) {
|
||||||
|
for (let y = 0; y < BOARD_HEIGHT; y++) {
|
||||||
|
const candidate = new THREE.Vector2(x, y);
|
||||||
|
if (!occupiedPositions.find((part) => part.equals(candidate))) {
|
||||||
|
emptyPositions.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyPositions[THREE.MathUtils.randInt(0, emptyPositions.length - 1)]!;
|
||||||
|
};
|
||||||
|
|
||||||
|
const eat = () => {
|
||||||
|
highScore.value = Math.max(highScore.value, score);
|
||||||
|
food.copy(randomFoodPos());
|
||||||
|
score += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const die = () => {
|
||||||
|
state.value = "dead";
|
||||||
|
};
|
||||||
|
|
||||||
|
const spawn = () => {
|
||||||
|
state.value = "alive";
|
||||||
|
score = 0;
|
||||||
|
tail.length = 0;
|
||||||
|
|
||||||
|
position.set(0, 0);
|
||||||
|
direction.set(1, 0);
|
||||||
|
nextDirection.set(1, 0);
|
||||||
|
food.copy(randomFoodPos());
|
||||||
|
};
|
||||||
|
|
||||||
|
useIntervalFn(() => {
|
||||||
|
if (state.value !== "alive") return;
|
||||||
|
|
||||||
|
direction.copy(nextDirection);
|
||||||
|
|
||||||
|
const previousPosition = position.clone();
|
||||||
|
position.set(
|
||||||
|
(position.x + direction.x + BOARD_WIDTH) % BOARD_WIDTH,
|
||||||
|
(position.y + direction.y + BOARD_HEIGHT) % BOARD_HEIGHT,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!position.equals(previousPosition)) {
|
||||||
|
if (tail.find((part) => part.equals(position))) {
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
tail.push(previousPosition);
|
||||||
|
if (food.equals(position)) {
|
||||||
|
eat();
|
||||||
|
} else {
|
||||||
|
tail.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const cellToBoardPos = (pos: THREE.Vector2) => ({
|
||||||
|
x: BOARD_X + CELL_SIZE + 1 + pos.x * (CELL_SIZE + CELL_PADDING),
|
||||||
|
y: BOARD_Y + pos.y * (CELL_SIZE + CELL_PADDING),
|
||||||
|
});
|
||||||
|
|
||||||
|
onRender((ctx) => {
|
||||||
|
assets.background.draw(ctx, 0, 0);
|
||||||
|
|
||||||
|
// score
|
||||||
|
assets.user.snakeScore.draw(ctx, 27, 32);
|
||||||
|
ctx.textBaseline = "top";
|
||||||
|
ctx.font = "10px NDS10";
|
||||||
|
ctx.fillStyle = "#282828";
|
||||||
|
fillTextHCentered(
|
||||||
|
ctx,
|
||||||
|
$t("settings.user.personalMessage.score", { score }),
|
||||||
|
27,
|
||||||
|
36,
|
||||||
|
108,
|
||||||
|
);
|
||||||
|
fillTextHCentered(
|
||||||
|
ctx,
|
||||||
|
$t("settings.user.personalMessage.best", { best: highScore.value }),
|
||||||
|
135,
|
||||||
|
36,
|
||||||
|
108,
|
||||||
|
);
|
||||||
|
|
||||||
|
// board
|
||||||
|
assets.user.snakeBoard.draw(ctx, 15, 48);
|
||||||
|
|
||||||
|
if (state.value === "waiting") {
|
||||||
|
ctx.fillStyle = "#fbfbfb";
|
||||||
|
const text = `\n\n\n ${$t("settings.user.personalMessage.startPrompt")}`;
|
||||||
|
let x = 15;
|
||||||
|
let y = 52;
|
||||||
|
for (let i = 0; i < text.length; i += 1, x += 16) {
|
||||||
|
while (text[i] === "\n") {
|
||||||
|
x = 15;
|
||||||
|
y += 16;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
ctx.fillText(text[i]!, x + 20, y);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// food
|
||||||
|
ctx.fillStyle = "#ff2020";
|
||||||
|
ctx.fillRect(
|
||||||
|
cellToBoardPos(food).x + 1,
|
||||||
|
cellToBoardPos(food).y + 1,
|
||||||
|
CELL_SIZE,
|
||||||
|
CELL_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// snake
|
||||||
|
for (const part of tail) {
|
||||||
|
ctx.fillStyle = state.value === "dead" ? "#991010" : "#20ff20";
|
||||||
|
ctx.fillRect(
|
||||||
|
cellToBoardPos(part).x + 1,
|
||||||
|
cellToBoardPos(part).y + 1,
|
||||||
|
CELL_SIZE,
|
||||||
|
CELL_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assets.user.snakeHead.draw(
|
||||||
|
ctx,
|
||||||
|
cellToBoardPos(position).x + 1,
|
||||||
|
cellToBoardPos(position).y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyDown((key) => {
|
||||||
|
if (state.value !== "alive") return;
|
||||||
|
|
||||||
|
const newDirection = direction.clone();
|
||||||
|
switch (key) {
|
||||||
|
case "NDS_UP":
|
||||||
|
newDirection.set(0, -1);
|
||||||
|
break;
|
||||||
|
case "NDS_RIGHT":
|
||||||
|
newDirection.set(1, 0);
|
||||||
|
break;
|
||||||
|
case "NDS_DOWN":
|
||||||
|
newDirection.set(0, 1);
|
||||||
|
break;
|
||||||
|
case "NDS_LEFT":
|
||||||
|
newDirection.set(-1, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDirection.clone().dot(direction) === 0) {
|
||||||
|
nextDirection.copy(newDirection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineOptions({ render: () => null });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CommonButtons
|
||||||
|
:y-offset="confirmationModal.buttonsYOffset"
|
||||||
|
b-label="Quit"
|
||||||
|
:a-label="state === 'waiting' ? 'Start' : 'Restart'"
|
||||||
|
@activate-b="handleCancel"
|
||||||
|
@activate-a="handleConfirm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user