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">
const settingsStore = useSettingsStore()
const settingsStore = useSettingsStore();
import gsap from 'gsap';
import ShaderWorker from '~/assets/workers/ShaderWorker?worker'
import type { Game } from '~/netcode/interfaces';
import gsap from "gsap";
import ShaderWorker from "~/assets/workers/ShaderWorker?worker";
import type { Game } from "~/netcode/interfaces";
const emit = defineEmits(["ready"])
const worker = new ShaderWorker()
const emit = defineEmits<{
ready: [];
}>();
const canvas = ref<HTMLCanvasElement | null>(null)
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null)
const worker = new ShaderWorker();
const canvas = ref<HTMLCanvasElement | null>(null);
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null);
export interface Props {
colorStart?: [number, number, number]
colorEnd?: [number, number, number]
colorStart?: [number, number, number];
colorEnd?: [number, number, number];
}
const props = withDefaults(defineProps<Props>(), {
colorStart: () => [0, .3, 0],
colorEnd: () => [0, 1, 0]
})
const _colorStart = reactive(props.colorStart)
const _colorEnd = reactive(props.colorEnd)
colorStart: () => [0, 0.3, 0],
colorEnd: () => [0, 1, 0],
});
const _colorStart = reactive(props.colorStart);
const _colorEnd = reactive(props.colorEnd);
function initShader() {
return new Promise<void>((resolve, reject) => {
if (!canvas.value) {
reject("canvas is null");
return
}
const offscreen = canvas.value?.transferControlToOffscreen()
worker.postMessage({
command: "initShader",
canvas: offscreen,
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
}, [offscreen])
if (settingsStore.shaderFramerate == 0) {
killShader()
}
resizeShader()
worker.onmessage = (e) => {
if (e.data == "ready") {
resolve()
}
}
})
return new Promise<void>((resolve, reject) => {
if (!canvas.value) {
reject("canvas is null");
return;
}
const offscreen = canvas.value?.transferControlToOffscreen();
worker.postMessage(
{
command: "initShader",
canvas: offscreen,
screen: {
height: window.innerHeight * devicePixelRatio,
width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
},
[offscreen]
);
if (settingsStore.shaderFramerate == 0) {
killShader();
}
resizeShader();
worker.onmessage = (e) => {
if (e.data == "ready") {
resolve();
}
};
});
}
function recolorShader() {
worker.postMessage({
command: 'recolorShader',
colorStart: [..._colorStart],
colorEnd: [..._colorEnd]
})
worker.postMessage({
command: "recolorShader",
colorStart: [..._colorStart],
colorEnd: [..._colorEnd],
});
}
function resizeShader() {
worker.postMessage({
command: 'resizeShader',
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
worker.postMessage({
command: "resizeShader",
screen: {
height: window.innerHeight * devicePixelRatio,
width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
}
function killShader() {
worker.postMessage({
command: 'killShader'
})
worker.postMessage({
command: "killShader",
});
}
function resumeShader() {
worker.postMessage({
command: 'resumeShader',
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
worker.postMessage({
command: "resumeShader",
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
}
watch(() => [settingsStore.shaderFramerate], () => {
watch(
() => [settingsStore.shaderFramerate],
() => {
if (settingsStore.shaderFramerate == 0) {
killShader()
return
killShader();
return;
}
resumeShader()
resumeShader();
}
);
})
watch(() => [settingsStore.shaderQuality], () => {
resizeShader()
})
watch(
() => [settingsStore.shaderQuality],
() => {
resizeShader();
}
);
// watch(() => [props.game.currentTurn], () => {
// if (props.game.teams.find() != goStore.self?.team) {
@@ -106,38 +129,43 @@ watch(() => [settingsStore.shaderQuality], () => {
// }
// }, { immediate: true })
watch([_colorEnd, _colorStart], () => {
recolorShader()
})
recolorShader();
});
onMounted(async () => {
recolorShader()
await initShader()
window.addEventListener("resize", resizeShader, true)
emit("ready")
})
recolorShader();
await initShader();
window.addEventListener("resize", resizeShader, true);
emit("ready");
});
onUnmounted(() => {
window.removeEventListener("resize", resizeShader)
killShader()
})
window.removeEventListener("resize", resizeShader);
killShader();
});
</script>
<template>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
height="270">test</canvas>
<canvas
ref="canvas"
id="background"
:class="{ pixelated: settingsStore.shaderPixelated }"
width="480"
height="270"
>test</canvas
>
</template>
<style scoped>
#background {
left: 0;
position: absolute;
height: 100%;
width: 100%;
left: 0;
position: absolute;
height: 100%;
width: 100%;
}
#background.pixelated {
image-rendering: pixelated;
image-rendering: pixelated;
}
</style>
</style>
+12 -46
View File
@@ -1,6 +1,4 @@
<script setup lang="ts">
// import goStore from '~/netcode';
// import { useSettingsStore } from '~/stores/settingsStore';
export interface Props {
uuid?: string
@@ -90,9 +88,6 @@ onMounted(() => {
</div>
</template>
<style scoped>
@@ -121,47 +116,6 @@ onMounted(() => {
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 {
height: 6px;
margin: 0;
@@ -171,6 +125,14 @@ onMounted(() => {
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 {
transform: scale(200%);
z-index: 10;
@@ -194,6 +156,10 @@ onMounted(() => {
}
</style>
<style>
.card.unlocked>* {
pointer-events: none;
}
#contextMenuButtons>* {
flex: 1;
border-radius: 12px;
+88 -72
View File
@@ -1,133 +1,149 @@
<script setup lang="ts">
import { containerEvents, delay } from '~/netcode/events';
import type { Container } from '~/netcode/interfaces';
import { containerEvents, delay } from "~/netcode/events";
import type { Container, PlayingTurn } from "~/netcode/interfaces";
export interface Props {
container: Container
wrapped?: boolean
hidden?: boolean
container: Container;
playingTurn?: PlayingTurn;
wrapped?: boolean;
hidden?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
wrapped: () => false,
hidden: () => false
})
wrapped: () => false,
hidden: () => false,
});
const settingsStore = useSettingsStore()
const emit = defineEmits(["cardClick"]);
const settingsStore = useSettingsStore();
const cards = computed(() => {
if (props.hidden) {
return props.container.cards.map((_v, i) => ({ _id: i, cardId: undefined }))
}
return props.container.cards
})
if (props.hidden) {
return props.container.cards.map((_v, i) => ({
_id: i,
cardId: undefined,
}));
}
return props.container.cards;
});
const animationSpeed = computed(() => settingsStore.animationSpeed + 'ms')
const shuffleAnimationSpeed = computed(() => settingsStore.animationSpeed * 2 + 'ms')
function populateContainer() {
const animationSpeed = computed(() => settingsStore.animationSpeed + "ms");
const shuffleAnimationSpeed = computed(
() => settingsStore.animationSpeed * 2 + "ms"
);
function populateContainer() {}
}
const rotate = ref(false)
const rotate = ref(false);
async function shuffleContainer() {
console.log("rotate")
rotate.value = true
await delay(settingsStore.animationSpeed * 2)
rotate.value = false
await delay(settingsStore.animationSpeed * 2)
rotate.value = true;
await delay(settingsStore.animationSpeed * 2);
rotate.value = false;
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() {
animationCallbacks.value.forEach(p => p())
animationCallbacks.value = []
animationCallbacks.value.forEach((p) => p());
animationCallbacks.value = [];
}
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) {
animationCallbacks.value.push(done)
animationCallbacks.value.push(done);
}
async function onLeave(_el: any, done: () => void) {
animationCallbacks.value.push(done)
animationCallbacks.value.push(done);
}
async function onBeforeEnter() {
}
async function onBeforeEnter() {}
onUnmounted(() => {
containerEvents.delete(props.container._id)
})
containerEvents.delete(props.container._id);
});
</script>
<template>
<div class="cards_container" :class="{ wrapped, rotate }">
<TransitionGroup name="list" @enter="onEnter" @leave="onLeave" @before-enter="onBeforeEnter">
<CardPreview :card_id="c.cardId" :wrapped="wrapped" v-for="c in cards" :key="c._id"
:uuid="c._id.toString()">
<slot></slot>
</CardPreview>
</TransitionGroup>
<!-- <img :class="`card_image`" :src="'/cards/background.png'" draggable="false"> -->
</div>
<div class="cards_container" :class="{ wrapped, rotate }">
<TransitionGroup
name="list"
@enter="onEnter"
@leave="onLeave"
@before-enter="onBeforeEnter"
>
<CardPreview
@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>
<style scoped>
.list-enter-active,
.list-leave-active {
transition: all v-bind('animationSpeed') ease;
transition: all v-bind("animationSpeed") ease;
}
.list-leave-to,
.list-enter-from {
opacity: 0;
transform: translateX(30px);
opacity: 0;
transform: translateX(30px);
}
.rotate {
rotate: 360deg;
rotate: 360deg;
}
.cards_container {
display: flex;
flex-direction: row;
justify-content: center;
min-width: 200px;
transition: rotate v-bind('shuffleAnimationSpeed') ease;
display: flex;
flex-direction: row;
justify-content: center;
min-width: 200px;
transition: rotate v-bind("shuffleAnimationSpeed") ease;
}
.cards_container.wrapped {
flex-direction: column-reverse;
flex-direction: column-reverse;
}
.cards_container.wrapped:before {
height: calc(250px - 10px);
display: block;
content: "";
width: 0;
height: calc(250px - 10px);
display: block;
content: "";
width: 0;
}
.empty {
max-height: 10px;
max-height: 10px;
}
.empty>.card_image {
filter: brightness(0.5);
max-height: 250px;
.empty > .card_image {
filter: brightness(0.5);
max-height: 250px;
}
</style>
</style>
+21 -18
View File
@@ -1,30 +1,33 @@
<script setup lang="ts">
const displayOptions = ref(false)
const displayOptions = ref(false);
</script>
<template>
<div :class="`card_overlay`" ref="card">
<img class="settings_button" src="/images/settings.png" alt="settings" @click="displayOptions = true" />
</div>
<OptionsDialog :isVisible="displayOptions" @close="displayOptions = false" />
<div :class="`card_overlay`" ref="card">
<img
class="settings_button"
src="/img/settings.png"
alt="settings"
@click="displayOptions = true"
/>
</div>
<OptionsDialog :isVisible="displayOptions" @close="displayOptions = false" />
</template>
<style scoped>
.card_overlay {
/* display: none; */
position: fixed;
z-index: 999;
display: flex;
pointer-events: none;
/* top: left: */
top: 0;
/* display: none; */
position: fixed;
z-index: 999;
display: flex;
pointer-events: none;
/* top: left: */
top: 0;
}
.settings_button {
pointer-events: all;
width: 30px;
padding: 5px;
pointer-events: all;
width: 30px;
padding: 5px;
}
</style>
</style>
+41 -48
View File
@@ -1,16 +1,14 @@
<script setup lang="ts">
import type { Container } from '~/netcode/interfaces';
import type { Container } from "~/netcode/interfaces";
export interface Props {
market: Container
fire_gems: Container
market: 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)
@@ -19,58 +17,53 @@ const authStore = useAuthStore()
// const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
function onBeforeEnter(el: Element) {
(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 marketCardClick(e: string) {
console.log(e);
}
function onEnter(el: any, done: () => unknown) {
console.log("enter")
// setTimeout(() => {
// el.style.visibility = null
// done()
// }, 1000)
done()
function fireGemCardClick(e: string) {
console.log(e);
}
</script>
<template>
<div id="market">
<CardPreview :tilted="false" :card_id="undefined"></CardPreview>
<div class="separator"></div>
<CardsGrouped :container="market"></CardsGrouped>
<div class="separator"></div>
<CardsGrouped :container="fire_gems" :wrapped="true"></CardsGrouped>
</div>
<div id="market">
<CardPreview :tilted="false" :card_id="undefined"></CardPreview>
<div class="separator"></div>
<CardsGrouped
:container="market"
@cardClick="marketCardClick"
></CardsGrouped>
<div class="separator"></div>
<CardsGrouped
:container="fire_gems"
:wrapped="true"
@cardClick="fireGemCardClick"
></CardsGrouped>
</div>
</template>
<style scoped>
#market {
width: min-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 2;
position: relative;
gap: 2px;
margin: 10px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60));
backdrop-filter: blur(5px) brightness(80%);
background: rgba(155, 62, 62, 0.46);
width: min-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 2;
position: relative;
gap: 2px;
margin: 10px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
backdrop-filter: blur(5px) brightness(80%);
background: rgba(155, 62, 62, 0.46);
}
.separator {
width: 50px;
width: 50px;
}
</style>
</style>
+52 -23
View File
@@ -1,23 +1,20 @@
<script setup lang="ts">
const settingsStore = useSettingsStore()
const settingsStore = useSettingsStore();
export interface Props {
isVisible: boolean;
}
const _devicePixelRatio = computed(() => devicePixelRatio)
const _window = computed(() => window)
const _devicePixelRatio = computed(() => devicePixelRatio);
const _window = computed(() => window);
const props = withDefaults(defineProps<Props>(), {
})
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits(['close'])
const emit = defineEmits(["close"]);
const close = () => {
emit('close')
}
emit("close");
};
</script>
<template>
@@ -30,7 +27,9 @@ const close = () => {
<span>Toggle Card Style</span>
</div>
<div class="dialog-body">
<button @click="settingsStore.switchCardStyles">Toggle Card style</button>
<button @click="settingsStore.switchCardStyles">
Toggle Card style
</button>
</div>
<div class="dialog-footer">
<span>{{ settingsStore.cardStyle }}</span>
@@ -39,7 +38,13 @@ const close = () => {
<span>Animation Speed</span>
</div>
<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 class="dialog-footer">
<span>{{ settingsStore.animationSpeed }}ms</span>
@@ -51,19 +56,44 @@ const close = () => {
<span>Shader Quality</span>
</div>
<div class="dialog-body">
<input type="range" min="1" max="10" v-model="settingsStore.shaderQuality"
:disabled="settingsStore.shaderFramerate == 0">
<input
type="range"
min="1"
max="10"
v-model="settingsStore.shaderQuality"
:disabled="settingsStore.shaderFramerate == 0"
/>
</div>
<div class="dialog-footer">
<span>{{ Math.floor(_window.innerWidth * _devicePixelRatio * settingsStore.shaderQuality / 10) }}x{{
Math.floor(_window.innerHeight *
_devicePixelRatio * settingsStore.shaderQuality / 10) }}</span>
<span
>{{
Math.floor(
(_window.innerWidth *
_devicePixelRatio *
settingsStore.shaderQuality) /
10
)
}}x{{
Math.floor(
(_window.innerHeight *
_devicePixelRatio *
settingsStore.shaderQuality) /
10
)
}}</span
>
</div>
<div class="dialog-header">
<span>Shader Framerate</span>
</div>
<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 class="dialog-footer">
<span v-if="settingsStore.shaderFramerate == 65">vsync</span>
@@ -73,7 +103,7 @@ const close = () => {
<span>Pixelated</span>
</div>
<div class="dialog-body">
<input type="checkbox" v-model="settingsStore.shaderPixelated">
<input type="checkbox" v-model="settingsStore.shaderPixelated" />
</div>
</div>
</div>
@@ -92,11 +122,10 @@ const close = () => {
align-items: center;
z-index: 1000;
display: grid;
}
.dialog-box {
background-image: url('../images/bg-grass.png');
background-image: url("../img/bg-grass.png");
padding: 0 20px;
border-radius: 5px;
width: 800px;
@@ -111,7 +140,7 @@ const close = () => {
.dialog-box h3 {
font-size: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
width: 100%;
padding: 10px 0;
@@ -139,4 +168,4 @@ const close = () => {
.dialog-footer {
grid-column-start: 3;
}
</style>
</style>
+219 -190
View File
@@ -1,31 +1,44 @@
<script setup lang="ts">
import { endTurn } from "~/netcode";
import type { Game, Player } from '~/netcode/interfaces';
import { useClientSideStore } from "~/stores/turn";
import { endTurn, lockCard } from "~/netcode";
import type { Game, Player } from "~/netcode/interfaces";
import { useTurnStore } from "~/stores/turnStore";
export interface Props {
player: Player;
game: Game,
hide_stack_and_discard?: boolean;
player: Player;
game: Game;
hide_stack_and_discard?: boolean;
}
const authStore = useAuthStore()
const authStore = useAuthStore();
const props = withDefaults(defineProps<Props>(), {
hide_stack_and_discard: () => false,
})
const playerList = computed(() => 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)
hide_stack_and_discard: () => false,
});
const playerList = computed(() =>
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 isSelfTurn = computed(() => isSelf && isPlayerTurn)
const isSelf = computed(
() => props.player.user.userId === authStore.selfUser.userId
);
const isSelfTurn = computed(() => isSelf && isPlayerTurn);
const warningText = ref("Effets restants!")
const clientStore = useClientSideStore()
const warningText = ref("Effets restants!");
const turnStore = useTurnStore();
// const make_discard = (target: Player) =>
// 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 prepare_champion = (target: CardParser) => clientStore.findUseEffect(CardEffects.PREPARE, target)
let warning = ref(false)
let warning = ref(false);
// function has_actions_remaining(): boolean {
// return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false)
@@ -52,27 +64,34 @@ let warning = ref(false)
// }
function checkEndTurn() {
// TODO: Add a confirmation dialog if resources are left
// if (
// (!warning.value)
// && has_actions_remaining()) {
// // Set a warning flag to true
// // Display an alert somehow using this warning flag
// warning.value = true
// }
// else if (!warning.value && can_buy_fire_gem()) {
// warningText.value = "P'tite Fire gem?"
// warning.value = true
// }
// else {
// warningText.value = "Effets restants!"
// warning.value = false
// endTurn()
// }
// TODO: Add a confirmation dialog if resources are left
// if (
// (!warning.value)
// && has_actions_remaining()) {
// // Set a warning flag to true
// // Display an alert somehow using this warning flag
// warning.value = true
// }
// else if (!warning.value && can_buy_fire_gem()) {
// warningText.value = "P'tite Fire gem?"
// warning.value = true
// }
// else {
// warningText.value = "Effets restants!"
// warning.value = false
// 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) {
// for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) {
// damage_team(team)
@@ -82,25 +101,34 @@ function checkEndTurn() {
// function check_for_guard(player: Player) {
// return player.board.some(c => c.guard === true)
// }
</script>
<template>
<div class="player" :class="{ turn: isPlayerTurn, self: isSelf }">
<div id="player_banner" class="big">test</div>
<div id="turn_buttons">
<button id="end_turn" class="end_turn_button turn_icon" :class="{ hidden: !isSelfTurn }"
@click="checkEndTurn">END TURN</button>
<div class="player" :class="{ turn: isPlayerTurn, self: isSelf }">
<div id="player_banner" class="big">test</div>
<div id="turn_buttons">
<button
id="end_turn"
class="end_turn_button turn_icon"
:class="{ hidden: !isSelfTurn }"
@click="checkEndTurn"
>
END TURN
</button>
<div class="warning"
:class="{ 'warning-fire-gem': warningText == 'P\'tite Fire gem?', 'visible': warning }">
/!\<br>{{ warningText }}
</div>
</div>
<div
class="warning"
:class="{
'warning-fire-gem': warningText == 'P\'tite Fire gem?',
visible: warning,
}"
>
/!\<br />{{ warningText }}
</div>
</div>
<div id="current_player_board">
<!-- <div class="other_player_buttons_container">
<div id="current_player_board">
<!-- <div class="other_player_buttons_container">
<button @click.stop="damage_team(props.player.team)"
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)">
@@ -118,14 +146,19 @@ function checkEndTurn() {
make Discard
</button>
</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"> -->
<!-- HAND -->
<!-- <template v-if="isSelf">
<!-- <div class="cards_container"> -->
<!-- HAND -->
<!-- <template v-if="isSelf">
<CardPreview :card_id="c?.card_id" v-for="c in props.player.hand" @click="c && playCard(c)">
<button
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>
</template> -->
<!-- BOARD -->
<!-- <CardPreview :card_id="c.cardId" v-for="c in props.player.board.cards"
<!-- BOARD -->
<!-- <CardPreview :card_id="c.cardId" v-for="c in props.player.board.cards"
@click="console.log/*lockCard(c)*/"
: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
<template v-if="c.uuid in props.player.turn.cards_locked">
<template v-for="e in clientStore.maskedServerEffects">
@@ -193,237 +226,233 @@ function checkEndTurn() {
STUN
</button>
</template> -->
<!-- </CardPreview> -->
<!-- </div> -->
</div>
<!-- </CardPreview> -->
<!-- </div> -->
</div>
</div>
</template>
<style scoped>
.warning {
background-color: rgba(255, 0, 0, 0.7);
color: white;
padding: 5px;
border-radius: 5px;
width: 100px;
font-size: 0.85em;
margin-left: 5px;
visibility: hidden
background-color: rgba(255, 0, 0, 0.7);
color: white;
padding: 5px;
border-radius: 5px;
width: 100px;
font-size: 0.85em;
margin-left: 5px;
visibility: hidden;
}
.warning.visible {
visibility: visible
visibility: visible;
}
.warning-fire-gem {
background-color: rgba(255, 165, 0, 0.7);
background-color: rgba(255, 165, 0, 0.7);
}
#turn_buttons {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
#end_turn {
z-index: 10;
height: 100%;
z-index: 10;
height: 100%;
}
#end_turn.hidden {
visibility: hidden
visibility: hidden;
}
.stack,
.discard,
.cards_container {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap-reverse;
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap-reverse;
}
.stack {
grid-area: stack;
margin-left: 75px;
grid-area: stack;
margin-left: 75px;
}
.discard {
margin-top: 40px;
grid-area: discard;
justify-content: start;
cursor: pointer;
pointer-events: none;
margin-left: 75px;
margin-top: 40px;
grid-area: discard;
justify-content: start;
cursor: pointer;
pointer-events: none;
margin-left: 75px;
}
.discard.invisible {
visibility: hidden;
visibility: hidden;
}
.player {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
/* border-top: 5px solid black; */
grid-template-areas:
"player_stats"
"player";
transition: all 1s;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
/* border-top: 5px solid black; */
grid-template-areas:
"player_stats"
"player";
transition: all 1s;
}
#current_player_board {
grid-area: player;
grid-area: player;
}
.damage_button {
background-color: #ff4d4d;
color: white;
border: none;
border-radius: 8px;
padding: 5px 25px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
background-color: #ff4d4d;
color: white;
border: none;
border-radius: 8px;
padding: 5px 25px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.damage_button:disabled {
background-color: rgba(255, 77, 77, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
background-color: rgba(255, 77, 77, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
}
.damage_button:hover {
background-color: #e60000;
background-color: #e60000;
}
.damage_button:disabled:hover {
background-color: rgba(230, 0, 0, 0.5);
background-color: rgba(230, 0, 0, 0.5);
}
.damage_button:active {
transform: translateY(2px);
box-shadow: none;
transform: translateY(2px);
box-shadow: none;
}
.all_damage_button {
padding: 5px 5px;
font-size: 14px;
padding: 5px 5px;
font-size: 14px;
}
.make_discard_button {
background-color: #55b42f;
color: white;
border: none;
border-radius: 8px;
padding: 5px 15px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
background-color: #55b42f;
color: white;
border: none;
border-radius: 8px;
padding: 5px 15px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.make_discard_button:disabled {
background-color: rgba(85, 180, 47, 0.5);
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
cursor: not-allowed;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
background-color: rgba(85, 180, 47, 0.5);
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
cursor: not-allowed;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.make_discard_button:hover {
background-color: #03883a;
background-color: #03883a;
}
.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 {
transform: translateY(2px);
box-shadow: none;
transform: translateY(2px);
box-shadow: none;
}
.turn_icon {
position: relative;
position: relative;
}
#player_info {
display: flex;
align-items: center;
min-width: 200px;
display: flex;
align-items: center;
min-width: 200px;
}
#player_banner.big {
padding: 0;
left: 50%;
transform: translateX(-50%);
max-width: min-content;
border: 5px solid black;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60));
backdrop-filter: blur(5px) brightness(80%) grayscale(80%);
padding: 0;
left: 50%;
transform: translateX(-50%);
max-width: min-content;
border: 5px solid black;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
backdrop-filter: blur(5px) brightness(80%) grayscale(80%);
}
#player_banner img {
height: 50px;
border-radius: 25px;
height: 50px;
border-radius: 25px;
}
.turn_icon img {
height: 50px;
height: 50px;
}
.turn_icon_number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.end_turn_button {
background-color: #4CAF50;
/* Green background color */
color: white;
border: none;
border-radius: 8px;
padding: 13px 10px;
margin: 0 0 0 5px;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
background-color: #4caf50;
/* Green background color */
color: white;
border: none;
border-radius: 8px;
padding: 13px 10px;
margin: 0 0 0 5px;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.end_turn_button:hover {
background-color: #45a049;
/* Darker green on hover */
background-color: #45a049;
/* Darker green on hover */
}
.end_turn_button:active {
transform: translateY(2px);
/* Add a slight press effect on click */
box-shadow: none;
/* Remove shadow on click */
transform: translateY(2px);
/* Add a slight press effect on click */
box-shadow: none;
/* Remove shadow on click */
}
.suicide_effect {
background: linear-gradient(to bottom right, #666666, #ccc);
color: red;
background: linear-gradient(to bottom right, #666666, #ccc);
color: red;
}
</style>
+27 -33
View File
@@ -1,65 +1,59 @@
<script setup lang="ts">
import type { Player } from '~/netcode/Player';
import type { Player } from "~/netcode/interfaces";
export interface Props {
player: Player
player: Player;
}
const props = defineProps<Props>()
const props = defineProps<Props>();
</script>
<template>
<div class="turn_icon">
<img src="/images/champion-shield.png" class="icon_img" draggable="false" />
<span class="turn_icon_number">
{{ player.team?.health }}
</span>
</div>
<!-- <div class="turn_icon" v-if="showTurnData">
<img src="/images/gold.png" class="icon_img" draggable="false" />
<div class="turn_icon">
<img src="/img/champion-shield.png" class="icon_img" draggable="false" />
<span class="turn_icon_number">
{{ player.team?.health }}
</span>
</div>
<!-- <div class="turn_icon" v-if="showTurnData">
<img src="/umg/gold.png" class="icon_img" draggable="false" />
<span class="turn_icon_number">
{{ player.turn?.gold_reserve }}
</span>
</div>
<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">
{{ player.turn?.damage_reserve }}
</span>
</div> -->
<div class="turn_icon">
<img src="/images/fuck.svg" class="icon_img" draggable="false" />
<span class="turn_icon_number">{{ (player.turn?.discard_markers ?? 0) + (player.turn?.fuck_u_markers ?? 0)
}}</span>
</div>
<div class="turn_icon">
<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>
</div>
</template>
<style scoped>
.turn_icon {
position: relative;
display: flex;
justify-content: center;
align-items: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
img.icon_img {
height: 50px;
width: 50px;
height: 50px;
width: 50px;
}
span.icon_img {
font-size: 50px;
font-size: 50px;
}
.turn_icon_number {
position: absolute;
text-shadow: white 0 0 1px;
position: absolute;
text-shadow: white 0 0 1px;
}
</style>
+24 -26
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { Player } from '~/netcode/Player';
import type { Player } from "~/netcode/Player";
export interface Props {
isVisible: Boolean;
@@ -10,15 +9,14 @@ export interface Props {
const props = withDefaults(defineProps<Props>(), {
won: () => false,
players: () => []
})
players: () => [],
});
const emit = defineEmits(['close'])
const emit = defineEmits(["close"]);
const close = () => {
reloadNuxtApp()
}
reloadNuxtApp();
};
</script>
<template>
@@ -30,24 +28,24 @@ const close = () => {
</div>
<div class="dialog-body">
<PlayerIcon :player="players[0]"></PlayerIcon>
<p v-if="won">
Vous avez gagné!
</p>
<p v-if="won">Vous avez gagné!</p>
<p v-else>
Vous avez perdu face à ce magnifique individu qui a très clairement beaucoup plus de bitches que vous! Cela
dit, je pense qu'il n'y a pas de bonnes
ou de mauvaises situations, juste des situations différentes. Vous avez perdu, mais vous avez gagné en
expérience! C'est ça le plus important
dans la vie, n'est-ce pas? Apprendre de ses erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez
heureux d'avoir eu l'opportunité
d'apprendre quelque chose aujourd'hui! Et puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend
plus fort"! Alors, allez-y, soyez fort!
Et n'oubliez pas, la prochaine fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un 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'" />
Vous avez perdu face à ce magnifique individu qui a très clairement
beaucoup plus de bitches que vous! Cela dit, je pense qu'il n'y a pas
de bonnes ou de mauvaises situations, juste des situations
différentes. Vous avez perdu, mais vous avez gagné en expérience!
C'est ça le plus important dans la vie, n'est-ce pas? Apprendre de ses
erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez heureux
d'avoir eu l'opportunité d'apprendre quelque chose aujourd'hui! Et
puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend plus
fort"! Alors, allez-y, soyez fort! Et n'oubliez pas, la prochaine
fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un
champion! Vous êtes le meilleur! Vous êtes le roi du monde!
<img
src="/img/so_back.png"
alt="Une image décrivant le meme 'We are so back'"
/>
</p>
</div>
</div>
</div>
@@ -68,7 +66,7 @@ const close = () => {
}
.dialog-box {
background-image: url('/images/bg-grass.png');
background-image: url("/img/bg-grass.png");
padding: 0 20px;
border-radius: 5px;
width: 800px;
@@ -84,7 +82,7 @@ const close = () => {
.dialog-box h3 {
font-size: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
width: 100%;
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;
}
export interface PlayingTurn {
currentTeam: string;
cardsLocked: string[];
effectsUsed: string[];
damage: number;
gold: number;
}
export interface Game extends GameObject {
fireGems: Container;
market: Container;
marketStack: Container;
teams: Team[];
currentTurn: string;
currentTurn: PlayingTurn;
}
export interface Team extends GameObject {
+250 -192
View File
@@ -1,113 +1,156 @@
<script setup lang="ts">
import LoadingScreen from '~/components/LoadingScreen.vue';
import { subscribe } from '~/netcode';
import { containerEvents, delay } from '~/netcode/events';
import { type Container, type Game, type Player } from '~/netcode/interfaces';
import LoadingScreen from "~/components/LoadingScreen.vue";
import { subscribe } from "~/netcode";
import { containerEvents, delay } from "~/netcode/events";
import { type Container, type Game, type Player } from "~/netcode/interfaces";
const config = useRuntimeConfig()
const settingsStore = useSettingsStore()
const config = useRuntimeConfig();
const settingsStore = useSettingsStore();
const route = useRoute()
const gameId = route.params.id
const route = useRoute();
const gameId = route.params.id;
const { data: game, pending, error, refresh } = await useFetch<Game>(
new URL(`/games/${gameId}`, config.public.endpoint).href,
{
credentials: "include",
},
)
const {
data: game,
pending,
error,
refresh,
} = await useFetch<Game>(
new URL(`/games/${gameId}`, config.public.endpoint).href,
{
credentials: "include",
}
);
if (game.value == null) {
navigateTo("/lobby");
throw new Error("game not found")
navigateTo("/lobby");
throw new Error("game not found");
}
const containers = computed<Record<string, Container>>(() => ({
[game.value!.fireGems._id]: game.value!.fireGems,
[game.value!.market._id]: game.value!.market,
[game.value!.marketStack._id]: game.value!.marketStack,
...Object.fromEntries(
game.value!.teams.reduce(
(acc, t) => acc.concat(t.players), [] as Player[]
).map(
p => [
[p.board._id, p.board],
[p.discard._id, p.discard],
[p.hand._id, p.hand],
[p.stack._id, p.stack]
]
).flat(1)
)
}))
[game.value!.fireGems._id]: game.value!.fireGems,
[game.value!.market._id]: game.value!.market,
[game.value!.marketStack._id]: game.value!.marketStack,
...Object.fromEntries(
game
.value!.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
.map((p) => [
[p.board._id, p.board],
[p.discard._id, p.discard],
[p.hand._id, p.hand],
[p.stack._id, p.stack],
])
.flat(1)
),
}));
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>)) {
actions.value.push(callback)
if (actions.value.length == 1) {
execAction(callback)
}
function queueAction(callback: () => Promise<any>) {
actions.value.push(callback);
if (actions.value.length == 1) {
execAction(callback);
}
}
async function execAction(callback: (() => Promise<any>)) {
await callback()
actions.value.shift()
if (actions.value.length > 0) {
execAction(actions.value[0])
}
async function execAction(callback: () => Promise<any>) {
await callback();
actions.value.shift();
if (actions.value.length > 0) {
execAction(actions.value[0]);
}
}
eventSource.addEventListener("message", async (message) => {
const data = JSON.parse(message.data)
console.log(data)
if (data.type == 'start') {
throw new Error("not implemented")
} else if (data.type == "populateContainer") {
throw new Error("not implemented")
} 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)
const current_game = game.value;
if (!current_game) {
throw new Error("game not found");
}
const data = JSON.parse(message.data);
console.log(data);
if (data.type == "start") {
throw new Error("not implemented");
} else if (data.type == "populateContainer") {
queueAction(async () => {
await containerEvents.get(data.container)?.startAnimation();
});
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 () => {
containerEvents.get(data.from)?.startAnimation()
containerEvents.get(data.to)?.startAnimation()
})
queueAction(async () => {
containerEvents.get(data.from)?.startAnimation();
containerEvents.get(data.to)?.startAnimation();
});
const fromContainer = containers.value[data.from as string]
const toContainer = containers.value[data.to as string]
for (const card of data.cards) {
queueAction(async () => {
const index = fromContainer.cards.findIndex(c => c._id == card._id)
fromContainer.cards.splice(index, 1)
await delay(settingsStore.animationSpeed)
})
const fromContainer = containers.value[data.from as string];
const toContainer = containers.value[data.to as string];
for (const card of data.cards) {
queueAction(async () => {
const index = fromContainer.cards.findIndex((c) => c._id == card._id);
fromContainer.cards.splice(index, 1);
await delay(settingsStore.animationSpeed);
});
queueAction(async () => {
toContainer.cards.push(card)
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 () => {
toContainer.cards.push(card);
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") {
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(() => 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 playerList = computed(() =>
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
);
// TODO : current turn sound
@@ -115,24 +158,24 @@ const selfPlayer = computed(() => authStore.logged ? playerList.value?.find(p =>
// TODO : join sound
const showResults = ref(false)
const isWinner = ref(false)
const loaded = ref(false)
const showResults = ref(false);
const isWinner = ref(false);
const loaded = ref(false);
function showLoserScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true
isWinner.value = false
// playSound('/sounds/ding.mp3')
showResults.value = true;
isWinner.value = false;
}
function showWinnerScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true
isWinner.value = true
// playSound('/sounds/ding.mp3')
showResults.value = true;
isWinner.value = true;
}
const discard_unwrapped = ref(false)
const focusedPlayer = ref<Player>(selfPlayer.value ?? playerList.value[0])
const discard_unwrapped = ref(false);
const focusedPlayer = ref<Player>(selfPlayer.value ?? playerList.value[0]);
// const clientStore = useClientSideStore()
// 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);
function focusPlayer(p: Player) {
focusedPlayer.value = p
focusedPlayer.value = p;
}
// watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
@@ -153,159 +196,174 @@ function focusPlayer(p: Player) {
// })
onUnmounted(() => {
eventSource.close()
})
eventSource.close();
});
</script>
<template>
<div @contextmenu.prevent="false" id="game_board" v-if="game">
<AnimatedBackground @ready="loaded = true"></AnimatedBackground>
<div id="stack_discard">
<div id="stack">
<CardsGrouped :container="focusedPlayer.stack" :wrapped="true" :hidden="true">
</CardsGrouped>
</div>
<div @contextmenu.prevent="false" id="game_board" v-if="game">
<AnimatedBackground @ready="loaded = true"></AnimatedBackground>
<div id="stack_discard">
<div id="stack">
<CardsGrouped
:container="focusedPlayer.stack"
:wrapped="true"
:hidden="true"
>
</CardsGrouped>
</div>
<div id="discard" :class="{ invisible: discard_unwrapped }" @click="() => discard_unwrapped = true">
<CardsGrouped :container="focusedPlayer.discard" :wrapped="true">
</CardsGrouped>
</div>
</div>
<div
id="discard"
:class="{ invisible: discard_unwrapped }"
@click="() => (discard_unwrapped = true)"
>
<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">
<PlayerListElement v-for="p in playerList" :player="p" @click="focusPlayer(p)" :focused="focusedPlayer">
</PlayerListElement>
</div>
</div> -->
<div id="current_player">
<PlayerSlot v-if="focusedPlayer" :player="focusedPlayer" :game="game!"></PlayerSlot>
</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 id="current_player">
<PlayerSlot
v-if="focusedPlayer"
:player="focusedPlayer"
:game="game!"
></PlayerSlot>
</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>
<style scoped>
#game_status {
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, .75);
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.75);
}
#game_board {
display: grid;
grid-template-columns: 1fr min-content 1fr;
grid-template-rows: min-content min-content 1fr;
grid-template-areas:
"stack_discard market empty"
"player_list player_list player_list"
"current_player current_player current_player";
height: 100%;
display: grid;
grid-template-columns: 1fr min-content 1fr;
grid-template-rows: min-content min-content 1fr;
grid-template-areas:
"stack_discard market empty"
"player_list player_list player_list"
"current_player current_player current_player";
height: 100%;
}
#player_list,
#current_player {
overflow: auto;
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
justify-content: center;
/* background: url('/images/bg-darkgrass.png'); */
overflow: auto;
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
justify-content: center;
/* background: url('/img/bg-darkgrass.png'); */
}
#current_player {
grid-area: current_player;
grid-area: current_player;
}
#stack_discard {
grid-area: stack_discard;
display: flex;
flex-direction: row;
justify-content: flex-end;
grid-area: stack_discard;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
#stack,
#discard {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
}
#discard {
cursor: pointer;
cursor: pointer;
}
.discard_overlay {
z-index: 9;
display: flex;
/* background: blue; */
pointer-events: none;
gap: 2px;
flex-wrap: wrap-reverse;
align-content: flex-end;
cursor: default;
position: absolute;
padding-bottom: inherit;
padding-left: 40px;
height: calc(100% - 64px);
padding-top: 64px;
width: calc(100% -40px);
margin-left: 0;
z-index: 9;
display: flex;
/* background: blue; */
pointer-events: none;
gap: 2px;
flex-wrap: wrap-reverse;
align-content: flex-end;
cursor: default;
position: absolute;
padding-bottom: inherit;
padding-left: 40px;
height: calc(100% - 64px);
padding-top: 64px;
width: calc(100% -40px);
margin-left: 0;
}
#discard_unwrapped_bg {
display: none;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 8;
background: rgba(0, 0, 0, 0.5);
pointer-events: all;
padding: inherit;
display: none;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 8;
background: rgba(0, 0, 0, 0.5);
pointer-events: all;
padding: inherit;
}
#discard_unwrapped_bg.unwrapped {
display: block;
display: block;
}
#market {
grid-area: market;
display: flex;
flex-direction: row;
justify-content: center;
grid-area: market;
display: flex;
flex-direction: row;
justify-content: center;
}
#player_list_container {
grid-area: player_list;
display: flex;
flex-direction: row;
justify-content: center;
grid-area: player_list;
display: flex;
flex-direction: row;
justify-content: center;
/* background-image: url("/images/bg-darkgrass.png"); */
/* background-image: url("/img/bg-darkgrass.png"); */
}
#player_list {
/* border: 5px solid #000; */
display: flex;
/* border: 5px solid #000; */
display: flex;
}
</style>
<style>
#player_list .card {
--max-height: 90px;
--max-height: 90px;
}
</style>
</style>
+217 -202
View File
@@ -1,273 +1,288 @@
<script setup lang="ts">
// 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";
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) => {
const data = JSON.parse(message.data)
if (data.type == "create") {
games.value.push(data)
} else if (data.type == "delete") {
const index = games.value.findIndex(g => g._id == data._id)
if (index == -1) {
throw new Error("Couldn't delete game")
}
games.value.splice(index, 1)
} else if (data.type == "update") {
throw new Error("not implemented")
} else if (data.type == "start") {
throw new Error("not implemented")
const data = JSON.parse(message.data);
if (data.type == "create") {
games.value.push(data);
} else if (data.type == "delete") {
const index = games.value.findIndex((g) => g._id == data._id);
if (index == -1) {
throw new Error("Couldn't delete game");
}
games.value.splice(index, 1);
} else if (data.type == "update") {
throw new Error("not implemented");
} else if (data.type == "start") {
throw new Error("not implemented");
}
});
const erika_alive = ref(true);
const router = useRouter();
function killErika() {
playSound('/sounds/splat1.mp3')
erika_alive.value = false;
playSound("/sounds/splat1.mp3");
erika_alive.value = false;
}
onUnmounted(() => {
eventSource.close()
})
eventSource.close();
});
</script>
<template>
<div class="background">
<div class="title">
Entrez dans le Royaume de Héron !
<div class="background">
<div class="title">Entrez dans le Royaume de Héron !</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 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>
<style scoped>
.background {
background-color: #f0f0f0;
background-image: url('https://www.pockettactics.com/wp-content/sites/pockettactics/2023/01/fire-emblem-engage-review-29.jpg');
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
background-color: #f0f0f0;
background-image: url("https://www.pockettactics.com/wp-content/sites/pockettactics/2023/01/fire-emblem-engage-review-29.jpg");
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
.title {
font-family: 'Kanit', 'Roboto', sans-serif;
font-size: 64px;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
margin-bottom: 100px;
font-family: "Kanit", "Roboto", sans-serif;
font-size: 64px;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
margin-bottom: 100px;
}
.middle-row {
display: flex;
flex-direction: column;
gap: 100px;
margin-bottom: 100px;
display: flex;
flex-direction: column;
gap: 100px;
margin-bottom: 100px;
}
.create-game-zone {
display: flex;
flex-direction: column;
gap: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.create-game-zone button {
background-color: #4CAF50;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-family: 'Roboto', sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 10px 24px;
height: 50px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
background-color: #4caf50;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-family: "Roboto", sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 10px 24px;
height: 50px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.games-list-zone {
display: flex;
flex-direction: column;
gap: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.games-list-zone button {
background-color: rgba(0, 0, 0, 0.8);
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-family: 'Roboto', sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 20px 24px;
height: 120px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
background-color: rgba(0, 0, 0, 0.8);
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-family: "Roboto", sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 20px 24px;
height: 120px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.games-list-zone button.own_game {
border: solid yellow 5px;
border: solid yellow 5px;
}
.overlay {
height: 100%;
width: 100%;
position: absolute;
pointer-events: none;
height: 100%;
width: 100%;
position: absolute;
pointer-events: none;
}
.erika-zone {
position: absolute;
top: 60%;
display: flex;
justify-content: right;
z-index: 2;
height: 40%;
width: 100%;
object-fit: cover;
position: absolute;
top: 60%;
display: flex;
justify-content: right;
z-index: 2;
height: 40%;
width: 100%;
object-fit: cover;
}
.overlay-image {
position: absolute;
height: 100%;
cursor: pointer;
pointer-events: all;
position: absolute;
height: 100%;
cursor: pointer;
pointer-events: all;
}
.overlay-button {
position: absolute;
z-index: 1;
width: 300px;
box-sizing: border-box;
pointer-events: none;
position: absolute;
z-index: 1;
width: 300px;
box-sizing: border-box;
pointer-events: none;
}
.kill-erika-button {
display: none;
pointer-events: all;
background-color: #f44336;
border: none;
color: white;
text-align: center;
text-decoration: none;
font-family: 'Roboto', sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 10px 24px;
height: 50px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
display: none;
pointer-events: all;
background-color: #f44336;
border: none;
color: white;
text-align: center;
text-decoration: none;
font-family: "Roboto", sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 10px 24px;
height: 50px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.speech-bubble {
position: absolute;
top: 60%;
right: 260px;
z-index: 1;
background-color: rgba(225, 225, 225, 0.8);
padding: 10px;
border-radius: 10px;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
font-family: 'Kanit', 'Roboto', sans-serif;
font-size: 16px;
color: black;
text-align: center;
max-width: 420px;
line-height: 1.5;
position: absolute;
top: 60%;
right: 260px;
z-index: 1;
background-color: rgba(225, 225, 225, 0.8);
padding: 10px;
border-radius: 10px;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
font-family: "Kanit", "Roboto", sans-serif;
font-size: 16px;
color: black;
text-align: center;
max-width: 420px;
line-height: 1.5;
}
.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">
const router = useRouter();
</script>
<template>
<div class="background">
<div class="overlay-2">
<div class="erika-zone-2">
<img @click="router.push({ path: '/lobby' })" class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/images/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse" />
</div>
</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="/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 class="background">
<div class="overlay-2">
<div class="erika-zone-2">
<img
@click="router.push({ path: '/lobby' })"
class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/img/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse"
/>
</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>
<style scoped>
.background {
background-color: #f0f0f0;
background-image: url('/images/bg-darkgrass.png');
background-size: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100vw;
;
background-color: #f0f0f0;
background-image: url("/img/bg-darkgrass.png");
background-size: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100vw;
}
.text-content {
background-color: rgba(255, 255, 255, .75);
padding: 1em;
border-radius: 1em;
max-width: 800px;
margin-top: 2em;
text-align: left;
background-color: rgba(255, 255, 255, 0.75);
padding: 1em;
border-radius: 1em;
max-width: 800px;
margin-top: 2em;
text-align: left;
h1 {
text-align: center;
font-size: 2em;
margin-bottom: 1.5em;
}
h1 {
text-align: center;
font-size: 2em;
margin-bottom: 1.5em;
}
p {
margin: 1em 0;
font-size: 1.2em;
}
p {
margin: 1em 0;
font-size: 1.2em;
}
}
.card-row {
display: flex;
justify-content: center;
gap: 1em;
display: flex;
justify-content: center;
gap: 1em;
}
.overlay-2 {
height: 00px;
width: 100%;
position: sticky;
z-index: 2;
top: 0;
pointer-events: none;
height: 00px;
width: 100%;
position: sticky;
z-index: 2;
top: 0;
pointer-events: none;
}
.erika-zone-2 {
position: absolute;
top: 300px;
display: flex;
justify-content: right;
z-index: 2;
height: 300px;
width: 100%;
object-fit: cover;
position: absolute;
top: 300px;
display: flex;
justify-content: right;
z-index: 2;
height: 300px;
width: 100%;
object-fit: cover;
}
.overlay-image {
position: sticky;
height: 100%;
cursor: pointer;
pointer-events: all;
position: sticky;
height: 100%;
cursor: pointer;
pointer-events: all;
}
</style>
</style>
+2 -1
View File
@@ -1,4 +1,5 @@
import { defineStore } from "pinia";
import type { Game } from "~/netcode/interfaces";
// import goStore, { useEffect } from "~/netcode";
// import type { Card } from "~/netcode/Card";
// import { CardEffects, type Effect } from "~/netcode/Effect";
@@ -10,7 +11,7 @@ import { defineStore } from "pinia";
// useClientSideStore().activatedClientside = []
// })
export const useClientSideStore = defineStore("clientside", {
export const useTurnStore = defineStore("turn", {
// state: () => ({
// activatedClientside: [] as Effect[],
// effectsHandled: new Set([