Feat : lobby

This commit is contained in:
2024-06-13 09:46:40 -04:00
parent 7c98da1df8
commit 8a3e2babc9
13 changed files with 375 additions and 441 deletions
-3
View File
@@ -1,3 +0,0 @@
[submodule "netcode/interfaces"]
path = netcode/interfaces
url = git@git.legonzaur.fr:heronfief/ts-interfaces.git
+13 -102
View File
@@ -2,110 +2,15 @@
// import goStore, { oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
const router = useRouter();
// useHead
// ({
// title
// : 'Héron Realms',
// })
// const heron_session = useCookie('heron_session')
// const route = useRoute()
// onMounted(async () => {
// const code = route.query.code
// if (!code) { return }
// if (Array.isArray(code)) { return }
// await setup_websocket()
// oauth2_register_discord(code)
// history.pushState(
// {},
// '',
// '/'
// )
// })
const authStore = useAuthStore()
await authStore.whoami()
// onMounted(async () => {
// if (!heron_session.value) { return }
// heron_cookie_login(heron_session.value)
// })
// function keyDownHandler(e: KeyboardEvent) {
// if (e.repeat) { return }
// console.log(e.code)
// if (e.code != "AltLeft") { return }
// goStore.zoomKeyPressed = true
// e.preventDefault()
// return false
// }
// function keyUpHandler(e: KeyboardEvent) {
// if (e.repeat) { return }
// if (e.code != "AltLeft") { return }
// goStore.zoomKeyPressed = false
// e.preventDefault()
// return false
// }
// function contextMenuHandler(e: MouseEvent) {
// e.preventDefault()
// }
// function mouseDownHandler(e: MouseEvent) {
// if (e.button !== 2) { return }
// goStore.zoomKeyPressed = true
// e.preventDefault()
// return false
// }
// function mouseUpHandler(e: MouseEvent) {
// if (e.button !== 2) { return }
// goStore.zoomKeyPressed = false
// e.preventDefault()
// return false
// }
// function follow_mouse(e: MouseEvent) {
// goStore.clientMouseX = e.clientX
// goStore.clientMouseY = e.clientY
// }
// onMounted(() => {
// window.addEventListener("contextmenu", contextMenuHandler)
// window.addEventListener("mousedown", mouseDownHandler)
// window.addEventListener("mouseup", mouseUpHandler)
// document.addEventListener("mousemove", follow_mouse)
// })
// import goStore, { setReady, oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
// await setup_websocket()
// useHead
// ({
// title
// : 'Héron Realms',
// })
// const heron_session = useCookie('heron_session')
// const route = useRoute()
// onMounted(async () => {
// const code = route.query.code
// if (!code) { return }
// if (Array.isArray(code)) { return }
// await setup_websocket()
// oauth2_register_discord(code)
// history.pushState(
// {},
// '',
// '/'
// )
// })
useHead
({
title
: 'Héron Realms',
})
// onMounted(async () => {
// if (!heron_session.value) { return }
@@ -171,7 +76,13 @@ const router = useRouter();
<template>
<div class="root_dom_div">
<!-- <CardOverlay v-if="goStore.zoomKeyPressed" :card_id="goStore.hoveredCard" /> -->
<NuxtLoadingIndicator>
Loading...
</NuxtLoadingIndicator>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
+4 -4
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import goStore from '~/netcode';
import { useSettingsStore } from '~/stores/settingsStore';
// import goStore from '~/netcode';
// import { useSettingsStore } from '~/stores/settingsStore';
export interface Props {
card_id: number | undefined
@@ -32,10 +32,10 @@ function cardClick() {
const rotation = ref(0)
function mouseenter(e: Event) {
goStore.hoveredCard = props.card_id
// goStore.hoveredCard = props.card_id
}
function mouseleave(e: Event) {
goStore.hoveredCard = undefined
// goStore.hoveredCard = undefined
}
watch(
+48 -37
View File
@@ -1,61 +1,72 @@
export default defineNuxtRouteMiddleware
(async (to, from) => {
if (to.path == '/login') {
return
}
import { useAuthStore } from "#imports";
if (to.path == '/') {
const code = to.query.code
export default defineNuxtRouteMiddleware(async (to, from) => {
const authStore = useAuthStore();
if (to.path == "/login") {
const code = to.query.code;
if (code) {
if (!Array.isArray(code)) {
try {
await oauth2_register_discord(code)
return navigateTo('/lobby')
await authStore.whoami();
return navigateTo("/lobby");
} catch {
return navigateTo('/login')
return navigateTo("/login");
}
} else {
return navigateTo('/login')
return navigateTo("/login");
}
}
return navigateTo("/lobby")
return navigateTo("/lobby");
}
if (!goStore.discordId) {
const heron_session = useCookie('heron_session')
if (!heron_session.value) { return navigateTo('/login') }
// if (to.path == '/') {
// const code = to.query.code
// if (code) {
// if (!Array.isArray(code)) {
// try {
// await oauth2_register_discord(code)
// return navigateTo('/lobby')
// } catch {
// return navigateTo('/login')
// }
// } else {
// return navigateTo('/login')
// }
// }
// return navigateTo("/lobby")
// }
try {
await heron_cookie_login(heron_session.value)
if (to.path == '/') {
return navigateTo("/lobby")
}
} catch {
return navigateTo('/login')
}
}
// if (!goStore.discordId) {
// const heron_session = useCookie('heron_session')
// if (!heron_session.value) { return navigateTo('/login') }
// try {
// await heron_cookie_login(heron_session.value)
// if (to.path == '/') {
// return navigateTo("/lobby")
// }
// } catch {
// return navigateTo('/login')
// }
// }
if (to.path.startsWith("/games")) {
if (Array.isArray(to.params.id)) { return navigateTo('/lobby') }
try {
await join_game(to.params.id)
return
} catch {
return navigateTo("/lobby")
if (to.path.startsWith("/game")) {
if (Array.isArray(to.params.id)) {
return navigateTo("/lobby");
}
return;
}
if (to.path == "/lobby") {
return
return;
}
if (to.path == "/rules") {
return
return;
}
// In a real app you would probably not redirect every route to `/`
// however it is important to check `to.path` before redirecting or you
// might get an infinite redirect loop
if (to.path !== '/') {
return navigateTo('/')
if (to.path !== "/") {
return navigateTo("/");
}
})
});
+48
View File
@@ -0,0 +1,48 @@
import type { Game, User } from "./interfaces";
export function subscribe(endpoint: string) {
const config = useRuntimeConfig();
const evtSource = new EventSource(new URL(endpoint, config.public.endpoint), {
withCredentials: true,
});
return evtSource;
}
export async function getLobby() {
const config = useRuntimeConfig();
const req = await useFetch<Array<Game>>(
new URL("/games", config.public.endpoint).href,
{
credentials: "include",
}
);
if (req.data.value == null) {
throw new Error("Lobby answer is empty");
}
return req.data as Ref<Game[]>;
}
export async function whoami() {
const config = useRuntimeConfig();
const req = useFetch<User>(new URL("/whoami", config.public.endpoint).href, {
credentials: "include",
});
if (
req.error.value?.statusCode &&
[401].includes(req.error.value.statusCode)
) {
await navigateTo(config.public.discordLoginEndpoint, {
external: true,
});
throw new Error("You are not logged in");
}
if (req.error.value) {
throw new Error(
"Something went wrong while trying to get user information"
);
}
return req.data;
}
export function createGame() {}
-12
View File
@@ -1,12 +0,0 @@
import type { IGameObject } from "../interfaces/generic";
export abstract class GameObject implements IGameObject {
static readonly instances = new Map<string, GameObject>()
readonly uuid: string;
readonly socket: WebSocket
constructor(socket: WebSocket, { uuid }: { uuid: string }) {
this.uuid = uuid
this.socket = socket
GameObject.instances.set(this.uuid, this)
}
}
-29
View File
@@ -1,29 +0,0 @@
import { GameObject } from "./GameObject";
import type { ICLobby } from "../interfaces/client";
export class Lobby extends GameObject implements ICLobby {
static instance: Lobby
constructor(socket: WebSocket, data: { uuid: string, games: Array<string> }) {
if (Lobby.instance) { return Lobby.instance }
super(socket, data)
Lobby.instance = this
}
onAddGame() {
}
onRemoveGame() {
}
async request_createGame() {
throw Error("not implemented")
}
removeGame() {
throw Error("not implemented")
}
}
+75 -57
View File
@@ -1,47 +1,66 @@
<script setup lang="ts">
import goStore, { setReady } from '../../netcode';
import type { Player } from '../../netcode/Player';
import { playSound } from '../../helpers/audioHelpers';
import { CardType, type Card } from '../../netcode/Card';
import { CardEffects, type Effect } from '../../netcode/Effect';
import type { Team } from '~/netcode/Team';
import { subscribe } from '~/netcode';
import { type Game } from '~/netcode/interfaces';
const current_players = computed<Array<Player>>(() => Array.from(goStore.current_game?.players ?? []).filter(p => p.team === goStore.current_game?.current_turn))
watch(
() => goStore.current_game?.current_turn?.uuid,
() => {
if (goStore.current_game?.current_turn?.uuid == goStore.self?.team?.uuid) {
playSound('/sounds/ding.mp3')
const config = useRuntimeConfig()
const route = useRoute()
const gameId = route.params.id
const { data: game, pending, error, refresh } = await useFetch<Game>(
new URL(`/games/${gameId}`, config.public.endpoint).href,
{
credentials: "include",
}
})
);
watch(
() => goStore.current_game?.losers,
() => {
if (goStore.self?.uuid && goStore.current_game?.losers && goStore.current_game.losers.includes(goStore.self.uuid)) {
showLoserScreen()
}
})
const eventSource = subscribe(`/games/${gameId}/subscribe`)
watch(
() => goStore.current_game?.ended,
() => {
if (goStore.current_game?.ended && goStore.self?.uuid && goStore.current_game?.losers && !goStore.current_game.losers.includes(goStore.self.uuid)) {
showWinnerScreen()
}
})
eventSource.addEventListener("message", (message) => {
const data = JSON.parse(message.data)
});
watch(
() => goStore.current_game?.players,
(newList, oldList) => {
if (!oldList || !newList) { return }
if (goStore.current_game?.started) { return }
if (newList?.length > oldList.length) {
playSound('/sounds/teleport.mp3')
}
const authStore = useAuthStore()
})
// const current_players = computed<Array<Player>>(() => Array.from(goStore.current_game?.players ?? []).filter(p => p.team === goStore.current_game?.current_turn))
// watch(
// () => goStore.current_game?.current_turn?.uuid,
// () => {
// if (goStore.current_game?.current_turn?.uuid == goStore.self?.team?.uuid) {
// playSound('/sounds/ding.mp3')
// }
// })
// watch(
// () => goStore.current_game?.losers,
// () => {
// if (goStore.self?.uuid && goStore.current_game?.losers && goStore.current_game.losers.includes(goStore.self.uuid)) {
// showLoserScreen()
// }
// })
// watch(
// () => goStore.current_game?.ended,
// () => {
// if (goStore.current_game?.ended && goStore.self?.uuid && goStore.current_game?.losers && !goStore.current_game.losers.includes(goStore.self.uuid)) {
// showWinnerScreen()
// }
// })
// watch(
// () => goStore.current_game?.players,
// (newList, oldList) => {
// if (!oldList || !newList) { return }
// if (goStore.current_game?.started) { return }
// if (newList?.length > oldList.length) {
// playSound('/sounds/teleport.mp3')
// }
// })
const showResults = ref(false)
const showOptions = ref(false)
@@ -60,24 +79,24 @@ function showWinnerScreen() {
}
const discard_unwrapped = ref(false)
const focusedPlayer = ref<Player>((goStore.current_game?.players.find(p => p.team == goStore.current_game?.current_turn) ?? goStore.current_game?.players[0])!)
const clientStore = useClientSideStore()
const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
// const focusedPlayer = ref<Player>((goStore.current_game?.players.find(p => p.team == goStore.current_game?.current_turn) ?? goStore.current_game?.players[0])!)
// const clientStore = useClientSideStore()
// const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
// const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
// const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
function focusPlayer(p: Player) {
focusedPlayer.value = p
}
// function focusPlayer(p: Player) {
// focusedPlayer.value = p
// }
watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
if (focusedPlayer.value.team != old_team) { return }
const first_player = goStore.current_game?.players.find(p => p.team == goStore.current_game?.current_turn)
if (!first_player) {
return
}
focusPlayer(first_player)
})
// watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
// if (focusedPlayer.value.team != old_team) { return }
// const first_player = goStore.current_game?.players.find(p => p.team == goStore.current_game?.current_turn)
// if (!first_player) {
// return
// }
// focusPlayer(first_player)
// })
</script>
<template>
@@ -85,7 +104,7 @@ watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
<AnimatedBackground></AnimatedBackground>
<div id="game_status">
<template v-if="goStore.self && !goStore.current_game?.started">
<template v-if="authStore.logged && game?.started">
<input @click.prevent="setReady(($event.target as HTMLInputElement).checked)" type="checkbox" id="ready"
:checked="goStore.self?.ready" />
<label for="ready">Ready</label>
@@ -110,7 +129,7 @@ watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
<CardPreview :tilted="false" v-if="discard_unwrapped" :card_id="c.card_id"
v-for="c in p.discard_pile">
<template
v-if="goStore.self?.turn && c && p == goStore.self && goStore.current_game?.current_turn == goStore.self.team">
v-if="goStore.self?.turn && c && p == goStore.self && game.current_turn == goStore.self.team">
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
@click.stop="sacrifice(c)">
@@ -139,7 +158,7 @@ watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
<div id="player_list_container">
<div id="player_list">
<PlayerListElement v-for="p in goStore.current_game?.players" :player="p" @click="focusPlayer(p)"
<PlayerListElement v-for="p in game.players" :player="p" @click="focusPlayer(p)"
:focused="focusedPlayer">
</PlayerListElement>
</div>
@@ -153,8 +172,7 @@ watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
</div>
<ResultDialog :isVisible="showResults" :won="isWinner" :players="goStore.current_game?.players"
@close="showResults = false">
<ResultDialog :isVisible="showResults" :won="isWinner" :players="game.players" @close="showResults = false">
</ResultDialog>
<GlobalOverlay></GlobalOverlay>
<SelfView></SelfView>
+63 -41
View File
@@ -1,23 +1,46 @@
<script setup lang="ts">
import goStore, { netcode_create_game, join_game } from '~/netcode';
import { playSound } from '~/helpers/audioHelpers';
import { ref } from 'vue';
// import { playSound } from '~/helpers/audioHelpers';
import { playSound } from "~/helpers/audioHelpers";
import { createGame, getLobby, subscribe } from "~/netcode"
import type { Player } from "~/netcode/interfaces";
const authStore = useAuthStore()
const games = await getLobby()
const eventSource = subscribe("/games/subscribe")
eventSource.addEventListener("message", (message) => {
const data = JSON.parse(message.data)
if (data.type == "create") {
games.value.push(data)
} else if (data.type == "delete") {
const index = games.value.findIndex(g => g._id == data._id)
if (index == -1) {
throw new Error("Couldn't delete game")
}
games.value.splice(index, 1)
} else if (data.type == "update") {
throw new Error("not implemented")
} else if (data.type == "start") {
throw new Error("not implemented")
}
});
const erika_alive = ref(true);
const router = useRouter();
function killErika() {
playSound('/sounds/splat1.mp3')
erika_alive.value = false;
}
// Autojoin broken
// watch(() => goStore.context, () => {
// console.log(goStore.context)
// if (goStore.context == "pregame") {
// router.push(`/games/${goStore.current_game?.uuid}`)
// }
// })
onUnmounted(() => {
eventSource.close()
})
</script>
@@ -29,35 +52,34 @@ function killErika() {
<div class="middle-row">
<div class="create-game-zone">
<button @click="netcode_create_game">Create Game</button>
<button @click="createGame">Create Game</button>
</div>
<div class="games-list-zone">
<div v-for="g in goStore.lobby?.games">
<button v-if="g" @click="router.push({ path: `/games/${g.uuid}` })"
:class="goStore.discordId == g.owner ? 'own_game' : ''">
<div v-for="g in games">
<button v-if="g" @click="router.push({ path: `/game/${g._id}` })"
:class="authStore.selfUser.userId == g.owner.userId ? 'own_game' : ''">
<template v-if="g.started && g.ended">
GAME OVER
</template>
<template v-if="g.players.every(p => p.discord_id !== goStore.discordId)">
<template v-if="!g.started">
JOIN
</template>
<template v-if="g.started && !g.ended">
<template
v-else-if="g.teams.every(t => t.players.every(p => p.user.userId !== authStore.selfUser.userId))">
SPECTATE
</template>
</template>
<template v-if="g.players.some(p => p.discord_id == goStore.discordId)">
<template v-else>
<template v-if="!g.started">
JOIN BACK
</template>
<template v-if="g.started && !g.ended">
<template v-else>
CONTINUE
</template>
</template>
<br>
{{ g.players.length }} current players:<br><span>{{ g.players.map(p => p.username).join(",")
{{ g.teams.reduce((prev, curr) => prev.concat(curr.players), [] as Player[]).length }} current
players:<br><span>{{
g.teams.reduce((prev, curr) => prev.concat(curr.players), [] as Player[]).map(p =>
p.user.username).join(",")
}}</span>
</button>
</div>
@@ -83,7 +105,6 @@ function killErika() {
</div>
</div>
</template>
@@ -120,7 +141,9 @@ function killErika() {
flex-direction: column;
gap: 10px;
button {
}
.create-game-zone button {
background-color: #4CAF50;
border: none;
color: white;
@@ -138,7 +161,6 @@ function killErika() {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
}
.games-list-zone {
@@ -146,7 +168,10 @@ function killErika() {
flex-direction: column;
gap: 20px;
button {
}
.games-list-zone button {
background-color: rgba(0, 0, 0, 0.8);
border: none;
color: white;
@@ -164,11 +189,10 @@ function killErika() {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
}
button.own_game {
.games-list-zone button.own_game {
border: solid yellow 5px;
}
}
.overlay {
@@ -187,22 +211,26 @@ function killErika() {
height: 40%;
width: 100%;
object-fit: cover;
}
.overlay-image {
.overlay-image {
position: absolute;
height: 100%;
cursor: pointer;
pointer-events: all;
}
}
.overlay-button {
.overlay-button {
position: absolute;
z-index: 1;
width: 300px;
box-sizing: border-box;
pointer-events: none;
.kill-erika-button {
}
.kill-erika-button {
display: none;
pointer-events: all;
background-color: #f44336;
@@ -220,8 +248,6 @@ function killErika() {
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
}
}
.speech-bubble {
@@ -241,11 +267,7 @@ function killErika() {
line-height: 1.5;
}
.erika-zone:hover {
.overlay-button {
.kill-erika-button {
.erika-zone:hover .overlay-button .kill-erika-button {
display: inline-block;
}
}
}
</style>
+7 -7
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { navigateTo, useRuntimeConfig } from 'nuxt/app';
import goStore, { netcode_create_game, join_game } from '../netcode';
// import { navigateTo, useRuntimeConfig } from 'nuxt/app';
// import goStore, { netcode_create_game, join_game } from '../netcode';
const config = useRuntimeConfig()
@@ -24,15 +24,15 @@ async function discordLogin() {
<CardPreview :card_id="undefined"></CardPreview>
</div>
<div v-if="goStore.current_game?.started">
<!-- <div v-if="goStore.current_game?.started">
<b>Mode spectateur</b>
</div>
</div> -->
<template v-if="!goStore.current_game?.started">
<!-- <template v-if="!goStore.current_game?.started"> -->
<!-- <input type="text" v-model="username" />
<button @click="register(username)">LOGIN</button> -->
<button class="menu-button" @click="discordLogin()">Se connecter via Discord</button>
</template>
<!-- <button class="menu-button" @click="discordLogin()">Se connecter via Discord</button>
</template> -->
</div>
<footer>
+9 -8
View File
@@ -1,7 +1,4 @@
<script setup lang="ts">
import type { Card } from '~/netcode/Card';
const router = useRouter();
</script>
@@ -10,7 +7,7 @@ const router = useRouter();
<div class="background">
<div class="overlay-2">
<div class="erika-zone-2">
<img @click="router.push({ path: '/' })" class="overlay-image"
<img @click="router.push({ path: '/lobby' })" class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/images/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse" />
</div>
@@ -298,7 +295,10 @@ const router = useRouter();
top: 0;
pointer-events: none;
.erika-zone-2 {
}
.erika-zone-2 {
position: absolute;
top: 300px;
display: flex;
@@ -308,12 +308,13 @@ const router = useRouter();
width: 100%;
object-fit: cover;
.overlay-image {
}
.overlay-image {
position: sticky;
height: 100%;
cursor: pointer;
pointer-events: all;
}
}
}
</style>
-33
View File
@@ -1,33 +0,0 @@
import path from "path"
import type { IUser } from "~/netcode/interfaces"
enum HTTPMethods {
GET = "GET",
POST = "POST"
}
const Endpoints = {
LOGIN: { url: "/login", method: HTTPMethods.POST, body: null }
} as const
type ReturnTypes = {
LOGIN: IUser
}
type EnpointsNames = keyof typeof Endpoints
const config = useRuntimeConfig();
export default async function api<T extends EnpointsNames>(target: T, body?: typeof Endpoints[T]["body"]): Promise<ReturnTypes[T]> {
const data = await fetch(path.join(config.public.endpoint, Endpoints[target].url), {
credentials: "same-origin",
mode: "same-origin",
method: Endpoints[target].method,
body,
})
return await data.json() as ReturnTypes[T]
}
+10 -10
View File
@@ -1,19 +1,19 @@
import path from "path";
import { defineStore } from "pinia";
import api from "~/services";
const config = useRuntimeConfig();
import { whoami } from "~/netcode";
export const useAuthStore = defineStore("auth", {
state: () => ({
self: { id: 0, image: "" }
selfUser: {} as { userId?: string; image?: string; username?: string },
logged: false,
}),
actions: {
login: async () => {
await api("LOGIN")
async whoami() {
const data = await whoami();
Object.assign(this.selfUser, data.value);
this.logged = true;
},
oauth2_register_discord: () => {
throw Error("not implemented")
}
}
throw Error("not implemented");
},
},
});