feat: card lock

This commit is contained in:
2024-07-03 00:04:23 +02:00
parent d5dd147fba
commit 866a537924
16 changed files with 1451 additions and 1231 deletions
+114 -86
View File
@@ -1,100 +1,123 @@
<script setup lang="ts"> <script setup lang="ts">
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore();
import gsap from 'gsap'; import gsap from "gsap";
import ShaderWorker from '~/assets/workers/ShaderWorker?worker' import ShaderWorker from "~/assets/workers/ShaderWorker?worker";
import type { Game } from '~/netcode/interfaces'; import type { Game } from "~/netcode/interfaces";
const emit = defineEmits(["ready"]) const emit = defineEmits<{
const worker = new ShaderWorker() ready: [];
}>();
const canvas = ref<HTMLCanvasElement | null>(null) const worker = new ShaderWorker();
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null)
const canvas = ref<HTMLCanvasElement | null>(null);
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null);
export interface Props { export interface Props {
colorStart?: [number, number, number] colorStart?: [number, number, number];
colorEnd?: [number, number, number] colorEnd?: [number, number, number];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
colorStart: () => [0, .3, 0], colorStart: () => [0, 0.3, 0],
colorEnd: () => [0, 1, 0] colorEnd: () => [0, 1, 0],
}) });
const _colorStart = reactive(props.colorStart)
const _colorEnd = reactive(props.colorEnd)
const _colorStart = reactive(props.colorStart);
const _colorEnd = reactive(props.colorEnd);
function initShader() { function initShader() {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (!canvas.value) { if (!canvas.value) {
reject("canvas is null"); reject("canvas is null");
return return;
} }
const offscreen = canvas.value?.transferControlToOffscreen() const offscreen = canvas.value?.transferControlToOffscreen();
worker.postMessage({ worker.postMessage(
command: "initShader", {
canvas: offscreen, command: "initShader",
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio }, canvas: offscreen,
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate }, screen: {
}, [offscreen]) height: window.innerHeight * devicePixelRatio,
if (settingsStore.shaderFramerate == 0) { width: window.innerWidth * devicePixelRatio,
killShader() devicePixelRatio,
} },
resizeShader() shaderOptions: {
worker.onmessage = (e) => { quality: settingsStore.shaderQuality,
if (e.data == "ready") { framerate: settingsStore.shaderFramerate,
resolve() },
} },
} [offscreen]
}) );
if (settingsStore.shaderFramerate == 0) {
killShader();
}
resizeShader();
worker.onmessage = (e) => {
if (e.data == "ready") {
resolve();
}
};
});
} }
function recolorShader() { function recolorShader() {
worker.postMessage({ worker.postMessage({
command: 'recolorShader', command: "recolorShader",
colorStart: [..._colorStart], colorStart: [..._colorStart],
colorEnd: [..._colorEnd] colorEnd: [..._colorEnd],
}) });
} }
function resizeShader() { function resizeShader() {
worker.postMessage({ worker.postMessage({
command: 'resizeShader', command: "resizeShader",
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio }, screen: {
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate }, height: window.innerHeight * devicePixelRatio,
}) width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
} }
function killShader() { function killShader() {
worker.postMessage({ worker.postMessage({
command: 'killShader' command: "killShader",
}) });
} }
function resumeShader() { function resumeShader() {
worker.postMessage({ worker.postMessage({
command: 'resumeShader', command: "resumeShader",
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate }, shaderOptions: {
}) quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
} }
watch(() => [settingsStore.shaderFramerate], () => { watch(
() => [settingsStore.shaderFramerate],
() => {
if (settingsStore.shaderFramerate == 0) { if (settingsStore.shaderFramerate == 0) {
killShader() killShader();
return return;
} }
resumeShader() resumeShader();
}
);
}) watch(
() => [settingsStore.shaderQuality],
watch(() => [settingsStore.shaderQuality], () => { () => {
resizeShader() resizeShader();
}) }
);
// watch(() => [props.game.currentTurn], () => { // watch(() => [props.game.currentTurn], () => {
// if (props.game.teams.find() != goStore.self?.team) { // if (props.game.teams.find() != goStore.self?.team) {
@@ -106,38 +129,43 @@ watch(() => [settingsStore.shaderQuality], () => {
// } // }
// }, { immediate: true }) // }, { immediate: true })
watch([_colorEnd, _colorStart], () => { watch([_colorEnd, _colorStart], () => {
recolorShader() recolorShader();
}) });
onMounted(async () => { onMounted(async () => {
recolorShader() recolorShader();
await initShader() await initShader();
window.addEventListener("resize", resizeShader, true) window.addEventListener("resize", resizeShader, true);
emit("ready") emit("ready");
}) });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("resize", resizeShader) window.removeEventListener("resize", resizeShader);
killShader() killShader();
}) });
</script> </script>
<template> <template>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480" <canvas
height="270">test</canvas> ref="canvas"
id="background"
:class="{ pixelated: settingsStore.shaderPixelated }"
width="480"
height="270"
>test</canvas
>
</template> </template>
<style scoped> <style scoped>
#background { #background {
left: 0; left: 0;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
#background.pixelated { #background.pixelated {
image-rendering: pixelated; image-rendering: pixelated;
} }
</style> </style>
+12 -46
View File
@@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
// import goStore from '~/netcode';
// import { useSettingsStore } from '~/stores/settingsStore';
export interface Props { export interface Props {
uuid?: string uuid?: string
@@ -90,9 +88,6 @@ onMounted(() => {
</div> </div>
</template> </template>
<style scoped> <style scoped>
@@ -121,47 +116,6 @@ onMounted(() => {
height: var(--max-height); height: var(--max-height);
} }
.card_image.outlined {
outline: 2px solid red;
outline-offset: -2px;
}
@keyframes unlocked-float {
from {
transform: none;
filter: drop-shadow(calc(var(--margin-bottom)*.3) var(--margin-bottom) 1px rgba(0, 0, 0, .60));
}
to {
transform: translateY(calc(var(--margin-bottom)*-.25));
filter: drop-shadow(calc(var(--margin-bottom)*.3) calc(var(--margin-bottom)*1.5) 1px rgba(0, 0, 0, .60));
}
}
@keyframes unlocked-rotate {
from {
rotate: -2deg;
}
to {
rotate: 2deg;
}
}
.card.unlocked {
margin-top: 0px;
margin-bottom: var(--margin-bottom);
filter: drop-shadow(calc(var(--margin-bottom)*.3) var(--margin-bottom) 1px rgba(0, 0, 0, .60));
animation: 2s infinite alternate ease-in-out unlocked-float, 3.1s infinite alternate ease-in-out unlocked-rotate;
animation-delay: var(--random-seed);
animation-play-state: running;
}
.card:not(.unlocked):hover {
animation-play-state: paused;
rotate: none !important;
}
.card.wrapped { .card.wrapped {
height: 6px; height: 6px;
margin: 0; margin: 0;
@@ -171,6 +125,14 @@ onMounted(() => {
filter: drop-shadow(.5px .5px .5px gray) drop-shadow(-.5px -.5px .5px gray); filter: drop-shadow(.5px .5px .5px gray) drop-shadow(-.5px -.5px .5px gray);
} }
.card.unlocked {
margin-top: 0px;
margin-bottom: var(--margin-bottom);
transform: translateY(calc(var(--margin-bottom)*-.25));
filter: drop-shadow(calc(var(--margin-bottom)*.3) var(--margin-bottom) 1px rgba(0, 0, 0, .60));
cursor: pointer;
}
.card.zoom { .card.zoom {
transform: scale(200%); transform: scale(200%);
z-index: 10; z-index: 10;
@@ -194,6 +156,10 @@ onMounted(() => {
} }
</style> </style>
<style> <style>
.card.unlocked>* {
pointer-events: none;
}
#contextMenuButtons>* { #contextMenuButtons>* {
flex: 1; flex: 1;
border-radius: 12px; border-radius: 12px;
+88 -72
View File
@@ -1,133 +1,149 @@
<script setup lang="ts"> <script setup lang="ts">
import { containerEvents, delay } from '~/netcode/events'; import { containerEvents, delay } from "~/netcode/events";
import type { Container } from '~/netcode/interfaces'; import type { Container, PlayingTurn } from "~/netcode/interfaces";
export interface Props { export interface Props {
container: Container container: Container;
wrapped?: boolean playingTurn?: PlayingTurn;
hidden?: boolean wrapped?: boolean;
hidden?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
wrapped: () => false, wrapped: () => false,
hidden: () => false hidden: () => false,
}) });
const settingsStore = useSettingsStore() const emit = defineEmits(["cardClick"]);
const settingsStore = useSettingsStore();
const cards = computed(() => { const cards = computed(() => {
if (props.hidden) { if (props.hidden) {
return props.container.cards.map((_v, i) => ({ _id: i, cardId: undefined })) return props.container.cards.map((_v, i) => ({
} _id: i,
return props.container.cards cardId: undefined,
}) }));
}
return props.container.cards;
});
const animationSpeed = computed(() => settingsStore.animationSpeed + 'ms') const animationSpeed = computed(() => settingsStore.animationSpeed + "ms");
const shuffleAnimationSpeed = computed(() => settingsStore.animationSpeed * 2 + 'ms') const shuffleAnimationSpeed = computed(
function populateContainer() { () => settingsStore.animationSpeed * 2 + "ms"
);
function populateContainer() {}
} const rotate = ref(false);
const rotate = ref(false)
async function shuffleContainer() { async function shuffleContainer() {
console.log("rotate") rotate.value = true;
rotate.value = true await delay(settingsStore.animationSpeed * 2);
await delay(settingsStore.animationSpeed * 2) rotate.value = false;
rotate.value = false await delay(settingsStore.animationSpeed * 2);
await delay(settingsStore.animationSpeed * 2)
} }
function getCardsPosition(cards: string) {}
function getCardsPosition(cards: string) { const animationCallbacks = ref<Array<() => void>>([]);
}
const animationCallbacks = ref<Array<() => void>>([])
function startAnimation() { function startAnimation() {
animationCallbacks.value.forEach(p => p()) animationCallbacks.value.forEach((p) => p());
animationCallbacks.value = [] animationCallbacks.value = [];
} }
function endAnimation() { function endAnimation() {
animationCallbacks.value.forEach(p => p()) animationCallbacks.value.forEach((p) => p());
} }
containerEvents.set(props.container._id, { populateContainer, shuffleContainer, getCardsPosition, startAnimation, endAnimation }) containerEvents.set(props.container._id, {
populateContainer,
shuffleContainer,
getCardsPosition,
startAnimation,
endAnimation,
});
async function onEnter(_el: any, done: () => void) { async function onEnter(_el: any, done: () => void) {
animationCallbacks.value.push(done) animationCallbacks.value.push(done);
} }
async function onLeave(_el: any, done: () => void) { async function onLeave(_el: any, done: () => void) {
animationCallbacks.value.push(done) animationCallbacks.value.push(done);
} }
async function onBeforeEnter() { async function onBeforeEnter() {}
}
onUnmounted(() => { onUnmounted(() => {
containerEvents.delete(props.container._id) containerEvents.delete(props.container._id);
}) });
</script> </script>
<template> <template>
<div class="cards_container" :class="{ wrapped, rotate }"> <div class="cards_container" :class="{ wrapped, rotate }">
<TransitionGroup name="list" @enter="onEnter" @leave="onLeave" @before-enter="onBeforeEnter"> <TransitionGroup
<CardPreview :card_id="c.cardId" :wrapped="wrapped" v-for="c in cards" :key="c._id" name="list"
:uuid="c._id.toString()"> @enter="onEnter"
<slot></slot> @leave="onLeave"
</CardPreview> @before-enter="onBeforeEnter"
</TransitionGroup> >
<!-- <img :class="`card_image`" :src="'/cards/background.png'" draggable="false"> --> <CardPreview
</div> @click="$emit('cardClick', c._id)"
v-for="c in cards"
:card_id="c.cardId"
:wrapped="wrapped"
:key="c._id"
:uuid="c._id.toString()"
:locked="
!playingTurn || playingTurn?.cardsLocked.includes(c._id.toString())
"
>
</CardPreview>
</TransitionGroup>
<!-- <img :class="`card_image`" :src="'/cards/background.png'" draggable="false"> -->
</div>
</template> </template>
<style scoped> <style scoped>
.list-enter-active, .list-enter-active,
.list-leave-active { .list-leave-active {
transition: all v-bind('animationSpeed') ease; transition: all v-bind("animationSpeed") ease;
} }
.list-leave-to, .list-leave-to,
.list-enter-from { .list-enter-from {
opacity: 0; opacity: 0;
transform: translateX(30px); transform: translateX(30px);
} }
.rotate { .rotate {
rotate: 360deg; rotate: 360deg;
} }
.cards_container { .cards_container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
min-width: 200px; min-width: 200px;
transition: rotate v-bind('shuffleAnimationSpeed') ease; transition: rotate v-bind("shuffleAnimationSpeed") ease;
} }
.cards_container.wrapped { .cards_container.wrapped {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.cards_container.wrapped:before { .cards_container.wrapped:before {
height: calc(250px - 10px); height: calc(250px - 10px);
display: block; display: block;
content: ""; content: "";
width: 0; width: 0;
} }
.empty { .empty {
max-height: 10px; max-height: 10px;
} }
.empty>.card_image { .empty > .card_image {
filter: brightness(0.5); filter: brightness(0.5);
max-height: 250px; max-height: 250px;
} }
</style> </style>
+21 -18
View File
@@ -1,30 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
const displayOptions = ref(false);
const displayOptions = ref(false)
</script> </script>
<template> <template>
<div :class="`card_overlay`" ref="card"> <div :class="`card_overlay`" ref="card">
<img class="settings_button" src="/images/settings.png" alt="settings" @click="displayOptions = true" /> <img
</div> class="settings_button"
<OptionsDialog :isVisible="displayOptions" @close="displayOptions = false" /> src="/img/settings.png"
alt="settings"
@click="displayOptions = true"
/>
</div>
<OptionsDialog :isVisible="displayOptions" @close="displayOptions = false" />
</template> </template>
<style scoped> <style scoped>
.card_overlay { .card_overlay {
/* display: none; */ /* display: none; */
position: fixed; position: fixed;
z-index: 999; z-index: 999;
display: flex; display: flex;
pointer-events: none; pointer-events: none;
/* top: left: */ /* top: left: */
top: 0; top: 0;
} }
.settings_button { .settings_button {
pointer-events: all; pointer-events: all;
width: 30px; width: 30px;
padding: 5px; padding: 5px;
} }
</style> </style>
+41 -48
View File
@@ -1,16 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Container } from '~/netcode/interfaces'; import type { Container } from "~/netcode/interfaces";
export interface Props { export interface Props {
market: Container market: Container;
fire_gems: Container fire_gems: Container;
} }
const props = defineProps<Props>() const props = defineProps<Props>();
const authStore = useAuthStore() const authStore = useAuthStore();
// const selfPlayer = computed(() => authStore.logged ? playerList.value?.find(p => p.user.userId == authStore.selfUser.userId) : undefined) // const selfPlayer = computed(() => authStore.logged ? playerList.value?.find(p => p.user.userId == authStore.selfUser.userId) : undefined)
@@ -19,58 +17,53 @@ const authStore = useAuthStore()
// const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1)) // const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
function marketCardClick(e: string) {
function onBeforeEnter(el: Element) { console.log(e);
(el as HTMLElement).style.visibility = "hidden"
const element = el as HTMLElement
element.style.top = element.offsetTop + "px"
element.style.left = element.offsetLeft + "px"
element.style.zIndex = "999"
} }
function onEnter(el: any, done: () => unknown) { function fireGemCardClick(e: string) {
console.log("enter") console.log(e);
// setTimeout(() => {
// el.style.visibility = null
// done()
// }, 1000)
done()
} }
</script> </script>
<template> <template>
<div id="market"> <div id="market">
<CardPreview :tilted="false" :card_id="undefined"></CardPreview> <CardPreview :tilted="false" :card_id="undefined"></CardPreview>
<div class="separator"></div> <div class="separator"></div>
<CardsGrouped :container="market"></CardsGrouped> <CardsGrouped
<div class="separator"></div> :container="market"
<CardsGrouped :container="fire_gems" :wrapped="true"></CardsGrouped> @cardClick="marketCardClick"
</div> ></CardsGrouped>
<div class="separator"></div>
<CardsGrouped
:container="fire_gems"
:wrapped="true"
@cardClick="fireGemCardClick"
></CardsGrouped>
</div>
</template> </template>
<style scoped> <style scoped>
#market { #market {
width: min-content; width: min-content;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 2; z-index: 2;
position: relative; position: relative;
gap: 2px; gap: 2px;
margin: 10px; margin: 10px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
padding-bottom: 10px; padding-bottom: 10px;
border-radius: 10px; border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60)); filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
backdrop-filter: blur(5px) brightness(80%); backdrop-filter: blur(5px) brightness(80%);
background: rgba(155, 62, 62, 0.46); background: rgba(155, 62, 62, 0.46);
} }
.separator { .separator {
width: 50px; width: 50px;
} }
</style> </style>
+52 -23
View File
@@ -1,23 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
const settingsStore = useSettingsStore();
const settingsStore = useSettingsStore()
export interface Props { export interface Props {
isVisible: boolean; isVisible: boolean;
} }
const _devicePixelRatio = computed(() => devicePixelRatio) const _devicePixelRatio = computed(() => devicePixelRatio);
const _window = computed(() => window) const _window = computed(() => window);
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {});
})
const emit = defineEmits(['close']) const emit = defineEmits(["close"]);
const close = () => { const close = () => {
emit('close') emit("close");
} };
</script> </script>
<template> <template>
@@ -30,7 +27,9 @@ const close = () => {
<span>Toggle Card Style</span> <span>Toggle Card Style</span>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<button @click="settingsStore.switchCardStyles">Toggle Card style</button> <button @click="settingsStore.switchCardStyles">
Toggle Card style
</button>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<span>{{ settingsStore.cardStyle }}</span> <span>{{ settingsStore.cardStyle }}</span>
@@ -39,7 +38,13 @@ const close = () => {
<span>Animation Speed</span> <span>Animation Speed</span>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<input type="range" min="50" max="250" step="25" v-model="settingsStore.animationSpeed"> <input
type="range"
min="50"
max="250"
step="25"
v-model="settingsStore.animationSpeed"
/>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<span>{{ settingsStore.animationSpeed }}ms</span> <span>{{ settingsStore.animationSpeed }}ms</span>
@@ -51,19 +56,44 @@ const close = () => {
<span>Shader Quality</span> <span>Shader Quality</span>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<input type="range" min="1" max="10" v-model="settingsStore.shaderQuality" <input
:disabled="settingsStore.shaderFramerate == 0"> type="range"
min="1"
max="10"
v-model="settingsStore.shaderQuality"
:disabled="settingsStore.shaderFramerate == 0"
/>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<span>{{ Math.floor(_window.innerWidth * _devicePixelRatio * settingsStore.shaderQuality / 10) }}x{{ <span
Math.floor(_window.innerHeight * >{{
_devicePixelRatio * settingsStore.shaderQuality / 10) }}</span> Math.floor(
(_window.innerWidth *
_devicePixelRatio *
settingsStore.shaderQuality) /
10
)
}}x{{
Math.floor(
(_window.innerHeight *
_devicePixelRatio *
settingsStore.shaderQuality) /
10
)
}}</span
>
</div> </div>
<div class="dialog-header"> <div class="dialog-header">
<span>Shader Framerate</span> <span>Shader Framerate</span>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<input type="range" min="0" max="65" step="5" v-model="settingsStore.shaderFramerate"> <input
type="range"
min="0"
max="65"
step="5"
v-model="settingsStore.shaderFramerate"
/>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<span v-if="settingsStore.shaderFramerate == 65">vsync</span> <span v-if="settingsStore.shaderFramerate == 65">vsync</span>
@@ -73,7 +103,7 @@ const close = () => {
<span>Pixelated</span> <span>Pixelated</span>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<input type="checkbox" v-model="settingsStore.shaderPixelated"> <input type="checkbox" v-model="settingsStore.shaderPixelated" />
</div> </div>
</div> </div>
</div> </div>
@@ -92,11 +122,10 @@ const close = () => {
align-items: center; align-items: center;
z-index: 1000; z-index: 1000;
display: grid; display: grid;
} }
.dialog-box { .dialog-box {
background-image: url('../images/bg-grass.png'); background-image: url("../img/bg-grass.png");
padding: 0 20px; padding: 0 20px;
border-radius: 5px; border-radius: 5px;
width: 800px; width: 800px;
@@ -111,7 +140,7 @@ const close = () => {
.dialog-box h3 { .dialog-box h3 {
font-size: 30px; font-size: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0; margin: 0;
width: 100%; width: 100%;
padding: 10px 0; padding: 10px 0;
@@ -139,4 +168,4 @@ const close = () => {
.dialog-footer { .dialog-footer {
grid-column-start: 3; grid-column-start: 3;
} }
</style> </style>
+219 -190
View File
@@ -1,31 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { endTurn } from "~/netcode"; import { endTurn, lockCard } from "~/netcode";
import type { Game, Player } from '~/netcode/interfaces'; import type { Game, Player } from "~/netcode/interfaces";
import { useClientSideStore } from "~/stores/turn"; import { useTurnStore } from "~/stores/turnStore";
export interface Props { export interface Props {
player: Player; player: Player;
game: Game, game: Game;
hide_stack_and_discard?: boolean; hide_stack_and_discard?: boolean;
} }
const authStore = useAuthStore() const authStore = useAuthStore();
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
hide_stack_and_discard: () => false, hide_stack_and_discard: () => false,
}) });
const playerList = computed(() => props.game.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])) const playerList = computed(() =>
const selfPlayer = computed(() => authStore.logged ? playerList.value?.find(p => p.user.userId == authStore.selfUser.userId) : undefined) props.game.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 currentTeam = computed(() => props.game.teams.find(t => t._id == props.game.currentTurn)) const currentTeam = computed(() =>
props.game.teams.find((t) => t._id == props.game.currentTurn.currentTeam)
);
const isPlayerTurn = computed(() => currentTeam.value?.players.some(p => p.user.userId == authStore.selfUser.userId)); const isPlayerTurn = computed(() =>
currentTeam.value?.players.some(
(p) => p.user.userId == authStore.selfUser.userId
)
);
const isSelf = computed(() => props.player.user.userId === authStore.selfUser.userId); const isSelf = computed(
const isSelfTurn = computed(() => isSelf && isPlayerTurn) () => props.player.user.userId === authStore.selfUser.userId
);
const isSelfTurn = computed(() => isSelf && isPlayerTurn);
const warningText = ref("Effets restants!");
const warningText = ref("Effets restants!") const turnStore = useTurnStore();
const clientStore = useClientSideStore()
// const make_discard = (target: Player) => // const make_discard = (target: Player) =>
// clientStore.findUseEffect(CardEffects.MAKE_DISCARD, target); // clientStore.findUseEffect(CardEffects.MAKE_DISCARD, target);
@@ -35,8 +48,7 @@ const clientStore = useClientSideStore()
// const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target) // const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
// const prepare_champion = (target: CardParser) => clientStore.findUseEffect(CardEffects.PREPARE, target) // const prepare_champion = (target: CardParser) => clientStore.findUseEffect(CardEffects.PREPARE, target)
let warning = ref(false);
let warning = ref(false)
// function has_actions_remaining(): boolean { // function has_actions_remaining(): boolean {
// return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false) // return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false)
@@ -52,27 +64,34 @@ let warning = ref(false)
// } // }
function checkEndTurn() { function checkEndTurn() {
// TODO: Add a confirmation dialog if resources are left // TODO: Add a confirmation dialog if resources are left
// if ( // if (
// (!warning.value) // (!warning.value)
// && has_actions_remaining()) { // && has_actions_remaining()) {
// // Set a warning flag to true // // Set a warning flag to true
// // Display an alert somehow using this warning flag // // Display an alert somehow using this warning flag
// warning.value = true // warning.value = true
// } // }
// else if (!warning.value && can_buy_fire_gem()) { // else if (!warning.value && can_buy_fire_gem()) {
// warningText.value = "P'tite Fire gem?" // warningText.value = "P'tite Fire gem?"
// warning.value = true // warning.value = true
// } // }
// else { // else {
// warningText.value = "Effets restants!" // warningText.value = "Effets restants!"
// warning.value = false // warning.value = false
// endTurn() // endTurn()
// } // }
endTurn(props.game._id) endTurn(props.game._id);
} }
function boardCardClick(cardUUID: string) {
if (props.game.currentTurn.cardsLocked.includes(cardUUID)) {
//Card already locked
return;
}
lockCard(props.game._id, cardUUID);
}
// function damage_team_all(team: Team) { // function damage_team_all(team: Team) {
// for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) { // for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) {
// damage_team(team) // damage_team(team)
@@ -82,25 +101,34 @@ function checkEndTurn() {
// function check_for_guard(player: Player) { // function check_for_guard(player: Player) {
// return player.board.some(c => c.guard === true) // return player.board.some(c => c.guard === true)
// } // }
</script> </script>
<template> <template>
<div class="player" :class="{ turn: isPlayerTurn, self: isSelf }"> <div class="player" :class="{ turn: isPlayerTurn, self: isSelf }">
<div id="player_banner" class="big">test</div> <div id="player_banner" class="big">test</div>
<div id="turn_buttons"> <div id="turn_buttons">
<button id="end_turn" class="end_turn_button turn_icon" :class="{ hidden: !isSelfTurn }" <button
@click="checkEndTurn">END TURN</button> id="end_turn"
class="end_turn_button turn_icon"
:class="{ hidden: !isSelfTurn }"
@click="checkEndTurn"
>
END TURN
</button>
<div class="warning" <div
:class="{ 'warning-fire-gem': warningText == 'P\'tite Fire gem?', 'visible': warning }"> class="warning"
/!\<br>{{ warningText }} :class="{
</div> 'warning-fire-gem': warningText == 'P\'tite Fire gem?',
</div> visible: warning,
}"
>
/!\<br />{{ warningText }}
</div>
</div>
<div id="current_player_board">
<div id="current_player_board"> <!-- <div class="other_player_buttons_container">
<!-- <div class="other_player_buttons_container">
<button @click.stop="damage_team(props.player.team)" <button @click.stop="damage_team(props.player.team)"
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team" class="damage_button" v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team" class="damage_button"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)"> :disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
@@ -118,14 +146,19 @@ function checkEndTurn() {
make Discard make Discard
</button> </button>
</div> --> </div> -->
<!-- <br /> --> <!-- <br /> -->
<CardsGrouped :container="player.board" :wrapped="false"> <CardsGrouped
</CardsGrouped> :container="player.board"
:wrapped="false"
:playingTurn="props.game.currentTurn"
@cardClick="boardCardClick"
>
</CardsGrouped>
<!-- <div class="cards_container"> --> <!-- <div class="cards_container"> -->
<!-- HAND --> <!-- HAND -->
<!-- <template v-if="isSelf"> <!-- <template v-if="isSelf">
<CardPreview :card_id="c?.card_id" v-for="c in props.player.hand" @click="c && playCard(c)"> <CardPreview :card_id="c?.card_id" v-for="c in props.player.hand" @click="c && playCard(c)">
<button <button
v-if="props.player.turn && c && isSelf && (props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0" v-if="props.player.turn && c && isSelf && (props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
@@ -135,12 +168,12 @@ function checkEndTurn() {
</CardPreview> </CardPreview>
</template> --> </template> -->
<!-- BOARD --> <!-- BOARD -->
<!-- <CardPreview :card_id="c.cardId" v-for="c in props.player.board.cards" <!-- <CardPreview :card_id="c.cardId" v-for="c in props.player.board.cards"
@click="console.log/*lockCard(c)*/" @click="console.log/*lockCard(c)*/"
:locked="/*props.player.turn && c.uuid in props.player.turn.cards_locked*/ false"> --> :locked="/*props.player.turn && c.uuid in props.player.turn.cards_locked*/ false"> -->
<!-- <template v-if="c && isSelf && isPlayerTurn && props.player.turn"> <!-- <template v-if="c && isSelf && isPlayerTurn && props.player.turn">
// Card Locked // Card Locked
<template v-if="c.uuid in props.player.turn.cards_locked"> <template v-if="c.uuid in props.player.turn.cards_locked">
<template v-for="e in clientStore.maskedServerEffects"> <template v-for="e in clientStore.maskedServerEffects">
@@ -193,237 +226,233 @@ function checkEndTurn() {
STUN STUN
</button> </button>
</template> --> </template> -->
<!-- </CardPreview> --> <!-- </CardPreview> -->
<!-- </div> --> <!-- </div> -->
</div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
.warning { .warning {
background-color: rgba(255, 0, 0, 0.7); background-color: rgba(255, 0, 0, 0.7);
color: white; color: white;
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
width: 100px; width: 100px;
font-size: 0.85em; font-size: 0.85em;
margin-left: 5px; margin-left: 5px;
visibility: hidden visibility: hidden;
} }
.warning.visible { .warning.visible {
visibility: visible visibility: visible;
} }
.warning-fire-gem { .warning-fire-gem {
background-color: rgba(255, 165, 0, 0.7); background-color: rgba(255, 165, 0, 0.7);
} }
#turn_buttons { #turn_buttons {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
#end_turn { #end_turn {
z-index: 10; z-index: 10;
height: 100%; height: 100%;
} }
#end_turn.hidden { #end_turn.hidden {
visibility: hidden visibility: hidden;
} }
.stack, .stack,
.discard, .discard,
.cards_container { .cards_container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
flex-wrap: wrap-reverse; flex-wrap: wrap-reverse;
} }
.stack { .stack {
grid-area: stack; grid-area: stack;
margin-left: 75px; margin-left: 75px;
} }
.discard { .discard {
margin-top: 40px; margin-top: 40px;
grid-area: discard; grid-area: discard;
justify-content: start; justify-content: start;
cursor: pointer; cursor: pointer;
pointer-events: none; pointer-events: none;
margin-left: 75px; margin-left: 75px;
} }
.discard.invisible { .discard.invisible {
visibility: hidden; visibility: hidden;
} }
.player { .player {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: min-content 1fr; grid-template-rows: min-content 1fr;
/* border-top: 5px solid black; */ /* border-top: 5px solid black; */
grid-template-areas: grid-template-areas:
"player_stats" "player_stats"
"player"; "player";
transition: all 1s; transition: all 1s;
} }
#current_player_board { #current_player_board {
grid-area: player; grid-area: player;
} }
.damage_button { .damage_button {
background-color: #ff4d4d; background-color: #ff4d4d;
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 5px 25px; padding: 5px 25px;
margin: 5px 0 0 0; margin: 5px 0 0 0;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
} }
.damage_button:disabled { .damage_button:disabled {
background-color: rgba(255, 77, 77, 0.5); background-color: rgba(255, 77, 77, 0.5);
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
cursor: not-allowed; cursor: not-allowed;
} }
.damage_button:hover { .damage_button:hover {
background-color: #e60000; background-color: #e60000;
} }
.damage_button:disabled:hover { .damage_button:disabled:hover {
background-color: rgba(230, 0, 0, 0.5); background-color: rgba(230, 0, 0, 0.5);
} }
.damage_button:active { .damage_button:active {
transform: translateY(2px); transform: translateY(2px);
box-shadow: none; box-shadow: none;
} }
.all_damage_button { .all_damage_button {
padding: 5px 5px; padding: 5px 5px;
font-size: 14px; font-size: 14px;
} }
.make_discard_button { .make_discard_button {
background-color: #55b42f; background-color: #55b42f;
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 5px 15px; padding: 5px 15px;
margin: 5px 0 0 0; margin: 5px 0 0 0;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
} }
.make_discard_button:disabled { .make_discard_button:disabled {
background-color: rgba(85, 180, 47, 0.5); background-color: rgba(85, 180, 47, 0.5);
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
text-transform: uppercase; text-transform: uppercase;
cursor: not-allowed; cursor: not-allowed;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
} }
.make_discard_button:hover { .make_discard_button:hover {
background-color: #03883a; background-color: #03883a;
} }
.make_discard_button:disabled:hover { .make_discard_button:disabled:hover {
background-color: rgba(3, 136, 58, 0.5); background-color: rgba(3, 136, 58, 0.5);
} }
.make_discard_button:active { .make_discard_button:active {
transform: translateY(2px); transform: translateY(2px);
box-shadow: none; box-shadow: none;
} }
.turn_icon { .turn_icon {
position: relative; position: relative;
} }
#player_info { #player_info {
display: flex; display: flex;
align-items: center; align-items: center;
min-width: 200px; min-width: 200px;
} }
#player_banner.big { #player_banner.big {
padding: 0; padding: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
max-width: min-content; max-width: min-content;
border: 5px solid black; border: 5px solid black;
border-radius: 10px; border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60)); filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
backdrop-filter: blur(5px) brightness(80%) grayscale(80%); backdrop-filter: blur(5px) brightness(80%) grayscale(80%);
} }
#player_banner img { #player_banner img {
height: 50px; height: 50px;
border-radius: 25px; border-radius: 25px;
} }
.turn_icon img { .turn_icon img {
height: 50px; height: 50px;
} }
.turn_icon_number { .turn_icon_number {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.end_turn_button { .end_turn_button {
background-color: #4CAF50; background-color: #4caf50;
/* Green background color */ /* Green background color */
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 13px 10px; padding: 13px 10px;
margin: 0 0 0 5px; margin: 0 0 0 5px;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
} }
.end_turn_button:hover { .end_turn_button:hover {
background-color: #45a049; background-color: #45a049;
/* Darker green on hover */ /* Darker green on hover */
} }
.end_turn_button:active { .end_turn_button:active {
transform: translateY(2px); transform: translateY(2px);
/* Add a slight press effect on click */ /* Add a slight press effect on click */
box-shadow: none; box-shadow: none;
/* Remove shadow on click */ /* Remove shadow on click */
} }
.suicide_effect { .suicide_effect {
background: linear-gradient(to bottom right, #666666, #ccc); background: linear-gradient(to bottom right, #666666, #ccc);
color: red; color: red;
} }
</style> </style>
+27 -33
View File
@@ -1,65 +1,59 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Player } from '~/netcode/Player'; import type { Player } from "~/netcode/interfaces";
export interface Props { export interface Props {
player: Player player: Player;
} }
const props = defineProps<Props>() const props = defineProps<Props>();
</script> </script>
<template> <template>
<div class="turn_icon"> <div class="turn_icon">
<img src="/images/champion-shield.png" class="icon_img" draggable="false" /> <img src="/img/champion-shield.png" class="icon_img" draggable="false" />
<span class="turn_icon_number"> <span class="turn_icon_number">
{{ player.team?.health }} {{ player.team?.health }}
</span> </span>
</div> </div>
<!-- <div class="turn_icon" v-if="showTurnData"> <!-- <div class="turn_icon" v-if="showTurnData">
<img src="/images/gold.png" class="icon_img" draggable="false" /> <img src="/umg/gold.png" class="icon_img" draggable="false" />
<span class="turn_icon_number"> <span class="turn_icon_number">
{{ player.turn?.gold_reserve }} {{ player.turn?.gold_reserve }}
</span> </span>
</div> </div>
<div class="turn_icon" v-if="showTurnData"> <div class="turn_icon" v-if="showTurnData">
<img src="/images/damage.png" class="icon_img" draggable="false" /> <img src="/umg/damage.png" class="icon_img" draggable="false" />
<span class="turn_icon_number"> <span class="turn_icon_number">
{{ player.turn?.damage_reserve }} {{ player.turn?.damage_reserve }}
</span> </span>
</div> --> </div> -->
<div class="turn_icon"> <div class="turn_icon">
<img src="/images/fuck.svg" class="icon_img" draggable="false" /> <img src="/img/fuck.svg" class="icon_img" draggable="false" />
<span class="turn_icon_number">{{ (player.turn?.discard_markers ?? 0) + (player.turn?.fuck_u_markers ?? 0) <span class="turn_icon_number">{{
}}</span> (player.turn?.discard_markers ?? 0) + (player.turn?.fuck_u_markers ?? 0)
</div> }}</span>
</div>
</template> </template>
<style scoped> <style scoped>
.turn_icon { .turn_icon {
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
img.icon_img { img.icon_img {
height: 50px; height: 50px;
width: 50px; width: 50px;
} }
span.icon_img { span.icon_img {
font-size: 50px; font-size: 50px;
} }
.turn_icon_number { .turn_icon_number {
position: absolute; position: absolute;
text-shadow: white 0 0 1px; text-shadow: white 0 0 1px;
} }
</style> </style>
+24 -26
View File
@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Player } from '~/netcode/Player'; import type { Player } from "~/netcode/Player";
export interface Props { export interface Props {
isVisible: Boolean; isVisible: Boolean;
@@ -10,15 +9,14 @@ export interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
won: () => false, won: () => false,
players: () => [] players: () => [],
}) });
const emit = defineEmits(['close']) const emit = defineEmits(["close"]);
const close = () => { const close = () => {
reloadNuxtApp() reloadNuxtApp();
} };
</script> </script>
<template> <template>
@@ -30,24 +28,24 @@ const close = () => {
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<PlayerIcon :player="players[0]"></PlayerIcon> <PlayerIcon :player="players[0]"></PlayerIcon>
<p v-if="won"> <p v-if="won">Vous avez gagné!</p>
Vous avez gagné!
</p>
<p v-else> <p v-else>
Vous avez perdu face à ce magnifique individu qui a très clairement beaucoup plus de bitches que vous! Cela Vous avez perdu face à ce magnifique individu qui a très clairement
dit, je pense qu'il n'y a pas de bonnes beaucoup plus de bitches que vous! Cela dit, je pense qu'il n'y a pas
ou de mauvaises situations, juste des situations différentes. Vous avez perdu, mais vous avez gagné en de bonnes ou de mauvaises situations, juste des situations
expérience! C'est ça le plus important différentes. Vous avez perdu, mais vous avez gagné en expérience!
dans la vie, n'est-ce pas? Apprendre de ses erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez C'est ça le plus important dans la vie, n'est-ce pas? Apprendre de ses
heureux d'avoir eu l'opportunité erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez heureux
d'apprendre quelque chose aujourd'hui! Et puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend d'avoir eu l'opportunité d'apprendre quelque chose aujourd'hui! Et
plus fort"! Alors, allez-y, soyez fort! puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend plus
Et n'oubliez pas, la prochaine fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un champion! fort"! Alors, allez-y, soyez fort! Et n'oubliez pas, la prochaine
Vous êtes le meilleur! Vous êtes fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un
le roi du monde! champion! Vous êtes le meilleur! Vous êtes le roi du monde!
<img src="/images/so_back.png" alt="Une image décrivant le meme 'We are so back'" /> <img
src="/img/so_back.png"
alt="Une image décrivant le meme 'We are so back'"
/>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
@@ -68,7 +66,7 @@ const close = () => {
} }
.dialog-box { .dialog-box {
background-image: url('/images/bg-grass.png'); background-image: url("/img/bg-grass.png");
padding: 0 20px; padding: 0 20px;
border-radius: 5px; border-radius: 5px;
width: 800px; width: 800px;
@@ -84,7 +82,7 @@ const close = () => {
.dialog-box h3 { .dialog-box h3 {
font-size: 30px; font-size: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0; margin: 0;
width: 100%; width: 100%;
padding: 10px 0; padding: 10px 0;
+11
View File
@@ -64,3 +64,14 @@ export async function endTurn(game: string) {
} }
); );
} }
export async function lockCard(game: string, cardId: string) {
const config = useRuntimeConfig();
await $fetch<User>(
new URL(`games/${game}/turn/lockCard/${cardId}`, config.public.endpoint).href,
{
method: "POST",
credentials: "include",
}
);
}
Submodule netcode/interfaces deleted from 9248bf70af
+9 -1
View File
@@ -2,12 +2,20 @@ export interface GameObject {
_id: string; _id: string;
} }
export interface PlayingTurn {
currentTeam: string;
cardsLocked: string[];
effectsUsed: string[];
damage: number;
gold: number;
}
export interface Game extends GameObject { export interface Game extends GameObject {
fireGems: Container; fireGems: Container;
market: Container; market: Container;
marketStack: Container; marketStack: Container;
teams: Team[]; teams: Team[];
currentTurn: string; currentTurn: PlayingTurn;
} }
export interface Team extends GameObject { export interface Team extends GameObject {
+250 -192
View File
@@ -1,113 +1,156 @@
<script setup lang="ts"> <script setup lang="ts">
import LoadingScreen from '~/components/LoadingScreen.vue'; import LoadingScreen from "~/components/LoadingScreen.vue";
import { subscribe } from '~/netcode'; import { subscribe } from "~/netcode";
import { containerEvents, delay } from '~/netcode/events'; import { containerEvents, delay } from "~/netcode/events";
import { type Container, type Game, type Player } from '~/netcode/interfaces'; import { type Container, type Game, type Player } from "~/netcode/interfaces";
const config = useRuntimeConfig() const config = useRuntimeConfig();
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore();
const route = useRoute() const route = useRoute();
const gameId = route.params.id const gameId = route.params.id;
const { data: game, pending, error, refresh } = await useFetch<Game>( const {
new URL(`/games/${gameId}`, config.public.endpoint).href, data: game,
{ pending,
credentials: "include", error,
}, refresh,
) } = await useFetch<Game>(
new URL(`/games/${gameId}`, config.public.endpoint).href,
{
credentials: "include",
}
);
if (game.value == null) { if (game.value == null) {
navigateTo("/lobby"); navigateTo("/lobby");
throw new Error("game not found") throw new Error("game not found");
} }
const containers = computed<Record<string, Container>>(() => ({ const containers = computed<Record<string, Container>>(() => ({
[game.value!.fireGems._id]: game.value!.fireGems, [game.value!.fireGems._id]: game.value!.fireGems,
[game.value!.market._id]: game.value!.market, [game.value!.market._id]: game.value!.market,
[game.value!.marketStack._id]: game.value!.marketStack, [game.value!.marketStack._id]: game.value!.marketStack,
...Object.fromEntries( ...Object.fromEntries(
game.value!.teams.reduce( game
(acc, t) => acc.concat(t.players), [] as Player[] .value!.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
).map( .map((p) => [
p => [ [p.board._id, p.board],
[p.board._id, p.board], [p.discard._id, p.discard],
[p.discard._id, p.discard], [p.hand._id, p.hand],
[p.hand._id, p.hand], [p.stack._id, p.stack],
[p.stack._id, p.stack] ])
] .flat(1)
).flat(1) ),
) }));
}))
const eventSource = subscribe(`/games/${gameId}/subscribe`) const eventSource = subscribe(`/games/${gameId}/subscribe`);
const actions = ref<(() => Promise<any>)[]>([]) const actions = ref<(() => Promise<any>)[]>([]);
function queueAction(callback: (() => Promise<any>)) { function queueAction(callback: () => Promise<any>) {
actions.value.push(callback) actions.value.push(callback);
if (actions.value.length == 1) { if (actions.value.length == 1) {
execAction(callback) execAction(callback);
} }
} }
async function execAction(callback: (() => Promise<any>)) { async function execAction(callback: () => Promise<any>) {
await callback() await callback();
actions.value.shift() actions.value.shift();
if (actions.value.length > 0) { if (actions.value.length > 0) {
execAction(actions.value[0]) execAction(actions.value[0]);
} }
} }
eventSource.addEventListener("message", async (message) => { eventSource.addEventListener("message", async (message) => {
const data = JSON.parse(message.data) const current_game = game.value;
console.log(data) if (!current_game) {
if (data.type == 'start') { throw new Error("game not found");
throw new Error("not implemented") }
} else if (data.type == "populateContainer") { const data = JSON.parse(message.data);
throw new Error("not implemented") console.log(data);
} else if (data.type == "shuffleContainer") { if (data.type == "start") {
queueAction(async () => { throw new Error("not implemented");
await containerEvents.get(data.container)?.shuffleContainer() } else if (data.type == "populateContainer") {
}) queueAction(async () => {
} else if (data.type == "distributeCards") { await containerEvents.get(data.container)?.startAnimation();
// containerEvents.get(data.from)?.getCardsPosition(data.cards) });
const fromContainer = containers.value[data.container.id as string];
queueAction(async () => {
for (const card of data.container.cards) {
fromContainer.cards.push(card);
await delay(settingsStore.animationSpeed);
}
});
queueAction(async () => {
await containerEvents.get(data.container)?.endAnimation();
});
} else if (data.type == "shuffleContainer") {
queueAction(async () => {
await containerEvents.get(data.container)?.shuffleContainer();
});
} else if (data.type == "distributeCards") {
// containerEvents.get(data.from)?.getCardsPosition(data.cards)
queueAction(async () => { queueAction(async () => {
containerEvents.get(data.from)?.startAnimation() containerEvents.get(data.from)?.startAnimation();
containerEvents.get(data.to)?.startAnimation() containerEvents.get(data.to)?.startAnimation();
}) });
const fromContainer = containers.value[data.from as string] const fromContainer = containers.value[data.from as string];
const toContainer = containers.value[data.to as string] const toContainer = containers.value[data.to as string];
for (const card of data.cards) { for (const card of data.cards) {
queueAction(async () => { queueAction(async () => {
const index = fromContainer.cards.findIndex(c => c._id == card._id) const index = fromContainer.cards.findIndex((c) => c._id == card._id);
fromContainer.cards.splice(index, 1) fromContainer.cards.splice(index, 1);
await delay(settingsStore.animationSpeed) await delay(settingsStore.animationSpeed);
}) });
queueAction(async () => { queueAction(async () => {
toContainer.cards.push(card) toContainer.cards.push(card);
await delay(settingsStore.animationSpeed) await delay(settingsStore.animationSpeed);
}) });
}
queueAction(async () => {
containerEvents.get(data.from)?.endAnimation()
containerEvents.get(data.to)?.endAnimation()
await delay(settingsStore.animationSpeed * 2)
})
} else if (data.type == "endTurn") {
throw new Error("not implemented")
} }
queueAction(async () => {
containerEvents.get(data.from)?.endAnimation();
containerEvents.get(data.to)?.endAnimation();
await delay(settingsStore.animationSpeed * 2);
});
} else if (data.type == "endTurn") {
queueAction(async () => {
current_game.currentTurn.cardsLocked = [];
current_game.currentTurn.effectsUsed = [];
current_game.currentTurn.damage = 0;
current_game.currentTurn.gold = 0;
await delay(settingsStore.animationSpeed);
});
} else if (data.type == "lockCard") {
queueAction(async () => {
current_game.currentTurn.cardsLocked.push(data.card);
});
} else if (data.type == "useEffect") {
queueAction(async () => {
current_game.currentTurn.effectsUsed.push(data.effect);
});
} else if (data.type == "currentTurn.updateGold") {
queueAction(async () => {
console.log(`adding gold amount : ` + data.operation);
current_game.currentTurn.gold = data.gold;
});
}
}); });
const authStore = useAuthStore() const authStore = useAuthStore();
const playerList = computed(() =>
const playerList = computed(() => game.value!.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])) 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 selfPlayer = computed(() =>
authStore.logged
? playerList.value?.find((p) => p.user.userId == authStore.selfUser.userId)
: undefined
);
// TODO : current turn sound // TODO : current turn sound
@@ -115,24 +158,24 @@ const selfPlayer = computed(() => authStore.logged ? playerList.value?.find(p =>
// TODO : join sound // TODO : join sound
const showResults = ref(false) const showResults = ref(false);
const isWinner = ref(false) const isWinner = ref(false);
const loaded = ref(false) const loaded = ref(false);
function showLoserScreen() { function showLoserScreen() {
// playSound('/sounds/ding.mp3') // playSound('/sounds/ding.mp3')
showResults.value = true showResults.value = true;
isWinner.value = false isWinner.value = false;
} }
function showWinnerScreen() { function showWinnerScreen() {
// playSound('/sounds/ding.mp3') // playSound('/sounds/ding.mp3')
showResults.value = true showResults.value = true;
isWinner.value = true isWinner.value = true;
} }
const discard_unwrapped = ref(false) const discard_unwrapped = ref(false);
const focusedPlayer = ref<Player>(selfPlayer.value ?? playerList.value[0]) const focusedPlayer = ref<Player>(selfPlayer.value ?? playerList.value[0]);
// const clientStore = useClientSideStore() // const clientStore = useClientSideStore()
// const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target) // const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
@@ -140,7 +183,7 @@ const focusedPlayer = ref<Player>(selfPlayer.value ?? playerList.value[0])
// const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target); // const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
function focusPlayer(p: Player) { function focusPlayer(p: Player) {
focusedPlayer.value = p focusedPlayer.value = p;
} }
// watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => { // watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
@@ -153,159 +196,174 @@ function focusPlayer(p: Player) {
// }) // })
onUnmounted(() => { onUnmounted(() => {
eventSource.close() eventSource.close();
}) });
</script> </script>
<template> <template>
<div @contextmenu.prevent="false" id="game_board" v-if="game"> <div @contextmenu.prevent="false" id="game_board" v-if="game">
<AnimatedBackground @ready="loaded = true"></AnimatedBackground> <AnimatedBackground @ready="loaded = true"></AnimatedBackground>
<div id="stack_discard"> <div id="stack_discard">
<div id="stack"> <div id="stack">
<CardsGrouped :container="focusedPlayer.stack" :wrapped="true" :hidden="true"> <CardsGrouped
</CardsGrouped> :container="focusedPlayer.stack"
</div> :wrapped="true"
:hidden="true"
>
</CardsGrouped>
</div>
<div id="discard" :class="{ invisible: discard_unwrapped }" @click="() => discard_unwrapped = true"> <div
<CardsGrouped :container="focusedPlayer.discard" :wrapped="true"> id="discard"
</CardsGrouped> :class="{ invisible: discard_unwrapped }"
</div> @click="() => (discard_unwrapped = true)"
</div> >
<CardsGrouped :container="focusedPlayer.discard" :wrapped="true">
</CardsGrouped>
</div>
</div>
<MarketCards :market="game.market" :fire_gems="game.fireGems"></MarketCards> <MarketCards :market="game.market" :fire_gems="game.fireGems"></MarketCards>
<!-- <div id="player_list_container"> <!-- <div id="player_list_container">
<div id="player_list"> <div id="player_list">
<PlayerListElement v-for="p in playerList" :player="p" @click="focusPlayer(p)" :focused="focusedPlayer"> <PlayerListElement v-for="p in playerList" :player="p" @click="focusPlayer(p)" :focused="focusedPlayer">
</PlayerListElement> </PlayerListElement>
</div> </div>
</div> --> </div> -->
<div id="current_player"> <div id="current_player">
<PlayerSlot v-if="focusedPlayer" :player="focusedPlayer" :game="game!"></PlayerSlot> <PlayerSlot
</div> v-if="focusedPlayer"
<ResultDialog :isVisible="showResults" :won="isWinner" :players="playerList" @close="showResults = false"> :player="focusedPlayer"
</ResultDialog> :game="game!"
<GlobalOverlay></GlobalOverlay> ></PlayerSlot>
<SelfView v-if="selfPlayer" :container="selfPlayer.hand"></SelfView>
<LoadingScreen v-if="!loaded"></LoadingScreen>
</div> </div>
<ResultDialog
:isVisible="showResults"
:won="isWinner"
:players="playerList"
@close="showResults = false"
>
</ResultDialog>
<GlobalOverlay></GlobalOverlay>
<SelfView v-if="selfPlayer" :container="selfPlayer.hand"></SelfView>
<LoadingScreen v-if="!loaded"></LoadingScreen>
</div>
</template> </template>
<style scoped> <style scoped>
#game_status { #game_status {
position: fixed; position: fixed;
z-index: 10; z-index: 10;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, .75); background-color: rgba(255, 255, 255, 0.75);
} }
#game_board { #game_board {
display: grid; display: grid;
grid-template-columns: 1fr min-content 1fr; grid-template-columns: 1fr min-content 1fr;
grid-template-rows: min-content min-content 1fr; grid-template-rows: min-content min-content 1fr;
grid-template-areas: grid-template-areas:
"stack_discard market empty" "stack_discard market empty"
"player_list player_list player_list" "player_list player_list player_list"
"current_player current_player current_player"; "current_player current_player current_player";
height: 100%; height: 100%;
} }
#player_list, #player_list,
#current_player { #current_player {
overflow: auto; overflow: auto;
position: relative; position: relative;
isolation: isolate; isolation: isolate;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
/* background: url('/images/bg-darkgrass.png'); */ /* background: url('/img/bg-darkgrass.png'); */
} }
#current_player { #current_player {
grid-area: current_player; grid-area: current_player;
} }
#stack_discard { #stack_discard {
grid-area: stack_discard; grid-area: stack_discard;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
} }
#stack, #stack,
#discard { #discard {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
#discard { #discard {
cursor: pointer; cursor: pointer;
} }
.discard_overlay { .discard_overlay {
z-index: 9; z-index: 9;
display: flex; display: flex;
/* background: blue; */ /* background: blue; */
pointer-events: none; pointer-events: none;
gap: 2px; gap: 2px;
flex-wrap: wrap-reverse; flex-wrap: wrap-reverse;
align-content: flex-end; align-content: flex-end;
cursor: default; cursor: default;
position: absolute; position: absolute;
padding-bottom: inherit; padding-bottom: inherit;
padding-left: 40px; padding-left: 40px;
height: calc(100% - 64px); height: calc(100% - 64px);
padding-top: 64px; padding-top: 64px;
width: calc(100% -40px); width: calc(100% -40px);
margin-left: 0; margin-left: 0;
} }
#discard_unwrapped_bg { #discard_unwrapped_bg {
display: none; display: none;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 8; z-index: 8;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
pointer-events: all; pointer-events: all;
padding: inherit; padding: inherit;
} }
#discard_unwrapped_bg.unwrapped { #discard_unwrapped_bg.unwrapped {
display: block; display: block;
} }
#market { #market {
grid-area: market; grid-area: market;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
} }
#player_list_container { #player_list_container {
grid-area: player_list; grid-area: player_list;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
/* background-image: url("/images/bg-darkgrass.png"); */ /* background-image: url("/img/bg-darkgrass.png"); */
} }
#player_list { #player_list {
/* border: 5px solid #000; */ /* border: 5px solid #000; */
display: flex; display: flex;
} }
</style> </style>
<style> <style>
#player_list .card { #player_list .card {
--max-height: 90px; --max-height: 90px;
} }
</style> </style>
+217 -202
View File
@@ -1,273 +1,288 @@
<script setup lang="ts"> <script setup lang="ts">
// import { playSound } from '~/helpers/audioHelpers'; // import { playSound } from '~/helpers/audioHelpers';
import { playSound } from "~/helpers/audioHelpers"; import { playSound } from "~/helpers/audioHelpers";
import { createGame, getLobby, subscribe } from "~/netcode" import { createGame, getLobby, subscribe } from "~/netcode";
import type { Player } from "~/netcode/interfaces"; import type { Player } from "~/netcode/interfaces";
const authStore = useAuthStore() const authStore = useAuthStore();
const games = await getLobby() const games = await getLobby();
const eventSource = subscribe("/games/subscribe") const eventSource = subscribe("/games/subscribe");
eventSource.addEventListener("message", (message) => { eventSource.addEventListener("message", (message) => {
const data = JSON.parse(message.data) const data = JSON.parse(message.data);
if (data.type == "create") { if (data.type == "create") {
games.value.push(data) games.value.push(data);
} else if (data.type == "delete") { } else if (data.type == "delete") {
const index = games.value.findIndex(g => g._id == data._id) const index = games.value.findIndex((g) => g._id == data._id);
if (index == -1) { if (index == -1) {
throw new Error("Couldn't delete game") 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")
} }
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 erika_alive = ref(true);
const router = useRouter(); const router = useRouter();
function killErika() { function killErika() {
playSound('/sounds/splat1.mp3') playSound("/sounds/splat1.mp3");
erika_alive.value = false; erika_alive.value = false;
} }
onUnmounted(() => { onUnmounted(() => {
eventSource.close() eventSource.close();
}) });
</script> </script>
<template> <template>
<div class="background"> <div class="background">
<div class="title"> <div class="title">Entrez dans le Royaume de Héron !</div>
Entrez dans le Royaume de Héron !
<div class="middle-row">
<div class="create-game-zone">
<button @click="createGame">Create Game</button>
</div>
<div class="games-list-zone">
<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-else-if="
g.teams.every((t) =>
t.players.every(
(p) => p.user.userId !== authStore.selfUser.userId
)
)
"
>
SPECTATE
</template>
<template v-else>
<template v-if="!g.started"> JOIN BACK </template>
<template v-else> CONTINUE </template>
</template>
<br />
{{
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> </div>
</div>
<div class="middle-row">
<div class="create-game-zone">
<button @click="createGame">Create Game</button>
</div>
<div class="games-list-zone">
<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-else-if="g.teams.every(t => t.players.every(p => p.user.userId !== authStore.selfUser.userId))">
SPECTATE
</template>
<template v-else>
<template v-if="!g.started">
JOIN BACK
</template>
<template v-else>
CONTINUE
</template>
</template>
<br>
{{ 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>
</div>
</div>
<div class="overlay" v-if="erika_alive">
<div class="erika-zone">
<img @click="router.push('/rules')" class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/images/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse" />
<div class="overlay-button">
<button class="kill-erika-button" @click.stop="killErika()">Tuer Erika</button>
</div>
</div>
<div class="speech-bubble" @click.prevent.self>
<p>Ohoho!<br>Je suis Erika Furudo, la détective la plus forte du monde !</p>
<p>Je vais vous montrer comment jouer à Héron Realms !</p>
<p>Cliquez moi-dessus pour afficher les <span style="color: red">règles</span>.</p>
<p>Ou sinon, commencez direct en créant une partie ou rejoingnant une partie existante.</p>
</div>
</div>
</div> </div>
<div class="overlay" v-if="erika_alive">
<div class="erika-zone">
<img
@click="router.push('/rules')"
class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/img/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse"
/>
<div class="overlay-button">
<button class="kill-erika-button" @click.stop="killErika()">
Tuer Erika
</button>
</div>
</div>
<div class="speech-bubble" @click.prevent.self>
<p>
Ohoho!<br />Je suis Erika Furudo, la détective la plus forte du monde
!
</p>
<p>Je vais vous montrer comment jouer à Héron Realms !</p>
<p>
Cliquez moi-dessus pour afficher les
<span style="color: red">règles</span>.
</p>
<p>
Ou sinon, commencez direct en créant une partie ou rejoingnant une
partie existante.
</p>
</div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.background { .background {
background-color: #f0f0f0; background-color: #f0f0f0;
background-image: url('https://www.pockettactics.com/wp-content/sites/pockettactics/2023/01/fire-emblem-engage-review-29.jpg'); background-image: url("https://www.pockettactics.com/wp-content/sites/pockettactics/2023/01/fire-emblem-engage-review-29.jpg");
background-size: cover; background-size: cover;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
} }
.title { .title {
font-family: 'Kanit', 'Roboto', sans-serif; font-family: "Kanit", "Roboto", sans-serif;
font-size: 64px; font-size: 64px;
color: white; color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
margin-bottom: 100px; margin-bottom: 100px;
} }
.middle-row { .middle-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 100px; gap: 100px;
margin-bottom: 100px; margin-bottom: 100px;
} }
.create-game-zone { .create-game-zone {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.create-game-zone button { .create-game-zone button {
background-color: #4CAF50; background-color: #4caf50;
border: none; border: none;
color: white; color: white;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
font-family: 'Roboto', sans-serif; font-family: "Roboto", sans-serif;
font-size: 16px; font-size: 16px;
margin: auto; margin: auto;
cursor: pointer; cursor: pointer;
border-radius: 12px; border-radius: 12px;
padding: 10px 24px; padding: 10px 24px;
height: 50px; height: 50px;
transition-duration: 0.4s; transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 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); 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 { .games-list-zone {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
} }
.games-list-zone button { .games-list-zone button {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
border: none; border: none;
color: white; color: white;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
font-family: 'Roboto', sans-serif; font-family: "Roboto", sans-serif;
font-size: 16px; font-size: 16px;
margin: auto; margin: auto;
cursor: pointer; cursor: pointer;
border-radius: 12px; border-radius: 12px;
padding: 20px 24px; padding: 20px 24px;
height: 120px; height: 120px;
transition-duration: 0.4s; transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 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); 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 button.own_game { .games-list-zone button.own_game {
border: solid yellow 5px; border: solid yellow 5px;
} }
.overlay { .overlay {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
} }
.erika-zone { .erika-zone {
position: absolute; position: absolute;
top: 60%; top: 60%;
display: flex; display: flex;
justify-content: right; justify-content: right;
z-index: 2; z-index: 2;
height: 40%; height: 40%;
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
} }
.overlay-image { .overlay-image {
position: absolute; position: absolute;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
pointer-events: all; pointer-events: all;
} }
.overlay-button { .overlay-button {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
width: 300px; width: 300px;
box-sizing: border-box; box-sizing: border-box;
pointer-events: none; pointer-events: none;
} }
.kill-erika-button { .kill-erika-button {
display: none; display: none;
pointer-events: all; pointer-events: all;
background-color: #f44336; background-color: #f44336;
border: none; border: none;
color: white; color: white;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
font-family: 'Roboto', sans-serif; font-family: "Roboto", sans-serif;
font-size: 16px; font-size: 16px;
margin: auto; margin: auto;
cursor: pointer; cursor: pointer;
border-radius: 12px; border-radius: 12px;
padding: 10px 24px; padding: 10px 24px;
height: 50px; height: 50px;
transition-duration: 0.4s; transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 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); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
} }
.speech-bubble { .speech-bubble {
position: absolute; position: absolute;
top: 60%; top: 60%;
right: 260px; right: 260px;
z-index: 1; z-index: 1;
background-color: rgba(225, 225, 225, 0.8); background-color: rgba(225, 225, 225, 0.8);
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
font-family: 'Kanit', 'Roboto', sans-serif; font-family: "Kanit", "Roboto", sans-serif;
font-size: 16px; font-size: 16px;
color: black; color: black;
text-align: center; text-align: center;
max-width: 420px; max-width: 420px;
line-height: 1.5; line-height: 1.5;
} }
.erika-zone:hover .overlay-button .kill-erika-button { .erika-zone:hover .overlay-button .kill-erika-button {
display: inline-block; display: inline-block;
} }
</style> </style>
+364 -292
View File
@@ -1,320 +1,392 @@
<script setup lang="ts"> <script setup lang="ts">
const router = useRouter(); const router = useRouter();
</script> </script>
<template> <template>
<div class="background"> <div class="background">
<div class="overlay-2"> <div class="overlay-2">
<div class="erika-zone-2"> <div class="erika-zone-2">
<img @click="router.push({ path: '/lobby' })" class="overlay-image" <img
src="https://static.wikia.nocookie.net/umineko/images/9/9d/PC_Erika.Casual.Sprite_36.png" @click="router.push({ path: '/lobby' })"
alt="pétasse" /> class="overlay-image"
</div> src="https://static.wikia.nocookie.net/umineko/img/9/9d/PC_Erika.Casual.Sprite_36.png"
</div> alt="pétasse"
<div class="text-content"> />
<h1>Règles de Héron Realms</h1> </div>
<section>
<p>Héron Realms, c'est un jeu de <span style="text-decoration:line-through;">héron building</span>
deck-building.</p>
<p>Tout le monde part d'un même deck de base.</p>
<p>Le but du jeu est de l'enrichir pour obtenir de quoi gagner.</p>
<p>Et comme la violence est une solution universelle, ça se traduit en battre ses adversaires.</p>
<p>&#8203;</p>
<p>Et quoi de mieux pour faire ça que des cartes colorées?</p>
<div class="card-row">
<CardPreview :card_id="6" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
<CardPreview :card_id="36" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<hr>
<p>&#8203;</p>
<h2>Objectif</h2>
<p>L'unique objectif du jeu est d'être le dernier en lice. Pour y arriver, il faut descendre les Points
de Vie de vos adversaires à 0 en partant de 50.</p>
<p>
Le moyen principal d'y arriver, c'est de faire des <span style="color: red">Dégats</span> <img
src="/images/damage.png" class="icon_img" draggable="false" />. Tout le reste des mécaniques du
jeu sont là pour vous faciliter la tâche,
ou pour mettre des bâtons dans les roues de vos adversaires.
</p>
<p>&#8203;</p>
<hr>
<h2>Deckbuilding</h2>
<p>Comme c'est un jeu de "deckbuilding", on va expliquer ce qu'est le "deck" et ce qui nous permet de le
"build".</p>
<div class="card-row">
<CardPreview :card_id="undefined" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Le deck d'un joueur est-ce qui constitue sa pioche, qui lui permettra ensuite de constituer sa main
(la main est préparée à la fin du tour et permet de jouer le tour d'après).
</p>
<p>
Quand la pioche est vide, elle est recyclée à partir de sa défausse.
</p>
<p>
La défausse est ce qui contient les cartes déjà jouées. Mais aussi celles qui sont achetées! Elle
finit par être mélangée pour former la nouvelle pioche.
</p>
<p>&#8203;</p>
<hr>
<h2>Deck de base</h2>
<p>
Tout le monde commence avec 10 cartes divisées en 4 types:
</p>
<ul>
<li>7x Or (1)</li>
<li>1x Or (2)</li>
<li>1x Dégats (1)</li>
<li>1x Dégats (2)</li>
</ul>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="59" :tilted="false"></CardPreview>
<CardPreview :card_id="58" :tilted="false"></CardPreview>
<CardPreview :card_id="57" :tilted="false"></CardPreview>
</div>
<p>
La pioche est formée au hasard: on peut donc avoir une main de départ avec 5 or et 2 dégats, ou 3 or
et 4 dégats, ou 4 or et 3 dégats, etc.
</p>
<p>
A partir de sa main, on peut jouer des cartes. Ces cartes peuvent déclencher des effets: on peut
considérer qu'ils vont dans un "pool commun", et qu'on peut les utiliser dans
l'ordre qu'on souhaite. 1 de dégats et 2 de dégats me donnent 3 ressources de dégats que je split
comme je veux ensuite. Pareil pour l'or et pour le soin, qui sont les 3 ressources
principales du jeu.
</p>
<p>&#8203;</p>
<hr>
<h2>Types de cartes</h2>
<p>
Les cartes sont divisées en 3 catégories: objets, actions et champions.
</p>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="52" :tilted="false"></CardPreview>
<CardPreview :card_id="43" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<h3>Objets & actions</h3>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="52" :tilted="false"></CardPreview>
</div>
<p>
Par simplicité, disons que les objets et les actions sont pareil: on les joue, ils font un effet
disponible durant le tour actuel, et ils sont défaussés.
</p>
<h3>Champions (& gardes)</h3>
<div class="card-row">
<CardPreview :card_id="43" :tilted="false"></CardPreview>
</div>
<p>
Les champions sont un peu plus complexes: ils restent sur le terrain, et on peut les utiliser à
chaque tour (en les mobilisant).
</p>
<p>Ils ont une défense <img src="/images/champion-shield.png" style="height: 30px" draggable="false" />
indiquée en bas à droite et ils ne sont assommés (défaussés) que si on leur fait des dégats =
défense.</p>
<p>Ces dégats doivent être faits d'un coup. Je ne peux pas faire un peu de dégats pendant un tour, et un
peu le tour d'après pour les tuer.</p>
<div class="card-row">
<CardPreview :card_id="49" :tilted="false"></CardPreview>
</div>
<p>
Par ailleurs, certains champions sont des Gardes <img src="/images/champion-shield-guard.png"
style="height: 30px" draggable="false" /> (ils ont un bouclier noir), ce qui veut dire qu'on est
obligés de les tuer pour faire des dégats à des champions non-gardes ou au joueur.
</p>
<p>
(J'en profite pour préciser qu'on peut en effet, sans garde, choisir d'attaquer le joueur
directement ou ses champions.)
Incidentellement, ça veut dire qu'avec seulement 2 de dégats, je ne peux rien faire à un joueur avec
un garde à 3 de défense.
</p>
<p>&#8203;</p>
<hr>
<h2>Building</h2>
<p>
Passons au "building": on peut acheter des cartes pour les ajouter à notre défausse qui, si vous
avez suivi, constituera la prochaine pioche.
Avec le jeu de base, <span style="color: darkgoldenrod">l'or</span> <img src="/images/gold.png"
class="icon_img" draggable="false" /> sert exclusivement à accomplir ce but: acheter des cartes.
</p>
<div class="card-row">
<CardPreview :card_id="6" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
<CardPreview :card_id="36" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
</div>
<p>
Un marché est mis à disposition de tous les joueurs, mettant à disposition 5 cartes (pas ici parce
que j'ai pas la place).
</p>
<p>
Ces cartes ont chacune un coût indiqué en haut à droite.
Il faut au moins autant d'or que ce qui est indiqué pour acheter une carte. Une carte achetée laisse
la place à une nouvelle carte directement, qu'on peut
décider d'acheter juste après (tant qu'on a l'argent).
</p>
<p>
Il ne nous reste plus qu'à comprendre ce que font les cartes du marché, puisqu'elles sont plus
complexes (et bien plus fortes) que votre deck de base.
Prenons des exemples!
</p>
<hr>
<h2>Couleurs</h2>
<p>
La première chose qui ne vous échappera pas, c'est qu'on a 4 couleurs différentes pour les cartes.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="13" :tilted="false"></CardPreview>
<CardPreview :card_id="3" :tilted="false"></CardPreview>
<CardPreview :card_id="1" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: darkgoldenrod">jaunes</span> sont les cartes Empire.</p>
<p>Elles font beaucoup de soin, et activent souvent des effets de pioche.</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="39" :tilted="false"></CardPreview>
<CardPreview :card_id="34" :tilted="false"></CardPreview>
<CardPreview :card_id="40" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: red">rouges</span> sont les cartes Necros.</p>
<p>Elles sont les seules à pouvoir sacrifier des cartes pour les sortir de son deck de façon permanente.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="52" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
<CardPreview :card_id="47" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: darkgreen">vertes</span> sont les cartes Sauvages.</p>
<p>Elles font beaucoup de dégats, et forcent vos adversaires à jouer moins de cartes.</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="24" :tilted="false"></CardPreview>
<CardPreview :card_id="25" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: darkblue">bleues</span> sont les cartes Guilde.</p>
<p>Elles donnent beaucoup d'or, assomment les champions adverses et manipulent votre pioche.</p>
<p>&#8203;</p>
<hr>
<h2>Déclencheurs</h2>
<p>Les effets de cartes peuvent se déclencher pour 3 raisons différentes.</p>
<div class="card-row">
<CardPreview :card_id="14" :tilted="false"></CardPreview>
</div>
<p>Sur cette carte, on peut voir les trois en même temps.</p>
<p>La 1ère ligne s'active tout le temps.</p>
<p>La 2ème ligne est une capacité Allié: il faut une autre carte de la même couleur en jeu pour pouvoir
en profiter.</p>
<p>La 3ème ligne est un Effet Poubelle (différent du sacrifice des cartes rouges): vous pouvez retirer
cette carte de votre jeu pour bénéficier, en plus des autres effets, de l'effet marqué. Mais la
carte sera défaussée à tout jamais.</p>
<p>&#8203;</p>
<hr>
<h2>Effets</h2>
<p>Une variété d'effets peuvent être inclus dans les cartes.</p>
<p>Il y en a plein et les expliquer ici serait relou, alors je vous laisse poser la question en vocal si
vous n'en comprenez pas un.</p>
<p>&#8203;</p>
<hr>
<h2>Activer les cartes dans la version en ligne</h2>
<p>Il suffit de cliquer sur la carte que vous voulez activer dans votre main pour la "LOCK".</p>
<p>Une carte "LOCKED" est considérée en jeu, & peut activer des effets Allié pour d'autres cartes, etc.
</p>
<p>Si vous avez des choix à faire sur la carte, un menu apparaîtra pour vous laisser choisir.</p>
<p>Pour faire des dommages, il y aura un bouton "Damage" sur vos ennemis. Pareil sur leurs champions.
</p>
<p>Quand vous avez un effet "Sacrifice", vous pouvez fouiller votre défausse pour choisir la carte à
sacrifier. Vous pouvez également sacrifier une carte de la main qui n'est pas encore locked!</p>
<h2>Zoomer sur les cartes</h2>
<p>Pour zoomer sur une carte, maintenez votre clic droit.</p>
</section>
</div>
</div> </div>
<div class="text-content">
<h1>Règles de Héron Realms</h1>
<section>
<p>
Héron Realms, c'est un jeu de
<span style="text-decoration: line-through">héron building</span>
deck-building.
</p>
<p>Tout le monde part d'un même deck de base.</p>
<p>Le but du jeu est de l'enrichir pour obtenir de quoi gagner.</p>
<p>
Et comme la violence est une solution universelle, ça se traduit en
battre ses adversaires.
</p>
<p>&#8203;</p>
<p>Et quoi de mieux pour faire ça que des cartes colorées?</p>
<div class="card-row">
<CardPreview :card_id="6" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
<CardPreview :card_id="36" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<hr />
<p>&#8203;</p>
<h2>Objectif</h2>
<p>
L'unique objectif du jeu est d'être le dernier en lice. Pour y
arriver, il faut descendre les Points de Vie de vos adversaires à 0 en
partant de 50.
</p>
<p>
Le moyen principal d'y arriver, c'est de faire des
<span style="color: red">Dégats</span>
<img src="/img/damage.png" class="icon_img" draggable="false" />. Tout
le reste des mécaniques du jeu sont là pour vous faciliter la tâche,
ou pour mettre des bâtons dans les roues de vos adversaires.
</p>
<p>&#8203;</p>
<hr />
<h2>Deckbuilding</h2>
<p>
Comme c'est un jeu de "deckbuilding", on va expliquer ce qu'est le
"deck" et ce qui nous permet de le "build".
</p>
<div class="card-row">
<CardPreview :card_id="undefined" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Le deck d'un joueur est-ce qui constitue sa pioche, qui lui permettra
ensuite de constituer sa main (la main est préparée à la fin du tour
et permet de jouer le tour d'après).
</p>
<p>
Quand la pioche est vide, elle est recyclée à partir de sa défausse.
</p>
<p>
La défausse est ce qui contient les cartes déjà jouées. Mais aussi
celles qui sont achetées! Elle finit par être mélangée pour former la
nouvelle pioche.
</p>
<p>&#8203;</p>
<hr />
<h2>Deck de base</h2>
<p>Tout le monde commence avec 10 cartes divisées en 4 types:</p>
<ul>
<li>7x Or (1)</li>
<li>1x Or (2)</li>
<li>1x Dégats (1)</li>
<li>1x Dégats (2)</li>
</ul>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="59" :tilted="false"></CardPreview>
<CardPreview :card_id="58" :tilted="false"></CardPreview>
<CardPreview :card_id="57" :tilted="false"></CardPreview>
</div>
<p>
La pioche est formée au hasard: on peut donc avoir une main de départ
avec 5 or et 2 dégats, ou 3 or et 4 dégats, ou 4 or et 3 dégats, etc.
</p>
<p>
A partir de sa main, on peut jouer des cartes. Ces cartes peuvent
déclencher des effets: on peut considérer qu'ils vont dans un "pool
commun", et qu'on peut les utiliser dans l'ordre qu'on souhaite. 1 de
dégats et 2 de dégats me donnent 3 ressources de dégats que je split
comme je veux ensuite. Pareil pour l'or et pour le soin, qui sont les
3 ressources principales du jeu.
</p>
<p>&#8203;</p>
<hr />
<h2>Types de cartes</h2>
<p>
Les cartes sont divisées en 3 catégories: objets, actions et
champions.
</p>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="52" :tilted="false"></CardPreview>
<CardPreview :card_id="43" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<h3>Objets & actions</h3>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="52" :tilted="false"></CardPreview>
</div>
<p>
Par simplicité, disons que les objets et les actions sont pareil: on
les joue, ils font un effet disponible durant le tour actuel, et ils
sont défaussés.
</p>
<h3>Champions (& gardes)</h3>
<div class="card-row">
<CardPreview :card_id="43" :tilted="false"></CardPreview>
</div>
<p>
Les champions sont un peu plus complexes: ils restent sur le terrain,
et on peut les utiliser à chaque tour (en les mobilisant).
</p>
<p>
Ils ont une défense
<img
src="/img/champion-shield.png"
style="height: 30px"
draggable="false"
/>
indiquée en bas à droite et ils ne sont assommés (défaussés) que si on
leur fait des dégats = défense.
</p>
<p>
Ces dégats doivent être faits d'un coup. Je ne peux pas faire un peu
de dégats pendant un tour, et un peu le tour d'après pour les tuer.
</p>
<div class="card-row">
<CardPreview :card_id="49" :tilted="false"></CardPreview>
</div>
<p>
Par ailleurs, certains champions sont des Gardes
<img
src="/img/champion-shield-guard.png"
style="height: 30px"
draggable="false"
/>
(ils ont un bouclier noir), ce qui veut dire qu'on est obligés de les
tuer pour faire des dégats à des champions non-gardes ou au joueur.
</p>
<p>
(J'en profite pour préciser qu'on peut en effet, sans garde, choisir
d'attaquer le joueur directement ou ses champions.) Incidentellement,
ça veut dire qu'avec seulement 2 de dégats, je ne peux rien faire à un
joueur avec un garde à 3 de défense.
</p>
<p>&#8203;</p>
<hr />
<h2>Building</h2>
<p>
Passons au "building": on peut acheter des cartes pour les ajouter à
notre défausse qui, si vous avez suivi, constituera la prochaine
pioche. Avec le jeu de base,
<span style="color: darkgoldenrod">l'or</span>
<img src="/img/gold.png" class="icon_img" draggable="false" /> sert
exclusivement à accomplir ce but: acheter des cartes.
</p>
<div class="card-row">
<CardPreview :card_id="6" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
<CardPreview :card_id="36" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
</div>
<p>
Un marché est mis à disposition de tous les joueurs, mettant à
disposition 5 cartes (pas ici parce que j'ai pas la place).
</p>
<p>
Ces cartes ont chacune un coût indiqué en haut à droite. Il faut au
moins autant d'or que ce qui est indiqué pour acheter une carte. Une
carte achetée laisse la place à une nouvelle carte directement, qu'on
peut décider d'acheter juste après (tant qu'on a l'argent).
</p>
<p>
Il ne nous reste plus qu'à comprendre ce que font les cartes du
marché, puisqu'elles sont plus complexes (et bien plus fortes) que
votre deck de base. Prenons des exemples!
</p>
<hr />
<h2>Couleurs</h2>
<p>
La première chose qui ne vous échappera pas, c'est qu'on a 4 couleurs
différentes pour les cartes.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="13" :tilted="false"></CardPreview>
<CardPreview :card_id="3" :tilted="false"></CardPreview>
<CardPreview :card_id="1" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: darkgoldenrod">jaunes</span> sont les
cartes Empire.
</p>
<p>
Elles font beaucoup de soin, et activent souvent des effets de pioche.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="39" :tilted="false"></CardPreview>
<CardPreview :card_id="34" :tilted="false"></CardPreview>
<CardPreview :card_id="40" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: red">rouges</span> sont les cartes
Necros.
</p>
<p>
Elles sont les seules à pouvoir sacrifier des cartes pour les sortir
de son deck de façon permanente.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="52" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
<CardPreview :card_id="47" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: darkgreen">vertes</span> sont les
cartes Sauvages.
</p>
<p>
Elles font beaucoup de dégats, et forcent vos adversaires à jouer
moins de cartes.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="24" :tilted="false"></CardPreview>
<CardPreview :card_id="25" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: darkblue">bleues</span> sont les cartes
Guilde.
</p>
<p>
Elles donnent beaucoup d'or, assomment les champions adverses et
manipulent votre pioche.
</p>
<p>&#8203;</p>
<hr />
<h2>Déclencheurs</h2>
<p>
Les effets de cartes peuvent se déclencher pour 3 raisons différentes.
</p>
<div class="card-row">
<CardPreview :card_id="14" :tilted="false"></CardPreview>
</div>
<p>Sur cette carte, on peut voir les trois en même temps.</p>
<p>La 1ère ligne s'active tout le temps.</p>
<p>
La 2ème ligne est une capacité Allié: il faut une autre carte de la
même couleur en jeu pour pouvoir en profiter.
</p>
<p>
La 3ème ligne est un Effet Poubelle (différent du sacrifice des cartes
rouges): vous pouvez retirer cette carte de votre jeu pour bénéficier,
en plus des autres effets, de l'effet marqué. Mais la carte sera
défaussée à tout jamais.
</p>
<p>&#8203;</p>
<hr />
<h2>Effets</h2>
<p>Une variété d'effets peuvent être inclus dans les cartes.</p>
<p>
Il y en a plein et les expliquer ici serait relou, alors je vous
laisse poser la question en vocal si vous n'en comprenez pas un.
</p>
<p>&#8203;</p>
<hr />
<h2>Activer les cartes dans la version en ligne</h2>
<p>
Il suffit de cliquer sur la carte que vous voulez activer dans votre
main pour la "LOCK".
</p>
<p>
Une carte "LOCKED" est considérée en jeu, & peut activer des effets
Allié pour d'autres cartes, etc.
</p>
<p>
Si vous avez des choix à faire sur la carte, un menu apparaîtra pour
vous laisser choisir.
</p>
<p>
Pour faire des dommages, il y aura un bouton "Damage" sur vos ennemis.
Pareil sur leurs champions.
</p>
<p>
Quand vous avez un effet "Sacrifice", vous pouvez fouiller votre
défausse pour choisir la carte à sacrifier. Vous pouvez également
sacrifier une carte de la main qui n'est pas encore locked!
</p>
<h2>Zoomer sur les cartes</h2>
<p>Pour zoomer sur une carte, maintenez votre clic droit.</p>
</section>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.background { .background {
background-color: #f0f0f0; background-color: #f0f0f0;
background-image: url('/images/bg-darkgrass.png'); background-image: url("/img/bg-darkgrass.png");
background-size: auto; background-size: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100vw; width: 100vw;
;
} }
.text-content { .text-content {
background-color: rgba(255, 255, 255, .75); background-color: rgba(255, 255, 255, 0.75);
padding: 1em; padding: 1em;
border-radius: 1em; border-radius: 1em;
max-width: 800px; max-width: 800px;
margin-top: 2em; margin-top: 2em;
text-align: left; text-align: left;
h1 { h1 {
text-align: center; text-align: center;
font-size: 2em; font-size: 2em;
margin-bottom: 1.5em; margin-bottom: 1.5em;
} }
p { p {
margin: 1em 0; margin: 1em 0;
font-size: 1.2em; font-size: 1.2em;
} }
} }
.card-row { .card-row {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 1em; gap: 1em;
} }
.overlay-2 { .overlay-2 {
height: 00px; height: 00px;
width: 100%; width: 100%;
position: sticky; position: sticky;
z-index: 2; z-index: 2;
top: 0; top: 0;
pointer-events: none; pointer-events: none;
} }
.erika-zone-2 { .erika-zone-2 {
position: absolute; position: absolute;
top: 300px; top: 300px;
display: flex; display: flex;
justify-content: right; justify-content: right;
z-index: 2; z-index: 2;
height: 300px; height: 300px;
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
} }
.overlay-image { .overlay-image {
position: sticky; position: sticky;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
pointer-events: all; pointer-events: all;
} }
</style> </style>
+2 -1
View File
@@ -1,4 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { Game } from "~/netcode/interfaces";
// import goStore, { useEffect } from "~/netcode"; // import goStore, { useEffect } from "~/netcode";
// import type { Card } from "~/netcode/Card"; // import type { Card } from "~/netcode/Card";
// import { CardEffects, type Effect } from "~/netcode/Effect"; // import { CardEffects, type Effect } from "~/netcode/Effect";
@@ -10,7 +11,7 @@ import { defineStore } from "pinia";
// useClientSideStore().activatedClientside = [] // useClientSideStore().activatedClientside = []
// }) // })
export const useClientSideStore = defineStore("clientside", { export const useTurnStore = defineStore("turn", {
// state: () => ({ // state: () => ({
// activatedClientside: [] as Effect[], // activatedClientside: [] as Effect[],
// effectsHandled: new Set([ // effectsHandled: new Set([