584 lines
16 KiB
Vue
584 lines
16 KiB
Vue
<script setup lang="ts">
|
|
import LoadingScreen from "~/components/game/overlay/LoadingScreen.vue";
|
|
import { endTurn, subscribe, useToken, startGame, joinGame } from "~/netcode";
|
|
import { delay } from "~/netcode/events";
|
|
import {
|
|
type Container,
|
|
type Effect,
|
|
type Game,
|
|
type Player,
|
|
} from "~/netcode/interfaces";
|
|
import { playSound } from "~/helpers/audioHelpers";
|
|
|
|
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]);
|
|
}
|
|
}
|
|
|
|
const handlers = {
|
|
start: async (data: any, game: Game) => {
|
|
game.started = true;
|
|
game.currentTurn = data.game.currentTurn;
|
|
},
|
|
populateContainer: async (data: any) => {
|
|
const fromContainer = containers.value[data.container._id as string];
|
|
playSound("/sounds/distribute.mp3");
|
|
for (const card of data.container.cards) {
|
|
fromContainer.cards.push(card);
|
|
// if (!settingsStore.fastAnimations) {
|
|
// await delay(settingsStore.animationSpeed);
|
|
// }
|
|
}
|
|
},
|
|
shuffleContainer: async (data: any) => {
|
|
playSound("/sounds/shuffle.mp3");
|
|
const container = containers.value[data.container as string];
|
|
container.shuffling = true;
|
|
await delay(settingsStore.animationSpeed * 2);
|
|
container.shuffling = false;
|
|
await delay(settingsStore.animationSpeed * 2);
|
|
},
|
|
distributeCards: async (data: any) => {
|
|
const fromContainer = containers.value[data.from as string];
|
|
const toContainer = containers.value[data.to as string];
|
|
if (settingsStore.fastAnimations) {
|
|
playSound("/sounds/distribute.mp3");
|
|
}
|
|
for (const card of data.cards) {
|
|
const index = fromContainer.cards.findIndex(
|
|
(c) => !c || c._id == card._id
|
|
);
|
|
fromContainer.cards.splice(index, 1);
|
|
if (!settingsStore.fastAnimations) {
|
|
await delay(settingsStore.animationSpeed);
|
|
playSound("/sounds/distribute.mp3");
|
|
}
|
|
toContainer.cards.push(card);
|
|
if (!settingsStore.fastAnimations) {
|
|
await delay(settingsStore.animationSpeed);
|
|
}
|
|
}
|
|
await delay(settingsStore.animationSpeed * 2);
|
|
},
|
|
endTurn: async (data: any, game: Game) => {
|
|
game.currentTurn = data.currentTurn;
|
|
await delay(settingsStore.animationSpeed);
|
|
const playingPlayer = game.teams.find(
|
|
(t) => t._id == game.currentTurn.currentTeam
|
|
)?.players[0];
|
|
if (playingPlayer) {
|
|
focusPlayer(playingPlayer);
|
|
await delay(settingsStore.animationSpeed);
|
|
}
|
|
},
|
|
lockCard: async (data: any, game: Game) => {
|
|
playSound("/sounds/lock.mp3");
|
|
game.currentTurn.cardsLocked.push(data.card);
|
|
await delay(settingsStore.animationSpeed);
|
|
},
|
|
destroyCard: async (data: any) => {
|
|
const fromContainer = containers.value[data.from as string];
|
|
const index = fromContainer.cards.findIndex(
|
|
(c) => !c || c._id == data.card
|
|
);
|
|
fromContainer.cards.splice(index, 1);
|
|
playSound("/sounds/destroy.mp3");
|
|
await delay(settingsStore.animationSpeed);
|
|
},
|
|
joinGame: async (data: any, game: Game) => {
|
|
playSound("/sounds/teleport.mp3");
|
|
game.teams.push(data.team);
|
|
},
|
|
lockEffect: async (data: any, game: Game) => {
|
|
game.currentTurn.lockedEffects.push(data.effect);
|
|
await delay(settingsStore.animationSpeed);
|
|
},
|
|
unlockEffect: async (data: any, game: Game) => {
|
|
const effectIndex = game.currentTurn.lockedEffects.indexOf(data.effect);
|
|
game.currentTurn.lockedEffects.splice(effectIndex, 1);
|
|
await delay(settingsStore.animationSpeed);
|
|
},
|
|
"currentTurn.updateGold": async (data: any, game: Game) => {
|
|
console.log(`adding gold amount : ` + data.operation);
|
|
game.currentTurn.gold = data.gold;
|
|
},
|
|
"currentTurn.updateDamage": async (data: any, game: Game) => {
|
|
console.log(`adding damage amount : ` + data.operation);
|
|
game.currentTurn.damage = data.damage;
|
|
},
|
|
"currentTurn.addToken": async (data: any, game: Game) => {
|
|
game.currentTurn.tokens.push(data.token);
|
|
await delay(settingsStore.animationSpeed);
|
|
},
|
|
"currentTurn.useToken": async (data: any, game: Game) => {
|
|
const index = game.currentTurn.tokens.findIndex(
|
|
(t) => t._id == data.tokenId
|
|
);
|
|
game.currentTurn.tokens.splice(index, 1);
|
|
if (!settingsStore.fastAnimations) {
|
|
await delay(settingsStore.animationSpeed);
|
|
}
|
|
},
|
|
"team.updateHealth": async (data: any, game: Game) => {
|
|
const team = game.teams.find((t) => t._id == data.team);
|
|
if (!team) {
|
|
throw new Error("Team not found");
|
|
}
|
|
console.log(`adding health amount : ` + data.operation);
|
|
team.health = data.health;
|
|
},
|
|
"player.addFuckUMarker": async (data: any, game: Game) => {
|
|
console.log(`adding fuckUMarkers : ` + data.operation);
|
|
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
|
if (!targetPlayer) {
|
|
throw new Error("Cannot add fuckU Markers : player not found");
|
|
}
|
|
targetPlayer.fuckUMarkers = data.fuckUMarkers;
|
|
},
|
|
"player.removeFuckUMarker": async (data: any, game: Game) => {
|
|
console.log(`adding fuckUMarkers : ` + data.operation);
|
|
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
|
if (!targetPlayer) {
|
|
throw new Error("Cannot remove fuckU Markers : player not found");
|
|
}
|
|
targetPlayer.fuckUMarkers = data.fuckUMarkers;
|
|
},
|
|
"player.addDiscardMarker": async (data: any, game: Game) => {
|
|
console.log(`adding discardMarkers : ` + data.operation);
|
|
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
|
if (!targetPlayer) {
|
|
throw new Error("Cannot add discard Markers : player not found");
|
|
}
|
|
targetPlayer.discardMarkers = data.discardMarkers;
|
|
},
|
|
"player.removeDiscardMarker": async (data: any, game: Game) => {
|
|
console.log(`adding fuckUMarkers : ` + data.operation);
|
|
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
|
if (!targetPlayer) {
|
|
throw new Error("Cannot remove discard Markers : player not found");
|
|
}
|
|
targetPlayer.discardMarkers = data.discardMarkers;
|
|
},
|
|
killTeam: async (data: any, game: Game) => {
|
|
const index = game.teams.findIndex((t) => t._id == data.teamId);
|
|
if (!index) {
|
|
console.error("team index not found. Desyncs might happen");
|
|
await refresh();
|
|
return;
|
|
}
|
|
game.teams.splice(index, 1);
|
|
},
|
|
consumeEffect: async (data: any) => {
|
|
console.warn("consumeEffect not implemented");
|
|
},
|
|
endGame: async () => {
|
|
alert("game ended");
|
|
},
|
|
};
|
|
|
|
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, current_game);
|
|
} 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>
|
|
<GameOverlayResultDialog
|
|
:isVisible="showResults"
|
|
:won="isWinner"
|
|
:players="playerList"
|
|
@close="showResults = false"
|
|
>
|
|
</GameOverlayResultDialog>
|
|
<GamePlayerSelf
|
|
v-if="selfPlayer"
|
|
:container="selfPlayer.hand"
|
|
></GamePlayerSelf>
|
|
</template>
|
|
</ClientOnly>
|
|
|
|
<!-- <LoadingScreen v-if="!loaded"></LoadingScreen> -->
|
|
<footer>
|
|
<GameOverlayOptionsDialog></GameOverlayOptionsDialog>
|
|
<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">
|
|
<button
|
|
class="end_turn_button turn_icon"
|
|
@click="startGame(game._id)"
|
|
v-if="game.owner.userId == authStore.selfUser.userId"
|
|
>
|
|
Start game
|
|
</button>
|
|
<button
|
|
class="end_turn_button turn_icon"
|
|
@click="joinGame(game._id)"
|
|
v-else-if="
|
|
playerList?.every(
|
|
(p) => p.user.userId != authStore.selfUser.userId
|
|
)
|
|
"
|
|
>
|
|
Join game
|
|
</button>
|
|
<button v-else>Leave game</button>
|
|
</template>
|
|
</div>
|
|
</ClientOnly>
|
|
|
|
<button @click="navigateTo('/lobby')">Return to lobby</button>
|
|
</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>
|