Files
front/pages/game.vue
T
legonzaur e1784dffca
release-tag / release-image (push) Successful in 47s
fix : render game configuration button only on client
2024-12-21 14:56:49 +01:00

405 lines
9.8 KiB
Vue

<script setup lang="ts">
import LoadingScreen from "~/components/game/overlay/LoadingScreen.vue";
import {
endTurn,
subscribe,
useToken,
startGame,
joinGame,
deleteGame,
leaveGame,
surrender,
} from "~/netcode";
import { delay } from "~/netcode/events";
import {
type Container,
type Effect,
type Game,
type Player,
} from "~/netcode/interfaces";
import { handlers } from "~/netcode/events";
const config = useRuntimeConfig();
const settingsStore = useSettingsStore();
const authStore = useAuthStore();
const route = useRoute();
const gameId = route.query.id;
const {
data: game,
pending,
error,
refresh,
execute,
} = await useFetch<Game>(config.public.endpoint + `/games/${gameId}`, {
credentials: "include",
server: false,
immediate: true,
});
await execute();
const containers = computed<Record<string, Container>>(() => ({
[game.value!.fireGems._id]: game.value!.fireGems,
[game.value!.market._id]: game.value!.market,
[game.value!.marketStack._id]: game.value!.marketStack,
...Object.fromEntries(
game
.value!.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
.map((p) => [
[p.board._id, p.board],
[p.discard._id, p.discard],
[p.hand._id, p.hand],
[p.stack._id, p.stack],
])
.flat(1)
),
}));
let eventSource: EventSource;
const actions = ref<(() => Promise<any>)[]>([]);
function queueAction(callback: () => Promise<any>) {
actions.value.push(callback);
if (actions.value.length == 1) {
execAction(callback);
}
}
async function execAction(callback: () => Promise<any>) {
await callback();
actions.value.shift();
if (actions.value.length > 0) {
execAction(actions.value[0]);
}
}
onBeforeMount(async () => {
if (game.value == null) {
navigateTo("/lobby");
throw new Error("game not found");
}
if (game.value.started) {
focusedPlayer.value = game.value.teams.find(
(t) => t._id == game.value?.currentTurn.currentTeam
)?.players[0];
} else {
focusedPlayer.value = selfPlayer.value ?? playerList.value![0];
}
eventSource = subscribe(`/games/${gameId}/subscribe`);
eventSource.addEventListener("message", async (message) => {
const current_game = game.value;
if (!current_game) {
throw new Error("game not found");
}
const data = JSON.parse(message.data);
queueAction(async () => {
if (data.type in handlers) {
await handlers[data.type as keyof typeof handlers](data, {
game: current_game,
containers,
playerList,
refresh,
focusPlayer,
settingsStore,
});
} else {
console.error(data.type + " not implemented");
}
});
});
});
const playerList = computed(
() =>
(game.value &&
game.value.teams.reduce(
(acc, t) => acc.concat(t.players),
[] as Player[]
)) ??
[]
);
const selfPlayer = computed(() =>
authStore.logged
? playerList.value?.find((p) => p.user.userId == authStore.selfUser.userId)
: undefined
);
const isTurn = computed(
() =>
(game.value &&
selfPlayer.value &&
game.value.teams.find((t) => t.players.includes(selfPlayer.value!))
?._id == game.value.currentTurn.currentTeam) ??
false
);
const turnChangeAnimationSpeed = computed(
() => settingsStore.animationSpeed * 2 + "ms"
);
// TODO : current turn sound
// TODO : winner screen
// TODO : join sound
const showResults = ref(false);
const isWinner = ref(false);
const loaded = ref(false);
const discard_unwrapped = ref(false);
const focusedPlayer = ref<Player | undefined>(undefined);
function showLoserScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true;
isWinner.value = false;
}
function showWinnerScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true;
isWinner.value = true;
}
function focusPlayer(p: Player) {
focusedPlayer.value = p;
}
function useTokenClick(t: Effect) {
console.log(t);
useToken(game.value!._id, t._id);
}
function showDiscard() {
discard_unwrapped.value = true;
}
onUnmounted(() => {
if (eventSource) {
eventSource.close();
}
});
</script>
<template>
<div @contextmenu.prevent="false" id="game_board">
<AnimatedBackground @ready="loaded = true" :isTurn="isTurn ?? false"></AnimatedBackground>
<ClientOnly>
<template v-if="game && focusedPlayer && playerList">
<!-- <Transition name="slide-left" mode="out-in"> -->
<div id="stack_discard" :key="focusedPlayer?._id">
<div id="stack">
<GameCardGrouped :container="focusedPlayer.stack" :wrapped="true" :hidden="true">
</GameCardGrouped>
</div>
<GameOverlayDiscard :game="game" :player="focusedPlayer"></GameOverlayDiscard>
</div>
<!-- </Transition> -->
<GameMarket :game="game" :market="game.market" :fire_gems="game.fireGems" :marketStack="game.marketStack">
</GameMarket>
<div>
<GameToken :token="t" v-for="t in game.currentTurn.tokens" @useToken="useTokenClick"></GameToken>
</div>
<div id="player_list_container">
<div id="player_list">
<GamePlayerListElement v-for="p in playerList" :player="p" :game="game" @click="focusPlayer(p)"
:focused="focusedPlayer == p" :is-self-turn="isTurn" :self="selfPlayer == p">
</GamePlayerListElement>
</div>
</div>
<div id="current_player">
<Transition name="slide-left" mode="out-in">
<GamePlayerSlot v-if="focusedPlayer" :player="focusedPlayer" :game="game" :key="focusedPlayer._id">
</GamePlayerSlot>
</Transition>
</div>
<GameModalResultDialog :isVisible="showResults" :won="isWinner" :players="playerList"
@close="showResults = false">
</GameModalResultDialog>
<GamePlayerSelf v-if="selfPlayer" :container="selfPlayer.hand"></GamePlayerSelf>
</template>
</ClientOnly>
<!-- <LoadingScreen v-if="!loaded"></LoadingScreen> -->
<footer>
<div>
<GameModalOptionsDialog></GameModalOptionsDialog>
<ClientOnly>
<GameModalConfigureDialog v-if="game && game.owner.userId == authStore.selfUser.userId" :game="game">
</GameModalConfigureDialog>
</ClientOnly>
</div>
<ClientOnly>
<div id="turn_buttons" v-if="game">
<template v-if="game.started">
<button id="end_turn" class="end_turn_button turn_icon" :class="{ hidden: !isTurn }"
@click="endTurn(game._id)" v-if="isTurn">
END TURN
</button>
</template>
<template v-else-if="authStore.logged">
<template v-if="game.owner.userId == authStore.selfUser.userId">
<button class="end_turn_button turn_icon" @click="startGame(game._id)">
Start game
</button>
</template>
<button class="end_turn_button turn_icon" @click="joinGame(game._id)" v-else-if="!selfPlayer">
Join game
</button>
<button v-else @click="leaveGame(game._id)">Leave game</button>
</template>
</div>
<div>
<button v-if="game && game.started && selfPlayer" class="red" @click="surrender(game._id)">
Surrender
</button>
<button @click="navigateTo('/lobby')">Return to lobby</button>
</div>
</ClientOnly>
</footer>
</div>
</template>
<style scoped>
footer {
margin: 10px;
display: flex;
flex-direction: row;
grid-area: footer;
height: 50px;
/* background: red; */
justify-content: space-between;
}
#game_status {
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.75);
}
#game_board {
display: grid;
grid-template-columns: 1fr min-content 1fr;
grid-template-rows: min-content min-content 1fr min-content;
grid-template-areas:
"stack_discard market turn_stats"
"player_list player_list player_list"
"current_player current_player current_player"
"footer footer footer";
height: 100%;
}
#player_list {
display: flex;
flex-direction: inherit;
justify-content: center;
}
#current_player {
grid-area: current_player;
display: flex;
flex-direction: column;
justify-content: center;
overflow-x: clip;
/* background: url('/img/bg-darkgrass.png'); */
}
#stack_discard {
grid-area: stack_discard;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
#stack,
#discard {
display: flex;
flex-direction: row;
}
#discard {
cursor: pointer;
}
.discard_overlay {
z-index: 9;
display: flex;
/* background: blue; */
pointer-events: none;
gap: 2px;
flex-wrap: wrap-reverse;
align-content: flex-end;
cursor: default;
position: absolute;
padding-bottom: inherit;
padding-left: 40px;
height: calc(100% - 64px);
padding-top: 64px;
width: calc(100% -40px);
margin-left: 0;
}
#discard_unwrapped_bg {
display: none;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 8;
background: rgba(0, 0, 0, 0.5);
pointer-events: all;
padding: inherit;
}
#discard_unwrapped_bg.unwrapped {
display: block;
}
#market {
grid-area: market;
display: flex;
flex-direction: row;
justify-content: center;
}
#player_list_container {
grid-area: player_list;
display: flex;
flex-direction: row;
justify-content: center;
/* background-image: url("/img/bg-darkgrass.png"); */
}
#player_list {
/* border: 5px solid #000; */
display: flex;
}
.slide-left-enter-active,
.slide-left-leave-active {
transition: all v-bind("settingsStore.animationSpeed * 2 + 'ms'") ease-out;
}
.slide-left-enter-from {
opacity: 0;
transform: translateX(100%);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
}
</style>