Compare commits
176
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77724d5539 | ||
|
|
21c41bb9fd | ||
|
|
bb625b1fee | ||
|
|
fd3cf0ba23 | ||
|
|
4ec29cf811 | ||
|
|
b92eb4b901 | ||
|
|
e36c916e0b | ||
|
|
ca8e38624e | ||
|
|
b00fffd314 | ||
|
|
11dd342b8e | ||
|
|
9e1af6f3df | ||
|
|
5e992122d8 | ||
|
|
aa58e6c4f8 | ||
|
|
1a05229d0b | ||
|
|
62d7346617 | ||
|
|
9708abbf81 | ||
|
|
8f300394ab | ||
|
|
2c986fbd97 | ||
|
|
3a88d1c107 | ||
|
|
7690292567 | ||
|
|
5623f7c91f | ||
|
|
08fb23e729 | ||
|
|
2876f7b91e | ||
|
|
775712371d | ||
|
|
6edadc3ef3 | ||
|
|
7713faa4d0 | ||
|
|
f9197d470e | ||
|
|
de38416bcd | ||
|
|
23e5f9f432 | ||
|
|
240ab7e1d4 | ||
|
|
80f6d02fc7 | ||
|
|
c68446bf25 | ||
|
|
63519429e6 | ||
|
|
5282ca3c4a | ||
|
|
f5e4b10d1b | ||
|
|
efebc79d35 | ||
|
|
ad324a9a97 | ||
|
|
4145cacf1a | ||
|
|
da54495d1c | ||
|
|
f60ec850f0 | ||
|
|
05130ad97e | ||
|
|
e905731bc2 | ||
|
|
9638bcf1dd | ||
|
|
bce8edc150 | ||
|
|
fadc8cc451 | ||
|
|
8cb8bdfc5e | ||
|
|
0f1ab017ed | ||
|
|
9384f8f23e | ||
|
|
5ce9770e8c | ||
|
|
174eacc417 | ||
|
|
f68304cf22 | ||
|
|
8dbb64a540 | ||
|
|
c2674be596 | ||
|
|
94613b2d20 | ||
|
|
c0ce9b4b33 | ||
|
|
4768a09405 | ||
|
|
2e8046f81c | ||
|
|
c1bbf7dce4 | ||
|
|
302890a4ac | ||
|
|
0669619543 | ||
|
|
cfbaeca52b | ||
|
|
ba48570833 | ||
|
|
ff182b66ae | ||
|
|
ad6af3724d | ||
|
|
44b840e2bc | ||
|
|
7a338fe5fc | ||
|
|
c48363e9c3 | ||
|
|
90dfe57a49 | ||
|
|
0f771b12d5 | ||
|
|
baf2fc27a9 | ||
|
|
3330704670 | ||
|
|
8d4ae03519 | ||
|
|
7fd07b5a6a | ||
|
|
3e3284d8c4 | ||
|
|
df9e7a2e13 | ||
|
|
8dc263ab50 | ||
|
|
fde462166c | ||
|
|
2e201fa14e | ||
|
|
3c5b30be9e | ||
|
|
b1c9644941 | ||
|
|
2709bb799c | ||
|
|
6bef7778e8 | ||
|
|
6cdd846d3f | ||
|
|
9e985ad471 | ||
|
|
50ea1d456f | ||
|
|
3eba056253 | ||
|
|
9ce64ab591 | ||
|
|
bceae83341 | ||
|
|
db7959d7f1 | ||
|
|
12d6de5074 | ||
|
|
6112a7d6ba | ||
|
|
0facc4694f | ||
|
|
b3df241313 | ||
|
|
694e8429d1 | ||
|
|
22d1d804f9 | ||
|
|
b1a874aaeb | ||
|
|
25cf8a55e9 | ||
|
|
866a537924 | ||
|
|
d5dd147fba | ||
|
|
221f96860f | ||
|
|
6db8410ccc | ||
|
|
28bc888cb1 | ||
|
|
1fbe9a57ee | ||
|
|
6cc91d58ce | ||
|
|
0a8313b69d | ||
|
|
90a636b58a | ||
|
|
a22c6e1352 | ||
|
|
4a56f3ed62 | ||
|
|
8a3e2babc9 | ||
|
|
7c98da1df8 | ||
|
|
c0e02defa8 | ||
|
|
60ab679756 | ||
|
|
83fe97a427 | ||
|
|
4d66dea14c | ||
|
|
6ae1ce701d | ||
|
|
7d7f22a82c | ||
|
|
aa8332cb20 | ||
|
|
aa610a58a5 | ||
|
|
851c9a5190 | ||
|
|
f8205f374a | ||
|
|
3df2aee04c | ||
|
|
443bfc128a | ||
|
|
a3630abf92 | ||
|
|
74d99aff6e | ||
|
|
76814efb18 | ||
|
|
571a35633b | ||
|
|
5fa34eda14 | ||
|
|
37ad826a80 | ||
|
|
3f8a580984 | ||
|
|
b5f4d91ff8 | ||
|
|
351cc06495 | ||
|
|
4ea5c25a47 | ||
|
|
766dc92931 | ||
|
|
f570b456ca | ||
|
|
8c7cc19991 | ||
|
|
df6d77b870 | ||
|
|
b2a3d4f701 | ||
|
|
6291989dd4 | ||
|
|
a6fcc28ced | ||
|
|
66b6cc61e9 | ||
|
|
90ed8a6211 | ||
|
|
ceab43e861 | ||
|
|
ffd9f0187d | ||
|
|
0305dbe9b7 | ||
|
|
db4aac845c | ||
|
|
bd6978d392 | ||
|
|
310b3e7b17 | ||
|
|
ca87817b33 | ||
|
|
6f7b1bdf03 | ||
|
|
dd09ab3c8c | ||
|
|
17a1d23768 | ||
|
|
0915c0a8d8 | ||
|
|
7ae052ee8b | ||
|
|
d9d8a7a8f0 | ||
|
|
32c4c9a4bf | ||
|
|
06ac020723 | ||
|
|
125bb0e894 | ||
|
|
221935faae | ||
|
|
ab975d499e | ||
|
|
dcac9ecd56 | ||
|
|
86ae4c57f8 | ||
|
|
b3ca9eafd8 | ||
|
|
2b678a51a7 | ||
|
|
e18a8dce49 | ||
|
|
2c2342580b | ||
|
|
cfc04b8451 | ||
|
|
02e6548eea | ||
|
|
2aac51a018 | ||
|
|
658add7888 | ||
|
|
5cd57a7266 | ||
|
|
58d0e4a416 | ||
|
|
1fc3378083 | ||
|
|
f16dad82ae | ||
|
|
51171fc2e1 | ||
|
|
d5c65cfb14 | ||
|
|
caa54e6ece |
@@ -0,0 +1,3 @@
|
||||
NUXT_PUBLIC_ENDPOINT=http://localhost:8765
|
||||
NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=https://discord.com/oauth2/authorize?client_id=
|
||||
NUXT_DEV_MODE=TRUE
|
||||
+23
-19
@@ -1,19 +1,16 @@
|
||||
name: release-tag
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
|
||||
jobs:
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DOCKER_ORG: legonzaur
|
||||
DOCKER_LATEST: nightly
|
||||
REPO_NAME: ${{ gitea.repository }}
|
||||
ENDPOINT: git.legonzaur.fr
|
||||
|
||||
steps:
|
||||
@@ -23,12 +20,6 @@ jobs:
|
||||
lfs: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
run: |
|
||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: setup private key
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -40,7 +31,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ssh-keyscan ${{ env.ENDPOINT }} >> ~/.ssh/known_hosts
|
||||
git remote set-url origin git@${{ env.ENDPOINT }}:${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}.git
|
||||
git remote set-url origin git@${{ env.ENDPOINT }}:${{ env.REPO_NAME }}.git
|
||||
git lfs pull
|
||||
|
||||
- name: Login to DockerHub
|
||||
@@ -50,15 +41,28 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.ENDPOINT }}/${{ env.REPO_NAME }}
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
secrets: |
|
||||
"NUXT_PUBLIC_WEBSOCKET_ENDPOINT=${{ secrets.NUXT_PUBLIC_WEBSOCKET_ENDPOINT }}"
|
||||
"NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=${{ secrets.NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT }}"
|
||||
build-args: |
|
||||
NUXT_PUBLIC_ENDPOINT=${{ vars.NUXT_PUBLIC_ENDPOINT }}
|
||||
NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=${{ vars.NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT }}
|
||||
NUXT_PUBLIC_WELCOME_MESSAGE=${{ vars.NUXT_PUBLIC_WELCOME_MESSAGE }}
|
||||
push: true
|
||||
tags: | # replace it with your local IP and tags
|
||||
${{ env.ENDPOINT }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||
${{ env.ENDPOINT }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -22,3 +22,6 @@ logs
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
assets/images/*
|
||||
public/img/skins
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "client: chrome",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "server: nuxt",
|
||||
"outputCapture": "std",
|
||||
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
|
||||
"args": [
|
||||
"dev"
|
||||
],
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "fullstack: nuxt",
|
||||
"configurations": [
|
||||
"server: nuxt",
|
||||
"client: chrome"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+3
-1
@@ -3,5 +3,7 @@
|
||||
"**/.nuxt/": true,
|
||||
"**/node_modules": true
|
||||
},
|
||||
"vue.server.hybridMode": true
|
||||
"vue.server.hybridMode": true,
|
||||
"editor.formatOnSave": true,
|
||||
"javascript.updateImportsOnFileMove.enabled": "never"
|
||||
}
|
||||
+17
-9
@@ -1,16 +1,24 @@
|
||||
FROM node:lts-alpine as build-stage
|
||||
FROM node:lts-alpine AS install-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN --mount=type=secret,id=NUXT_PUBLIC_WEBSOCKET_ENDPOINT \
|
||||
--mount=type=secret,id=NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT \
|
||||
NUXT_PUBLIC_WEBSOCKET_ENDPOINT=$(cat /run/secrets/NUXT_PUBLIC_WEBSOCKET_ENDPOINT ) \
|
||||
NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=$(cat /run/secrets/NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT ) \
|
||||
npx nuxi generate
|
||||
|
||||
FROM git.legonzaur.fr/heronfief/cards-gen:main AS svg-stage
|
||||
RUN python3 -u ./src/generate_cards_mvp.py
|
||||
|
||||
FROM install-stage AS build-stage
|
||||
COPY --from=svg-stage /app/output/heron ./assets/images/cards/heron-svg
|
||||
COPY --from=svg-stage /app/img/skins ./public/img
|
||||
|
||||
ARG NUXT_PUBLIC_ENDPOINT
|
||||
ARG NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT
|
||||
ARG NUXT_PUBLIC_WELCOME_MESSAGE
|
||||
|
||||
RUN npx nuxi generate
|
||||
|
||||
# étape de production
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
COPY --from=build-stage /app/.output/public /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
COPY --from=svg-stage /app/img/skins /usr/share/nginx/html/img/skins
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
@@ -1,103 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { cards, compileCards } from "./services/loadCards";
|
||||
import { getCurrentInstance } from "vue";
|
||||
// import goStore, { oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
|
||||
|
||||
import goStore, { setReady, oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
|
||||
const nuxtApp = useNuxtApp();
|
||||
|
||||
await setup_websocket()
|
||||
const authStore = useAuthStore();
|
||||
|
||||
useHead
|
||||
({
|
||||
title
|
||||
: 'Héron Realms',
|
||||
})
|
||||
|
||||
|
||||
|
||||
const heron_session = useCookie('heron_session')
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(async () => {
|
||||
const code = route.query.code
|
||||
if (!code) { return }
|
||||
if (Array.isArray(code)) { return }
|
||||
await setup_websocket()
|
||||
oauth2_register_discord(code)
|
||||
history.pushState(
|
||||
{},
|
||||
'',
|
||||
'/'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
if (!heron_session.value) { return }
|
||||
heron_cookie_login(heron_session.value)
|
||||
})
|
||||
|
||||
function keyDownHandler(e: KeyboardEvent) {
|
||||
if (e.repeat) { return }
|
||||
console.log(e.code)
|
||||
if (e.code != "AltLeft") { return }
|
||||
goStore.zoomKeyPressed = true
|
||||
e.preventDefault()
|
||||
return false
|
||||
console.log("compiling cards...");
|
||||
const app = getCurrentInstance()?.appContext.app;
|
||||
if (!app) {
|
||||
throw new Error("Couldn't bake cards: instance is null");
|
||||
}
|
||||
await compileCards(app);
|
||||
console.log("compiling done");
|
||||
|
||||
function keyUpHandler(e: KeyboardEvent) {
|
||||
if (e.repeat) { return }
|
||||
if (e.code != "AltLeft") { return }
|
||||
goStore.zoomKeyPressed = false
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
useHead({
|
||||
title: "Héron Realms",
|
||||
});
|
||||
|
||||
function contextMenuHandler(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
function mouseDownHandler(e: MouseEvent) {
|
||||
if (e.button !== 2) { return }
|
||||
goStore.zoomKeyPressed = true
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
function mouseUpHandler(e: MouseEvent) {
|
||||
if (e.button !== 2) { return }
|
||||
goStore.zoomKeyPressed = false
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
function follow_mouse(e: MouseEvent) {
|
||||
goStore.clientMouseX = e.clientX
|
||||
goStore.clientMouseY = e.clientY
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("contextmenu", contextMenuHandler)
|
||||
window.addEventListener("mousedown", mouseDownHandler)
|
||||
window.addEventListener("mouseup", mouseUpHandler)
|
||||
document.addEventListener("mousemove", follow_mouse)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("contextmenu", contextMenuHandler)
|
||||
window.removeEventListener("mousedown", mouseDownHandler)
|
||||
window.removeEventListener("mouseup", mouseUpHandler)
|
||||
document.removeEventListener("mousemove", follow_mouse)
|
||||
})
|
||||
|
||||
onDeactivated
|
||||
await authStore.whoami();
|
||||
</script>
|
||||
<template>
|
||||
<div class="root_dom_div">
|
||||
<CardOverlay v-if="goStore.zoomKeyPressed" :card_id="goStore.hoveredCard" />
|
||||
<NuxtPage />
|
||||
<NuxtLoadingIndicator> Loading... </NuxtLoadingIndicator>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--animation-curve: cubic-bezier(0.68, -0.275, 0.32, 1.275);
|
||||
}
|
||||
html {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -107,6 +43,29 @@ body {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4caf50;
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 16px;
|
||||
width: max-content;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
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);
|
||||
padding: 10px 24px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: gray;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#__nuxt {
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<html>
|
||||
I LOVE LOADING SCREENS
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
#define FBM_OCTAVES 6
|
||||
|
||||
#include "lygia/generative/fbm.glsl"
|
||||
|
||||
uniform vec2 u_resolution;
|
||||
uniform vec2 u_mouse;
|
||||
uniform vec3 u_color_start;
|
||||
uniform vec3 u_color_end;
|
||||
uniform vec2 u_offset;
|
||||
uniform float u_time;
|
||||
|
||||
void main(){
|
||||
vec2 st=(gl_FragCoord.xy+u_offset)/u_resolution;
|
||||
|
||||
|
||||
vec2 q=vec2(0.);
|
||||
q.x=fbm(st);
|
||||
q.y=fbm(st+vec2(1.));
|
||||
|
||||
q=q*.5+.5;
|
||||
vec2 r=vec2(0.);
|
||||
r.x=fbm(st+1.*q+vec2(1.7,9.2)+.00215*u_time);
|
||||
r.y=fbm(st+1.*q+vec2(8.3,2.8)+.00526*u_time);
|
||||
r=r*.5+.5;
|
||||
|
||||
float f=fbm(st+r);
|
||||
f = floor((f+.4)*4.)/4.;
|
||||
vec3 color=mix(u_color_start,u_color_end,f*.3);
|
||||
|
||||
gl_FragColor=vec4(color,1.);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import noise from "~/assets/shaders/noise.frag?raw";
|
||||
|
||||
import resolveLygia from "~/assets/workers/resolve-lygia.esm";
|
||||
|
||||
let interval: number;
|
||||
let timeout: number;
|
||||
|
||||
const shaderOptions = { quality: 1, framerate: 1000 / 60 };
|
||||
const screen = { width: 0, height: 0, devicePixelRatio: 0 };
|
||||
|
||||
const _colorStart = [0, 0, 0];
|
||||
const _colorEnd = [0, 0, 0];
|
||||
|
||||
let fragmentShader = resolveLygia(noise);
|
||||
|
||||
let canvas: OffscreenCanvas;
|
||||
|
||||
let render: (time: number) => void;
|
||||
|
||||
let must_resize = false;
|
||||
|
||||
async function main(canvas: OffscreenCanvas) {
|
||||
const gl = canvas.getContext("webgl");
|
||||
if (!gl) {
|
||||
return;
|
||||
}
|
||||
// setup GLSL program
|
||||
const program = gl.createProgram();
|
||||
if (!program) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
if (!fragment) {
|
||||
return;
|
||||
}
|
||||
gl.shaderSource(fragment, fragmentShader);
|
||||
gl.compileShader(fragment);
|
||||
if (!gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
|
||||
const lastError = gl.getShaderInfoLog(fragment);
|
||||
console.error(lastError);
|
||||
gl.deleteShader(fragment);
|
||||
return;
|
||||
}
|
||||
|
||||
const vertex = gl.createShader(gl.VERTEX_SHADER);
|
||||
if (!vertex) {
|
||||
return;
|
||||
}
|
||||
gl.shaderSource(
|
||||
vertex,
|
||||
`attribute vec4 a_position;void main() {gl_Position = a_position;}`
|
||||
);
|
||||
gl.compileShader(vertex);
|
||||
if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS)) {
|
||||
const lastError = gl.getShaderInfoLog(vertex);
|
||||
console.error(lastError);
|
||||
gl.deleteShader(vertex);
|
||||
return;
|
||||
}
|
||||
|
||||
gl.attachShader(program, fragment);
|
||||
gl.attachShader(program, vertex);
|
||||
gl.linkProgram(program);
|
||||
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
if (!linked) {
|
||||
return;
|
||||
}
|
||||
// const program = webglUtils.createProgramFromSources(gl, [vs, fragmentShader2]);
|
||||
|
||||
// look up where the vertex data needs to go.
|
||||
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
|
||||
|
||||
// look up uniform locations
|
||||
const offset = gl.getUniformLocation(program, "u_offset");
|
||||
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
|
||||
const colorStart = gl.getUniformLocation(program, "u_color_start");
|
||||
const quality = gl.getUniformLocation(program, "u_quality");
|
||||
const colorEnd = gl.getUniformLocation(program, "u_color_end");
|
||||
const timeLocation = gl.getUniformLocation(program, "u_time");
|
||||
|
||||
// Create a buffer to put three 2d clip space points in
|
||||
const positionBuffer = gl.createBuffer();
|
||||
|
||||
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
// fill it with a 2 triangles that cover clipspace
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([
|
||||
-1,
|
||||
-1, // first triangle
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
1,
|
||||
-1,
|
||||
1, // second triangle
|
||||
1,
|
||||
-1,
|
||||
1,
|
||||
1,
|
||||
]),
|
||||
gl.STATIC_DRAW
|
||||
);
|
||||
|
||||
const gl2 = gl;
|
||||
function render(time: number) {
|
||||
if (must_resize) {
|
||||
canvas.width = (screen.width * shaderOptions.quality) / 10;
|
||||
canvas.height = (screen.height * shaderOptions.quality) / 10;
|
||||
must_resize = false;
|
||||
}
|
||||
|
||||
time *= 0.001; // convert to seconds
|
||||
// Tell WebGL how to convert from clip space to pixels
|
||||
gl2.viewport(0, 0, gl2.canvas.width, gl2.canvas.height);
|
||||
|
||||
// Tell it to use our program (pair of shaders)
|
||||
gl2.useProgram(program);
|
||||
|
||||
// Turn on the attribute
|
||||
gl2.enableVertexAttribArray(positionAttributeLocation);
|
||||
|
||||
// Bind the position buffer.
|
||||
gl2.bindBuffer(gl2.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
||||
gl2.vertexAttribPointer(
|
||||
positionAttributeLocation,
|
||||
2, // 2 components per iteration
|
||||
gl2.FLOAT, // the data is 32bit floats
|
||||
false, // don't normalize the data
|
||||
0, // 0 = move forward size * sizeof(type) each iteration to get the next position
|
||||
0 // start at the beginning of the buffer
|
||||
);
|
||||
|
||||
const secondRatio = screen.width / screen.height;
|
||||
|
||||
if (secondRatio < 1) {
|
||||
const half = canvas.height / 2;
|
||||
gl2.uniform2f(
|
||||
resolutionLocation,
|
||||
canvas.width,
|
||||
canvas.height * secondRatio
|
||||
);
|
||||
gl2.uniform2f(offset, 0, -half + half / (1 / secondRatio));
|
||||
} else {
|
||||
const half = canvas.width / 2;
|
||||
gl2.uniform2f(
|
||||
resolutionLocation,
|
||||
canvas.width / secondRatio,
|
||||
canvas.height
|
||||
);
|
||||
gl2.uniform2f(offset, -half + half / secondRatio, 0);
|
||||
}
|
||||
gl2.uniform1i(quality, shaderOptions.quality);
|
||||
|
||||
gl2.uniform3f(colorStart, _colorStart[0], _colorStart[1], _colorStart[2]);
|
||||
gl2.uniform3f(colorEnd, _colorEnd[0], _colorEnd[1], _colorEnd[2]);
|
||||
gl2.uniform1f(timeLocation, time);
|
||||
gl2.drawArrays(
|
||||
gl2.TRIANGLES,
|
||||
0, // offset
|
||||
6 // num vertices to process
|
||||
);
|
||||
if (timeout) {
|
||||
self.clearTimeout(timeout);
|
||||
timeout = self.setTimeout(() => {
|
||||
requestAnimationFrame(render);
|
||||
}, 1000 / shaderOptions.framerate);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(render);
|
||||
|
||||
// self.clearInterval(interval)
|
||||
// interval = self.setInterval(() => requestAnimationFrame(render), 1000 / shaderOptions.framerate)
|
||||
self.clearTimeout(timeout);
|
||||
if (shaderOptions.framerate == 0) {
|
||||
return render;
|
||||
}
|
||||
timeout = self.setTimeout(() => {
|
||||
requestAnimationFrame(render);
|
||||
}, 1000 / shaderOptions.framerate);
|
||||
return render;
|
||||
}
|
||||
|
||||
self.addEventListener("message", (e) => {
|
||||
const data = e.data;
|
||||
if (data.command == "initShader") {
|
||||
canvas = data.canvas;
|
||||
shaderOptions.quality = data.shaderOptions.quality;
|
||||
shaderOptions.framerate = data.shaderOptions.framerate;
|
||||
if (shaderOptions.framerate == 65) {
|
||||
shaderOptions.framerate == Infinity;
|
||||
}
|
||||
screen.devicePixelRatio = data.screen.devicePixelRatio;
|
||||
screen.width = data.screen.width;
|
||||
screen.height = data.screen.height;
|
||||
loadShader();
|
||||
} else if (data.command == "killShader") {
|
||||
self.clearInterval(interval);
|
||||
self.clearTimeout(timeout);
|
||||
} else if (data.command == "resumeShader") {
|
||||
self.clearInterval(interval);
|
||||
self.clearTimeout(timeout);
|
||||
shaderOptions.framerate = data.shaderOptions.framerate;
|
||||
if (shaderOptions.framerate == 65) {
|
||||
shaderOptions.framerate == Infinity;
|
||||
}
|
||||
timeout = self.setTimeout(() => {
|
||||
requestAnimationFrame(render);
|
||||
}, 1000 / shaderOptions.framerate);
|
||||
// interval = self.setInterval(() => requestAnimationFrame(render), 1000 / shaderOptions.framerate)
|
||||
} else if (data.command == "recolorShader") {
|
||||
_colorStart[0] = data.colorStart[0];
|
||||
_colorStart[1] = data.colorStart[1];
|
||||
_colorStart[2] = data.colorStart[2];
|
||||
_colorEnd[0] = data.colorEnd[0];
|
||||
_colorEnd[1] = data.colorEnd[1];
|
||||
_colorEnd[2] = data.colorEnd[2];
|
||||
} else if (data.command == "resizeShader") {
|
||||
shaderOptions.quality = data.shaderOptions.quality;
|
||||
screen.devicePixelRatio = data.screen.devicePixelRatio;
|
||||
screen.width = data.screen.width;
|
||||
screen.height = data.screen.height;
|
||||
must_resize = true;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadShader() {
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
// let fragmentShader2 = fragmentShader.replace("#define FBM_OCTAVES 6", "#define FBM_OCTAVES " + shaderOptions.quality);
|
||||
const temp = await main(canvas);
|
||||
if (temp) {
|
||||
render = temp;
|
||||
}
|
||||
postMessage("ready");
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
function getFile(url) {
|
||||
if(url == "https://lygia.xyz/generative/fbm.glsl"){
|
||||
url = "/fbm.glsl"
|
||||
}
|
||||
let httpRequest = new XMLHttpRequest();
|
||||
httpRequest.open("GET", url, false);
|
||||
httpRequest.send();
|
||||
if (httpRequest.status == 200)
|
||||
return httpRequest.responseText;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
const lygiaURL = "https://lygia.xyz"
|
||||
|
||||
function resolveLygia(lines) {
|
||||
if ( !Array.isArray(lines) ) {
|
||||
lines = lines.split(/\r?\n/);
|
||||
}
|
||||
|
||||
let src = "";
|
||||
lines.forEach( (line, i) => {
|
||||
const line_trim = line.trim();
|
||||
if (line_trim.startsWith('#include \"lygia') ) {
|
||||
let include_url = line_trim.substring(15);
|
||||
include_url = lygiaURL + include_url.replace(/\"|\;|\s/g,'');
|
||||
src += getFile(include_url) + '\n';
|
||||
}
|
||||
else {
|
||||
src += line + '\n';
|
||||
}
|
||||
});
|
||||
return src;
|
||||
}
|
||||
|
||||
async function resolveLygiaAsync(lines) {
|
||||
if (!Array.isArray(lines))
|
||||
lines = lines.split(/\r?\n/);
|
||||
|
||||
let src = "";
|
||||
const response = await Promise.all(
|
||||
lines.map(async (line, i) => {
|
||||
const line_trim = line.trim();
|
||||
if (line_trim.startsWith('#include "lygia')) {
|
||||
let include_url = line_trim.substring(15);
|
||||
include_url = lygiaURL + include_url.replace(/\"|\;|\s/g, "");
|
||||
return fetch(include_url).then((res) => res.text());
|
||||
}
|
||||
else
|
||||
return line;
|
||||
})
|
||||
);
|
||||
|
||||
return response.join("\n");
|
||||
}
|
||||
export { resolveLygia as default };
|
||||
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
import gsap from "gsap";
|
||||
import ShaderWorker from "~/assets/workers/ShaderWorker?worker";
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: [];
|
||||
}>();
|
||||
|
||||
let worker: Worker;
|
||||
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null);
|
||||
|
||||
export interface Props {
|
||||
isTurn?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isTurn: () => false,
|
||||
});
|
||||
|
||||
const _colorStart = reactive([0, 0.3, 0]);
|
||||
const _colorEnd = reactive([0, 1, 0]);
|
||||
|
||||
if (props.isTurn) {
|
||||
Object.assign(_colorStart, [0, 0.3, 0]);
|
||||
Object.assign(_colorEnd, [0, 1, 0]);
|
||||
} else {
|
||||
Object.assign(_colorStart, [0, 0.2, 0.3]);
|
||||
Object.assign(_colorEnd, [0, 1, 1]);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function recolorShader() {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function killShader() {
|
||||
worker.postMessage({
|
||||
command: "killShader",
|
||||
});
|
||||
}
|
||||
|
||||
function resumeShader() {
|
||||
worker.postMessage({
|
||||
command: "resumeShader",
|
||||
shaderOptions: {
|
||||
quality: settingsStore.shaderQuality,
|
||||
framerate: settingsStore.shaderFramerate,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [settingsStore.shaderFramerate],
|
||||
() => {
|
||||
if (settingsStore.shaderFramerate == 0) {
|
||||
killShader();
|
||||
return;
|
||||
}
|
||||
resumeShader();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [settingsStore.shaderQuality],
|
||||
() => {
|
||||
resizeShader();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.isTurn],
|
||||
() => {
|
||||
if (props.isTurn) {
|
||||
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: 0.3, 2: 0 });
|
||||
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 0 });
|
||||
} else {
|
||||
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: 0.2, 2: 0.3 });
|
||||
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 1 });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch([_colorEnd, _colorStart], () => {
|
||||
recolorShader();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
worker = new ShaderWorker();
|
||||
recolorShader();
|
||||
initShader().then(() => {
|
||||
emit("ready");
|
||||
});
|
||||
window.addEventListener("resize", resizeShader, true);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", resizeShader);
|
||||
killShader();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
|
||||
height="270"></canvas>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#background {
|
||||
left: 0;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: -999999;
|
||||
}
|
||||
|
||||
#background.pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore from '~/netcode';
|
||||
|
||||
export interface Props {
|
||||
card_id: number | undefined
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const card: Ref<Element | null> = ref(null)
|
||||
|
||||
const x = computed(() => {
|
||||
const bounds = card.value?.getBoundingClientRect()
|
||||
let offset = 1
|
||||
if (!bounds) { return 0 }
|
||||
if ((bounds.width + goStore.clientMouseX) > (window.visualViewport?.width ?? Infinity)) {
|
||||
offset = - (bounds.width + 1)
|
||||
}
|
||||
return goStore.clientMouseX + offset
|
||||
})
|
||||
const y = computed(() => {
|
||||
const bounds = card.value?.getBoundingClientRect()
|
||||
let offset = 1
|
||||
if (!bounds) { return 0 }
|
||||
if ((bounds.height + goStore.clientMouseY) > (window.visualViewport?.height ?? Infinity)) {
|
||||
offset = - (bounds.height + 1)
|
||||
}
|
||||
return goStore.clientMouseY + offset
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`card_overlay`" :style="`top: ${y}px; left: ${x}px`" ref="card">
|
||||
<img :class="`card_image`" v-if="props.card_id"
|
||||
:src="'/cards/' + props.card_id.toString().padStart(3, '0') + '.png'" draggable="false">
|
||||
|
||||
<!-- <img :class="`card_image`" v-if="!props.card_id" :src="'/cards/background.png'" draggable="false"> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card_overlay {
|
||||
/* display: none; */
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,128 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore from '~/netcode';
|
||||
|
||||
export interface Props {
|
||||
card_id: number | undefined
|
||||
locked?: boolean
|
||||
wrapped?: boolean
|
||||
tilted?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
wrapped: () => false,
|
||||
tilted: () => true
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(["click"])
|
||||
|
||||
const childCount = ref<number>(0)
|
||||
|
||||
function cardClick() {
|
||||
if (!props.card_id) return
|
||||
emit("click")
|
||||
}
|
||||
|
||||
const rotation = ref(0)
|
||||
|
||||
function mouseenter(e: Event) {
|
||||
goStore.hoveredCard = props.card_id
|
||||
}
|
||||
function mouseleave(e: Event) {
|
||||
goStore.hoveredCard = undefined
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.card_id,
|
||||
() => {
|
||||
props.wrapped ? rotation.value = (Math.random() * 4) - 2 : rotation.value = Math.random() > .5 ? 2 : -2
|
||||
},
|
||||
{ immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`card ${childCount > 0 ? 'active' : ''} ${wrapped ? 'wrapped' : ''} ${props.locked ? 'locked' : ''}`"
|
||||
@contextmenu.prevent="false" @click.left="cardClick" :style="tilted ? `rotate:${rotation}deg;` : ''"
|
||||
@mouseenter="mouseenter" @mouseleave="mouseleave">
|
||||
<div id="contextMenuButtons" :ref="(el) => { childCount = ((el as Element)?.childElementCount ?? 0) }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<img :class="`card_image`" v-if="props.card_id"
|
||||
:src="'/cards/' + props.card_id.toString().padStart(3, '0') + '.png'" draggable="false">
|
||||
|
||||
<img :class="`card_image`" v-if="!props.card_id" :src="'/cards/background.png'" draggable="false">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
transition: 0.025s rotate linear;
|
||||
max-width: 256px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 15px;
|
||||
transition: all .125s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
isolation: isolate;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.card.active {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.card_image {
|
||||
max-height: 250px;
|
||||
transition: all .125s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card_image.outlined {
|
||||
outline: 2px solid red;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.card.locked {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 15px;
|
||||
filter: drop-shadow(5px 15px 2px rgba(0, 0, 0, .60));
|
||||
}
|
||||
|
||||
.card.wrapped {
|
||||
max-width: 10px;
|
||||
}
|
||||
|
||||
.card_image {
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
#contextMenuButtons {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
word-break: break-word;
|
||||
bottom: 0;
|
||||
width: 99%;
|
||||
transform: translateY(-0px);
|
||||
transition: 0.1s transform cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.card.active:hover #contextMenuButtons {
|
||||
transform: translateY(100%);
|
||||
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
#contextMenuButtons>* {
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
min-height: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Card } from '~/netcode/Card';
|
||||
|
||||
export interface Props {
|
||||
stack: (Card | null)[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
stack: () => [],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="cards_container">
|
||||
<template v-if="props.stack.length > 0" v-for="c in props.stack">
|
||||
<CardPreview :card_id="c?.card_id" :wrapped="true">
|
||||
<slot></slot>
|
||||
</CardPreview>
|
||||
</template>
|
||||
<template v-if="props.stack.length == 0">
|
||||
<div class="empty">
|
||||
<img :class="`card_image`" :src="'/cards/background.png'" draggable="false">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cards_container::before {
|
||||
width: calc(185px - 10px);
|
||||
display: block;
|
||||
content: "";
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.cards_container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty {
|
||||
max-width: 10px;
|
||||
}
|
||||
|
||||
.empty>.card_image {
|
||||
filter: brightness(0.5);
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,128 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore from '~/netcode';
|
||||
import type { Player } from '~/netcode/Player';
|
||||
import { playSound } from '~/helpers/audioHelpers';
|
||||
|
||||
const current_players = computed<Array<Player>>(() => Array.from(goStore.current_game?.players ?? []).filter(p => p.team === goStore.current_game?.current_turn))
|
||||
|
||||
watch(
|
||||
() => goStore.current_game?.current_turn?.uuid,
|
||||
() => {
|
||||
if (goStore.current_game?.current_turn?.uuid == goStore.self?.team?.uuid) {
|
||||
playSound('/sounds/ding.mp3')
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => goStore.current_game?.losers,
|
||||
() => {
|
||||
if (goStore.self?.uuid && goStore.current_game?.losers && goStore.current_game.losers.includes(goStore.self.uuid)) {
|
||||
showLoserScreen()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => goStore.current_game?.ended,
|
||||
() => {
|
||||
if (goStore.current_game?.ended && goStore.self?.uuid && goStore.current_game?.losers && !goStore.current_game.losers.includes(goStore.self.uuid)) {
|
||||
showWinnerScreen()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => goStore.current_game?.players,
|
||||
(newList, oldList) => {
|
||||
if (!oldList || !newList) { return }
|
||||
if (goStore.current_game?.started) { return }
|
||||
if (newList?.length > oldList.length) {
|
||||
playSound('/sounds/teleport.mp3')
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const showResults = ref(false)
|
||||
const isWinner = ref(false)
|
||||
|
||||
function showLoserScreen() {
|
||||
// playSound('/sounds/ding.mp3')
|
||||
showResults.value = true
|
||||
isWinner.value = false
|
||||
}
|
||||
|
||||
function showWinnerScreen() {
|
||||
// playSound('/sounds/ding.mp3')
|
||||
showResults.value = true
|
||||
isWinner.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @contextmenu.prevent="false" id="game_board">
|
||||
<div id="market">
|
||||
<MarketCards></MarketCards>
|
||||
</div>
|
||||
<div id="player_list">
|
||||
<PlayerListElement v-for="p in goStore.current_game?.players" :player="p">
|
||||
</PlayerListElement>
|
||||
</div>
|
||||
<div id="current_player"
|
||||
:class="(goStore.current_game?.current_turn && (goStore.current_game?.current_turn === goStore.self?.team)) ? 'self' : ''">
|
||||
<PlayerSlot v-for="p in current_players" :player="p"></PlayerSlot>
|
||||
</div>
|
||||
</div>
|
||||
<ResultDialog
|
||||
:isVisible="showResults"
|
||||
:won="isWinner"
|
||||
:players="goStore.current_game?.players"
|
||||
@close="showResults = false">
|
||||
</ResultDialog>
|
||||
<SelfView></SelfView>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#game_board {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
grid-template-rows: min-content 1fr;
|
||||
grid-template-areas:
|
||||
"market player_list"
|
||||
"current_player player_list";
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#player_list,
|
||||
#current_player {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
#current_player.self {
|
||||
background-image: url('../images/bg-grass.png');
|
||||
}
|
||||
|
||||
#market {
|
||||
grid-area: market;
|
||||
background-image: url("/images/bg-sand.png");
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#current_player {
|
||||
grid-area: current_player;
|
||||
background-image: url("/images/bg-darkgrass.png");
|
||||
|
||||
}
|
||||
|
||||
#player_list {
|
||||
grid-area: player_list;
|
||||
border-left: 5px solid black;
|
||||
height: 100%;
|
||||
background-image: url("/images/bg-darkgrass.png");
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
#player_list .card_image {
|
||||
max-width: 64px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,244 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore, { netcode_create_game, join_game } from '~/netcode';
|
||||
import { playSound } from '~/helpers/audioHelpers';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const erika_alive = ref(true);
|
||||
const router = useRouter();
|
||||
|
||||
function killErika() {
|
||||
playSound('/sounds/splat1.mp3')
|
||||
erika_alive.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<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="netcode_create_game">Create Game</button>
|
||||
</div>
|
||||
|
||||
<div class="games-list-zone">
|
||||
<div v-for="g in goStore.lobby?.games">
|
||||
<button v-if="g" @click="join_game(g.uuid)" :class="goStore.discordId == g.owner ? 'own_game' : ''">
|
||||
<template v-if="g.started && g.ended">
|
||||
GAME OVER
|
||||
</template>
|
||||
<template v-if="g.players.every(p => p.discord_id !== goStore.discordId)">
|
||||
<template v-if="!g.started">
|
||||
JOIN
|
||||
</template>
|
||||
<template v-if="g.started && !g.ended">
|
||||
SPECTATE
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="g.players.some(p => p.discord_id == goStore.discordId)">
|
||||
<template v-if="!g.started">
|
||||
JOIN BACK
|
||||
</template>
|
||||
<template v-if="g.started && !g.ended">
|
||||
CONTINUE
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<br>
|
||||
{{ g.players.length }} current players:<br><span>{{ g.players.map(p => p.username).join(",")
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" v-if="erika_alive">
|
||||
<div class="erika-zone">
|
||||
<img @click="router.push({ path: '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>
|
||||
|
||||
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.middle-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 100px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.create-game-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.games-list-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
button.own_game {
|
||||
border: solid yellow 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
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;
|
||||
|
||||
.overlay-image {
|
||||
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;
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.erika-zone:hover {
|
||||
.overlay-button {
|
||||
.kill-erika-button {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,106 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore, { netcode_create_game, join_game } from '~/netcode';
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
|
||||
async function discordLogin() {
|
||||
await navigateTo(config.public.discordLoginEndpoint, {
|
||||
external: true
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="background">
|
||||
<div class="title">
|
||||
Entrez dans le Royaume de Héron !
|
||||
</div>
|
||||
|
||||
<div class="middle-row">
|
||||
<div class="icon">
|
||||
<CardPreview :card_id="undefined"></CardPreview>
|
||||
</div>
|
||||
|
||||
<div v-if="goStore.current_game?.started">
|
||||
<b>Mode spectateur</b>
|
||||
</div>
|
||||
|
||||
<template v-if="!goStore.current_game?.started">
|
||||
<!-- <input type="text" v-model="username" />
|
||||
<button @click="register(username)">LOGIN</button> -->
|
||||
<button class="menu-button" @click="discordLogin()">Se connecter via Discord</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
Héron Realms n'est en aucun cas affilié à Hero Realms.<br>
|
||||
Le concept et certains noms de cartes sont la propriété de Wise Wizard Games.<br>
|
||||
Les illustrations sont la propriété de leurs auteurs respectifs (il y en a plein).<br>
|
||||
Ce magnifique fond d'écran est tiré de Fire Emblem: Engage.<br>
|
||||
Programmeur principal, owner du projet: Legonzaur
|
||||
</p>
|
||||
</footer>
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.middle-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 100px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.menu-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: 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);
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
width: 100%;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore, { buy, useEffect } from '@/netcode';
|
||||
import { CardEffects, type Effect } from '@/netcode/Effect'
|
||||
import { CardType } from '@/netcode/Card'
|
||||
|
||||
const isTurn = computed(() => goStore.self && goStore.current_game?.started && (goStore.self.team == goStore.current_game.current_turn))
|
||||
const clientStore = useClientSideStore()
|
||||
|
||||
const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="market">
|
||||
<CardPreview :tilted="false" :card_id="undefined"></CardPreview>
|
||||
<div class="separator"></div>
|
||||
<CardPreview :tilted="false" :card_id="c?.card_id" v-for="c in goStore.current_game?.market"
|
||||
@click="c && buy(c)">
|
||||
<template
|
||||
v-if="isTurn && goStore.self?.turn && c && c.cost && (goStore.self?.turn.gold_reserve ?? 0) >= c.cost">
|
||||
<button @click.stop="buy(c)">BUY</button>
|
||||
<button @click.stop="clientStore.findUseEffect(CardEffects.PLAY_NEXT_CARD_BOUGHT, c);"
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PLAY_NEXT_CARD_BOUGHT) && e.usable)">
|
||||
BUY IN HAND</button>
|
||||
<button @click.stop="clientStore.findUseEffect(CardEffects.STACK_NEXT_CARD_BOUGHT, c)"
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STACK_NEXT_CARD_BOUGHT) && e.usable)">
|
||||
BUY IN STACK (generic)</button>
|
||||
|
||||
<button @click.stop="clientStore.findUseEffect(CardEffects.STACK_NEXT_ACTION_BOUGHT, c)"
|
||||
v-if="c.card_type == CardType.ACTION && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STACK_NEXT_ACTION_BOUGHT) && e.usable)">
|
||||
BUY IN STACK (actions)</button>
|
||||
</template>
|
||||
</CardPreview>
|
||||
<div class="separator"></div>
|
||||
<CardPreview :tilted="false" :card_id="goStore.current_game?.gem_stack.at(-1)?.card_id"
|
||||
@click="buy(goStore.current_game?.gem_stack.at(-1)!)">
|
||||
<template
|
||||
v-if="isTurn && goStore.self?.turn && goStore.current_game?.gem_stack.length && goStore.current_game?.gem_stack.length > 0 && (goStore.self?.turn.gold_reserve ?? 0) >= (goStore.current_game?.gem_stack.at(-1)?.cost ?? Infinity)">
|
||||
<button @click.stop="buy(goStore.current_game.gem_stack.at(-1)!)">
|
||||
BUY
|
||||
</button>
|
||||
<button @click.stop="clientStore.findUseEffect(CardEffects.PLAY_NEXT_CARD_BOUGHT, buyableFiregem)"
|
||||
v-if="buyableFiregem && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PLAY_NEXT_CARD_BOUGHT) && e.usable)">
|
||||
BUY IN HAND</button>
|
||||
<button @click.stop="clientStore.findUseEffect(CardEffects.STACK_NEXT_CARD_BOUGHT, buyableFiregem)"
|
||||
v-if="buyableFiregem && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STACK_NEXT_CARD_BOUGHT) && e.usable)">
|
||||
BUY IN STACK(generic)
|
||||
</button>
|
||||
</template>
|
||||
</CardPreview>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#market {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Player } from '~/netcode/Player';
|
||||
|
||||
|
||||
export interface Props {
|
||||
player: Player;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player-icon-container">
|
||||
<img class="avatar" v-if="props.player.image" :src="props.player.image">
|
||||
<b class="player-name">{{ props.player.username }}</b>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.player-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 20px;
|
||||
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin-left: 10px;
|
||||
|
||||
color: white;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore from '@/netcode';
|
||||
import type { Player } from "~/netcode/Player";
|
||||
|
||||
const view = ref<"main" | "discard">("main")
|
||||
|
||||
function show(data: "main" | "discard") {
|
||||
view.value = data
|
||||
}
|
||||
|
||||
|
||||
export interface Props {
|
||||
player: Player;
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`player_list_element ${goStore.self === props.player ? 'self' : ''}`">
|
||||
<div class="button" id="player_list_buttons">
|
||||
<div class="main_button" @click="show('main')">
|
||||
<div class="tooltip">Board</div>
|
||||
<img :src="`/cards/${player.board.at(-1)?.card_id.toString().padStart(3, '0') ?? 'background'}.png`"
|
||||
draggable="false" :class="view === 'main' && 'selected'">
|
||||
<span class="button_card_number">{{ player.board.length }}</span>
|
||||
|
||||
</div>
|
||||
<div class="stack_button">
|
||||
<div class="tooltip">Stack</div>
|
||||
<img :src="'/cards/background.png'" draggable="false">
|
||||
<span class="button_card_number">{{ player.stack_pile.length }}</span>
|
||||
|
||||
</div>
|
||||
<div class="discard_button" @click="show('discard')">
|
||||
<div class="tooltip">Discard</div>
|
||||
<img :src="`/cards/${player.discard_pile.at(-1)?.card_id.toString().padStart(3, '0') ?? 'background'}.png`"
|
||||
draggable="false" :class="view === 'discard' && 'selected'">
|
||||
<span class="button_card_number">{{ player.discard_pile.length }}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view">
|
||||
<PlayerSlot :player="player" :hide_stack_and_discard="true" v-if="view == 'main'" />
|
||||
<template v-if="view == 'discard'">
|
||||
<CardPreview :tilted="false" v-for=" card in player.discard_pile " :card_id="card.card_id">
|
||||
</CardPreview>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button>div>img:not(.selected) {
|
||||
filter: brightness(.5)
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tooltip {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
transition: .1s transform;
|
||||
background: white;
|
||||
right: 0;
|
||||
border-top-right-radius: 1em;
|
||||
border-bottom-right-radius: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button>div {
|
||||
flex: 1 1 0;
|
||||
border-right: 5px solid black;
|
||||
width: 45px;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button>div:not(.stack_button) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button div:hover .tooltip {
|
||||
visibility: visible;
|
||||
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.button_card_number {
|
||||
font-size: 2em;
|
||||
position: absolute;
|
||||
text-shadow: white 0 0 1px;
|
||||
}
|
||||
|
||||
|
||||
.button>div:not(:last-child) {
|
||||
border-bottom: 5px solid black;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.player_list_element {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-areas:
|
||||
"button player";
|
||||
border-bottom: 5px solid black;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.player_list_element.self {
|
||||
background-image: url('../images/bg-grass.png');
|
||||
}
|
||||
|
||||
.view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
#player_list_buttons img {
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.view #contextMenuButtons button:not(.enemy_action) {
|
||||
display: none
|
||||
}
|
||||
</style>
|
||||
@@ -1,557 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore, {
|
||||
useEffect,
|
||||
playCard,
|
||||
playAllCards,
|
||||
discard,
|
||||
lockCard,
|
||||
heal,
|
||||
damage_champion,
|
||||
damage_team,
|
||||
endTurn
|
||||
|
||||
} from "~/netcode";
|
||||
import { CardType, type Card } from "~/netcode/Card";
|
||||
import type { Player } from "~/netcode/Player";
|
||||
import { CardEffects, type Effect, EffectType } from "@/netcode/Effect";
|
||||
import type { Team } from "~/netcode/Team";
|
||||
import { useClientSideStore } from "~/stores/turn";
|
||||
|
||||
export interface Props {
|
||||
player: Player;
|
||||
hide_stack_and_discard?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hide_stack_and_discard: () => false,
|
||||
})
|
||||
|
||||
const discard_unwrapped = ref(false)
|
||||
const isPlayerTurn = computed(() => props.player.team === goStore.current_game?.current_turn);
|
||||
const isSelf = computed(() => props.player.uuid === goStore.self?.uuid);
|
||||
const isSelfTurn = computed(() => goStore.self?.team === goStore.current_game?.current_turn)
|
||||
const isPlayerEnemy = computed(() => goStore.self?.team !== props.player.team)
|
||||
const warningText = ref("Effets restants!")
|
||||
const clientStore = useClientSideStore()
|
||||
|
||||
const make_discard = (target: Player) =>
|
||||
clientStore.findUseEffect(CardEffects.MAKE_DISCARD, target);
|
||||
const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
|
||||
const stun = (target: Card) => clientStore.findUseEffect(CardEffects.STUN, target)
|
||||
const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
|
||||
const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
|
||||
const prepare_champion = (target: Card) => clientStore.findUseEffect(CardEffects.PREPARE, target)
|
||||
|
||||
|
||||
let warning = ref(false)
|
||||
|
||||
function has_actions_remaining(): boolean {
|
||||
return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false)
|
||||
|| (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= (goStore.current_game?.market.reduce((min, card) => Math.min(min, card?.cost ?? Infinity), Infinity) ?? Infinity) : false)
|
||||
|| (goStore.self?.turn?.fuck_u_markers ? goStore.self?.turn?.fuck_u_markers > 0 : false)
|
||||
|| !goStore.self?.turn?.effects_availables.every(e => !e.usable || e.effect_type == EffectType.SUICIDE)
|
||||
|| !goStore.self?.board.every(c => (c.uuid in (goStore.self?.turn?.cards_locked ?? {})))
|
||||
|| false
|
||||
}
|
||||
|
||||
function can_buy_fire_gem(): boolean {
|
||||
return (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= 2 : false)
|
||||
}
|
||||
|
||||
function checkEndTurn() {
|
||||
// TODO: Add a confirmation dialog if resources are left
|
||||
console.log("Checking end turn")
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
function damage_team_all(team: Team) {
|
||||
for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) {
|
||||
damage_team(team)
|
||||
}
|
||||
}
|
||||
|
||||
function check_for_guard(player: Player) {
|
||||
return player.board.some(c => c.guard === true)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`player ${isPlayerTurn ? 'turn' : ''} ${discard_unwrapped ? 'discard_unwrapped' : ''}`">
|
||||
<template v-if="!hide_stack_and_discard">
|
||||
<div class="stack">
|
||||
|
||||
<CardsGrouped :stack="props.player.stack_pile">
|
||||
<!-- <button v-if="isSelf && isTurn" @click="draw">DRAW</button> -->
|
||||
</CardsGrouped>
|
||||
</div>
|
||||
|
||||
<div class="discard" :class="{ invisible: discard_unwrapped }" @click="() => discard_unwrapped = true">
|
||||
<CardsGrouped :stack="props.player.discard_pile" :visible="!discard_unwrapped"> </CardsGrouped>
|
||||
</div>
|
||||
|
||||
<div class="discard_overlay" v-if="discard_unwrapped">
|
||||
<CardPreview :tilted="false" v-if="discard_unwrapped" :card_id="c.card_id"
|
||||
v-for="c in props.player.discard_pile">
|
||||
<template v-if="goStore.self?.turn && c && isSelf && isPlayerTurn">
|
||||
<button
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
|
||||
@click.stop="sacrifice(c)">
|
||||
SACRIFICE
|
||||
</button>
|
||||
<button
|
||||
v-if="c.card_type == CardType.CHAMPION && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.RESTACK_DISCARDED_CHAMPION) && e.usable)"
|
||||
@click.stop="restack_discarded_champion(c)">
|
||||
RESTACK_DISCARDED_CHAMPION
|
||||
</button>
|
||||
<button
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.RESTACK_DISCARDED_CARD) && e.usable)"
|
||||
@click.stop="restack_discarded_card(c)">
|
||||
RESTACK_DISCARDED_CARD
|
||||
</button>
|
||||
|
||||
</template>
|
||||
</CardPreview>
|
||||
</div>
|
||||
<div :id="`discard_unwrapped_bg`" :class="`${discard_unwrapped ? 'unwrapped' : ''}`"
|
||||
@click="() => discard_unwrapped = false"></div>
|
||||
</template>
|
||||
<div id="player_banner" :class="hide_stack_and_discard ? 'small' : 'big'">
|
||||
<div class="ready-bubble not-ready" v-if="!props.player.ready && !goStore.current_game?.started">Pas encore
|
||||
prêt...
|
||||
</div>
|
||||
<div class="ready-bubble ready" v-if="props.player.ready && !goStore.current_game?.started">Prêt!</div>
|
||||
<div id="player_info">
|
||||
<PlayerIcon :player="props.player"></PlayerIcon>
|
||||
</div>
|
||||
<div id="player_stats">
|
||||
<template v-if="goStore.current_game?.started">
|
||||
<PlayerStats :player="props.player">
|
||||
</PlayerStats>
|
||||
<template v-if="!hide_stack_and_discard">
|
||||
<button id="end_turn" v-if="isSelfTurn" class="end_turn_button turn_icon"
|
||||
@click="checkEndTurn">END
|
||||
TURN</button>
|
||||
|
||||
<div class="warning"
|
||||
:class="{ 'warning-fire-gem': warningText == 'P\'tite Fire gem?', 'visible': warning }">
|
||||
/!\<br>{{ warningText }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<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)">
|
||||
Damage
|
||||
</button>
|
||||
<button @click.stop="damage_team_all(props.player.team)"
|
||||
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team"
|
||||
class="damage_button all_damage_button"
|
||||
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
|
||||
(All)
|
||||
</button>
|
||||
<button v-if="goStore.self && isSelfTurn && isPlayerEnemy && goStore.self?.turn"
|
||||
@click.stop="make_discard(props.player)" class="make_discard_button"
|
||||
:disabled="!clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.MAKE_DISCARD) && e.usable)">
|
||||
make Discard
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="cards_container">
|
||||
<!-- HAND -->
|
||||
<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"
|
||||
@click.stop="discard(c)">
|
||||
DISCARD
|
||||
</button>
|
||||
</CardPreview>
|
||||
<!-- BOARD -->
|
||||
<CardPreview :card_id="c.card_id" v-for="c in props.player.board" @click="lockCard(c)"
|
||||
:locked="props.player.turn && c.uuid in props.player.turn.cards_locked">
|
||||
|
||||
<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">
|
||||
<template v-if="e.card == c">
|
||||
<button v-if="!clientStore.effectsHandled.has(e.effect)" @click.stop="useEffect(e)"
|
||||
:disabled="!e.usable"
|
||||
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
|
||||
EFFECT<br>
|
||||
{{ e.effect + " " + (e.effect_type ?? '') }}
|
||||
</button>
|
||||
<button v-if="clientStore.effectsHandled.has(e.effect)"
|
||||
@click.stop="clientStore.activateEffect(e)" :disabled="!e.usable"
|
||||
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
|
||||
EFFECT<br>
|
||||
{{ e.effect + " " + (e.effect_type ?? '') }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<button
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PREPARE) && e.usable) && c.card_type == CardType.CHAMPION"
|
||||
@click.stop="prepare_champion(c)">PREPARE</button>
|
||||
</template>
|
||||
<!-- Card Unlocked -->
|
||||
<template v-if="!(c.uuid in props.player.turn.cards_locked)">
|
||||
<button v-if="(props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
|
||||
@click.stop="discard(c)">
|
||||
DISCARD
|
||||
</button>
|
||||
<button
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
|
||||
@click.stop="sacrifice(c)">
|
||||
SACRIFICE
|
||||
</button>
|
||||
<button @click.stop="lockCard(c)"
|
||||
:disabled="((props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0 && c !== props.player.turn.discard_marker_card) || props.player.turn.fuck_u_markers > 0">
|
||||
LOCK
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<template
|
||||
v-if="c && goStore.self?.turn && isPlayerEnemy && isSelfTurn && (c.health_as_champion ?? 0) > 0">
|
||||
<button class="enemy_action"
|
||||
:disabled="(goStore.self?.turn.damage_reserve ?? 0) < (c.health_as_champion ?? Infinity) || (check_for_guard(props.player) && !c.guard)"
|
||||
@click.stop="damage_champion(c)">
|
||||
DAMAGE ({{ c.health_as_champion }})
|
||||
</button>
|
||||
<button class="enemy_action"
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STUN) && e.usable)"
|
||||
@click.stop="stun(c)" :disabled="(check_for_guard(props.player) && !c.guard)">
|
||||
STUN
|
||||
</button>
|
||||
</template>
|
||||
</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
|
||||
}
|
||||
|
||||
.warning.visible {
|
||||
visibility: visible
|
||||
}
|
||||
|
||||
.warning-fire-gem {
|
||||
background-color: rgba(255, 165, 0, 0.7);
|
||||
}
|
||||
|
||||
#end_turn {
|
||||
z-index: 10;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stack,
|
||||
.discard,
|
||||
.cards_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
|
||||
.stack {
|
||||
grid-area: stack;
|
||||
margin-left: 75px;
|
||||
}
|
||||
|
||||
.discard {
|
||||
margin-top: 40px;
|
||||
grid-area: discard;
|
||||
justify-content: start;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
margin-left: 75px;
|
||||
}
|
||||
|
||||
.discard.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
.discard_overlay {
|
||||
z-index: 9;
|
||||
display: flex;
|
||||
/* background: blue; */
|
||||
pointer-events: none;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap-reverse;
|
||||
align-content: flex-end;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
padding-bottom: inherit;
|
||||
padding-left: 40px;
|
||||
height: calc(100% - 64px);
|
||||
padding-top: 64px;
|
||||
width: calc(100% -40px);
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#discard_unwrapped_bg {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 8;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
pointer-events: all;
|
||||
padding: inherit;
|
||||
}
|
||||
|
||||
#discard_unwrapped_bg.unwrapped {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.player {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-rows: min-content min-content 1fr;
|
||||
/* border-top: 5px solid black; */
|
||||
grid-template-areas:
|
||||
"player_stats player_stats"
|
||||
"stack player"
|
||||
"discard player";
|
||||
transition: all 1s;
|
||||
margin-bottom: -75px;
|
||||
padding-bottom: 75px;
|
||||
}
|
||||
|
||||
.discard_overlay {
|
||||
grid-template-areas:
|
||||
"player_stats player_stats"
|
||||
"stack player";
|
||||
}
|
||||
|
||||
#current_player_board {
|
||||
grid-area: player;
|
||||
}
|
||||
|
||||
.other_player_buttons_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0 5px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.damage_button:disabled {
|
||||
background-color: rgba(255, 77, 77, 0.5);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.damage_button:hover {
|
||||
background-color: #e60000;
|
||||
}
|
||||
|
||||
.damage_button:disabled:hover {
|
||||
background-color: rgba(230, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.damage_button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.all_damage_button {
|
||||
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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.make_discard_button:hover {
|
||||
background-color: #03883a;
|
||||
}
|
||||
|
||||
.make_discard_button:disabled:hover {
|
||||
background-color: rgba(3, 136, 58, 0.5);
|
||||
}
|
||||
|
||||
.make_discard_button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.turn_icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
#player_banner {
|
||||
display: flex;
|
||||
grid-area: player_stats;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding-top: 1em;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#player_banner.small {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
#player_banner>* {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
|
||||
#player_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#player_banner.big>#player_info {
|
||||
margin-left: 500px;
|
||||
}
|
||||
|
||||
#player_stats {
|
||||
display: flex;
|
||||
grid-area: player_stats;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#player_banner img {
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.turn_icon img {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.turn_icon_number {
|
||||
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);
|
||||
}
|
||||
|
||||
.end_turn_button: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 */
|
||||
}
|
||||
|
||||
.ready-bubble {
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
margin-bottom: 3%;
|
||||
}
|
||||
|
||||
.ready {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.not-ready {
|
||||
background-color: maroon;
|
||||
}
|
||||
|
||||
.suicide_effect {
|
||||
background: linear-gradient(to bottom right, #666666, #ccc);
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Player } from '~/netcode/Player';
|
||||
|
||||
|
||||
export interface Props {
|
||||
player: Player
|
||||
}
|
||||
|
||||
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">
|
||||
<img src="/images/gold.png" class="icon_img" draggable="false" />
|
||||
<span class="turn_icon_number">
|
||||
{{ player.turn?.gold_reserve }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="turn_icon">
|
||||
<img src="/images/damage.png" class="icon_img" draggable="false" />
|
||||
<span class="turn_icon_number">
|
||||
{{ player.turn?.damage_reserve }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="turn_icon">
|
||||
<span class="icon_img">🖕</span>
|
||||
<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;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
img.icon_img {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
span.icon_img {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.turn_icon_number {
|
||||
position: absolute;
|
||||
text-shadow: white 0 0 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,317 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import type { Card } from '~/netcode/Card';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="background">
|
||||
<div class="overlay-2">
|
||||
<div class="erika-zone-2">
|
||||
<img @click="router.push({ path: '/' })" 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>​</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>​</p>
|
||||
<hr>
|
||||
<p>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>​</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>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.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;
|
||||
;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
background-color: rgba(255, 255, 255, .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;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.overlay-2 {
|
||||
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;
|
||||
|
||||
.overlay-image {
|
||||
position: sticky;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,110 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import goStore, { useEffect, playCard, playAllCards, discard, lockCard, heal, damage_champion, damage_team, endTurn } from '~/netcode';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cards_container" id="self_hand">
|
||||
<CardPreview :card_id="c?.card_id" v-for=" c in goStore.self?.hand " @click="c && playCard(c)">
|
||||
<button v-if="c && goStore.self?.turn && (goStore.self.turn.fuck_u_markers ?? 0) > 0"
|
||||
@click.stop="discard(c)">DISCARD</button>
|
||||
</CardPreview>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cards_container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stack_and_discard {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
#play_all {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 10em;
|
||||
}
|
||||
|
||||
#turn_data {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: rgba(255, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
width: 60px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#end_turn {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#self_hand {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
left: 50%;
|
||||
bottom: -50px;
|
||||
max-width: 100vw;
|
||||
justify-content: flex-start;
|
||||
transition: 0.5s all cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
transform: translate(-50%, 150px)
|
||||
}
|
||||
|
||||
#self_hand:hover {
|
||||
transform: translate(-50%, 0px);
|
||||
}
|
||||
|
||||
#end_turn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
#self_hand .card {
|
||||
transition: 0.5s all cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#self_hand .card #contextMenuButtons {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
#self_hand .card.active:hover #contextMenuButtons {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
|
||||
#self_hand .card:hover {
|
||||
transform: translateY(-40px);
|
||||
|
||||
}
|
||||
|
||||
.cards_container {
|
||||
min-width: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,261 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
uuid?: string;
|
||||
cardId: number | undefined;
|
||||
locked?: boolean;
|
||||
wrapped?: boolean;
|
||||
tilted?: boolean;
|
||||
interactible?: boolean;
|
||||
noEffects?: boolean;
|
||||
used_effects_indexes?: number[];
|
||||
discardable?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
wrapped: () => false,
|
||||
tilted: () => true,
|
||||
locked: () => true,
|
||||
interactible: () => true,
|
||||
noEffects: () => false,
|
||||
discardable: () => false,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"click",
|
||||
"effectClick",
|
||||
"drop",
|
||||
"move",
|
||||
"startMove",
|
||||
"stopMove",
|
||||
]);
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const childCount = ref<number>(0);
|
||||
|
||||
const extension = computed(() => {
|
||||
if (settingsStore.cardStyle == CardStyles.SVG) {
|
||||
return ".svg";
|
||||
}
|
||||
return ".png";
|
||||
});
|
||||
|
||||
function cardClick() {
|
||||
if (!props.cardId) return;
|
||||
emit("click");
|
||||
}
|
||||
|
||||
const isZooming = ref<boolean>(false);
|
||||
|
||||
function mouseenter(e: MouseEvent) {
|
||||
// User is Right Clicking
|
||||
if ((e.buttons & 2) == 2) {
|
||||
isZooming.value = true;
|
||||
}
|
||||
|
||||
// goStore.hoveredCard = props.card_id
|
||||
}
|
||||
function mouseleave(e: MouseEvent) {
|
||||
isZooming.value = false;
|
||||
}
|
||||
|
||||
function rightClickDown(e: MouseEvent) {
|
||||
isZooming.value = true;
|
||||
}
|
||||
|
||||
function rightClickUp(e: MouseEvent) {
|
||||
isZooming.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="cardHTMLElement"
|
||||
class="card"
|
||||
:class="{
|
||||
active: childCount > 0,
|
||||
wrapped,
|
||||
unlocked: !props.locked,
|
||||
zoom: isZooming,
|
||||
no_effects: noEffects || !props.locked,
|
||||
discardable,
|
||||
}"
|
||||
@contextmenu.prevent="false"
|
||||
@mousedown.right="rightClickDown"
|
||||
@mouseup.right="rightClickUp"
|
||||
@click.stop="cardClick"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseleave"
|
||||
>
|
||||
<template v-if="props.cardId">
|
||||
<GameCardAtom
|
||||
v-if="settingsStore.cardStyle == CardStyles.SVG"
|
||||
:cardId="props.cardId"
|
||||
:used_effects_indexes="used_effects_indexes"
|
||||
@effectClick="emit('effectClick', $event)"
|
||||
></GameCardAtom>
|
||||
<div id="overlay">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<img
|
||||
:class="`card_image`"
|
||||
v-else
|
||||
:src="'/cards/background.png'"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* .card.dragging {
|
||||
transform: translateX(v-bind("translateX + 'px'"))
|
||||
translateY(v-bind("translateY + 'px'"));
|
||||
cursor: grabbing;
|
||||
z-index: 999;
|
||||
} */
|
||||
|
||||
/* .card.dragging > svg {
|
||||
transform: scale(110%);
|
||||
} */
|
||||
|
||||
.card.active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card_image {
|
||||
max-height: var(--max-height);
|
||||
z-index: 1;
|
||||
height: var(--max-height);
|
||||
}
|
||||
|
||||
.card.wrapped {
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card.wrapped .card_image {
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px gray)
|
||||
drop-shadow(-0.5px -0.5px 0.5px gray);
|
||||
}
|
||||
|
||||
.card.zoom {
|
||||
transform: scale(200%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#contextMenuButtons {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
word-break: break-word;
|
||||
bottom: 0;
|
||||
width: 99%;
|
||||
transform: translateY(-0px);
|
||||
transition: 0.05s transform var(--animation-curve);
|
||||
}
|
||||
|
||||
.card.active:hover #contextMenuButtons {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.card.no_effects svg > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#contextMenuButtons > * {
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
@keyframes gelatine {
|
||||
from,
|
||||
to {
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1, 1.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.5, 1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.1, 1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.svg_resource_icon {
|
||||
cursor: pointer;
|
||||
animation: gelatine 0.3s normal;
|
||||
transition: all 1s var(--animation-curve);
|
||||
}
|
||||
|
||||
.svg_resource_icon:active {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.svg_resource_icon:hover {
|
||||
transform: scale(130%);
|
||||
}
|
||||
|
||||
.used {
|
||||
opacity: 0.2;
|
||||
filter: grayscale(0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.svg_effect text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
--max-height: 250px;
|
||||
--max-width: 179px;
|
||||
--margin-bottom: calc(var(--max-height) * 0.05);
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 0px;
|
||||
margin-top: var(--margin-bottom);
|
||||
transition: all 0.15s var(--animation-curve);
|
||||
pointer-events: all;
|
||||
max-height: var(--max-height);
|
||||
max-width: var(--max-width);
|
||||
/* cursor: grab; */
|
||||
}
|
||||
|
||||
.card:not(svg) {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
width: calc(100% - 8%);
|
||||
height: 49%;
|
||||
}
|
||||
|
||||
.card.unlocked {
|
||||
margin-top: 0px;
|
||||
margin-bottom: var(--margin-bottom);
|
||||
transform: translateY(calc(var(--margin-bottom) * -0.25));
|
||||
filter: drop-shadow(
|
||||
calc(var(--margin-bottom) * 0.3) var(--margin-bottom) 1px rgba(0, 0, 0, 0.6)
|
||||
);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card.discardable {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.card.discardable svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { buyCard, buyGem, useToken } from "~/netcode";
|
||||
import {
|
||||
CardEffects,
|
||||
type Card,
|
||||
type Container,
|
||||
type Effect,
|
||||
type Game,
|
||||
} from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
game: Game;
|
||||
market: Container;
|
||||
marketStack: Container;
|
||||
fire_gems: Container;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// const selfPlayer = computed(() => authStore.logged ? playerList.value?.find(p => p.user.userId == authStore.selfUser.userId) : undefined)
|
||||
|
||||
// const isTurn = computed(() => goStore.self && goStore.current_game?.started && (goStore.self.team == goStore.current_game.current_turn))
|
||||
// const clientStore = useClientSideStore()
|
||||
|
||||
// const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
|
||||
|
||||
function marketCardClick(e: string) {
|
||||
buyCard(props.game._id, e);
|
||||
}
|
||||
|
||||
function fireGemCardClick(e: string) {
|
||||
buyGem(props.game._id);
|
||||
}
|
||||
|
||||
const marketTokens = computed(() =>
|
||||
props.game.currentTurn.tokens.filter((t) =>
|
||||
[
|
||||
CardEffects.STACK_NEXT_CARD_BOUGHT,
|
||||
CardEffects.STACK_NEXT_ACTION_BOUGHT,
|
||||
CardEffects.PLAY_NEXT_CARD_BOUGHT,
|
||||
].includes(t.effect)
|
||||
)
|
||||
);
|
||||
|
||||
const gemsTokens = computed(() =>
|
||||
props.game.currentTurn.tokens.filter((t) =>
|
||||
[
|
||||
CardEffects.STACK_NEXT_CARD_BOUGHT,
|
||||
CardEffects.PLAY_NEXT_CARD_BOUGHT,
|
||||
].includes(t.effect)
|
||||
)
|
||||
);
|
||||
|
||||
function tokenClick({ card, token }: { card: Card; token: Effect }) {
|
||||
useToken(props.game._id, token._id, card._id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="market">
|
||||
<GameCardGrouped
|
||||
:container="market"
|
||||
@cardClick="marketCardClick"
|
||||
:noEffects="true"
|
||||
:tokens="marketTokens"
|
||||
@tokenClick="tokenClick"
|
||||
>
|
||||
</GameCardGrouped>
|
||||
<div class="separator"></div>
|
||||
<GameCardGrouped
|
||||
:container="fire_gems"
|
||||
:wrapped="true"
|
||||
@cardClick="fireGemCardClick"
|
||||
:noEffects="true"
|
||||
:tokens="gemsTokens"
|
||||
@tokenClick="tokenClick"
|
||||
></GameCardGrouped>
|
||||
<div class="separator"></div>
|
||||
<GameCardGrouped
|
||||
:container="marketStack"
|
||||
:wrapped="true"
|
||||
:hidden="true"
|
||||
></GameCardGrouped>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.market {
|
||||
width: max-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;
|
||||
isolation: initial;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.market .card.zoom {
|
||||
transform: scale(200%) translateY(15%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { Effect } from "~/netcode/interfaces";
|
||||
|
||||
type Props = {
|
||||
token: Effect;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits(["useToken"]);
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
emit("useToken", props.token);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click.stop="onClick" @mousedown.left.stop="" id="token">
|
||||
{{ props.token.effect }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#token {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: white;
|
||||
border: 1px solid white;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: 0.2s all;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
#token:hover {
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { cards } from "~/services/loadCards";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
cardId: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
used_effects_indexes: {
|
||||
default: [],
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { cards };
|
||||
},
|
||||
beforeCreate() {
|
||||
(this as any).component = () => cards[this.cardId];
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component
|
||||
:is="cards[cardId]"
|
||||
:cardId="cardId"
|
||||
:used_effects_indexes="used_effects_indexes"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import type { Card } from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
card: Card;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const active = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="champion_small">
|
||||
<svg
|
||||
height="50"
|
||||
width="50"
|
||||
viewBox="-25 -25 50 50"
|
||||
class="champion_small_svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="guard">
|
||||
<path
|
||||
d="M 25.03887,48.82869 1.1094102,30.52586 10.249651,0.91124843 H 39.828094 L 48.968331,30.52586 Z"
|
||||
fill="black"
|
||||
stroke="lightgrey"
|
||||
stroke-width="2"
|
||||
></path>
|
||||
</clipPath>
|
||||
<clipPath id="none">
|
||||
<path
|
||||
d="M 0,246.85146 1.1094102,150 0,-11.33365 187.4845,-14.630353 185.82502,247.01727 Z"
|
||||
fill="black"
|
||||
stroke="lightgrey"
|
||||
stroke-width="2"
|
||||
></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<foreignObject
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="champion_small_foreignObject"
|
||||
:class="{ active }"
|
||||
>
|
||||
<div class="champion_small_inside">
|
||||
<GameCardAtom
|
||||
@mousedown.right="active = true"
|
||||
@mouseup.right="active = false"
|
||||
@mouseleave="active = false"
|
||||
width="75"
|
||||
height="75"
|
||||
:card-id="card.cardId"
|
||||
></GameCardAtom>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<circle
|
||||
cx="2"
|
||||
cy="-1"
|
||||
r="1"
|
||||
:fill="card.guard ? 'black' : 'lightgray'"
|
||||
stroke="white"
|
||||
stroke-width="0.2"
|
||||
class="champion_small_defense"
|
||||
:class="{ active }"
|
||||
></circle>
|
||||
<text
|
||||
text-anchor="middle"
|
||||
font-family="Helvetica"
|
||||
font-weight="bold"
|
||||
class="champion_small_defense"
|
||||
font-size="1.5"
|
||||
:class="{ active }"
|
||||
x="2"
|
||||
y="-.5"
|
||||
:fill="card.guard ? 'white' : 'black'"
|
||||
>
|
||||
{{ card.defense }}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.champion_small {
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.champion_small_svg {
|
||||
transform: scale(1500%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.champion_small_foreignObject {
|
||||
clip-path: polygon(
|
||||
58% 62%,
|
||||
67% 56%,
|
||||
59% 35.8%,
|
||||
40% 35.8%,
|
||||
31% 55%,
|
||||
41% 62%,
|
||||
49% 67.77%
|
||||
);
|
||||
transform: scale(30%) translate(-50%, -50%);
|
||||
transition: 0.15s all var(--animation-curve);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.champion_small_defense {
|
||||
transition: 0.15s all var(--animation-curve);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.champion_small_defense.active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.champion_small_foreignObject.active {
|
||||
clip-path: polygon(100% 100%, 100% 50%, 100% 0, 0 0, 0 50%, 0 100%, 49% 100%);
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.champion_small_foreignObject .champion_small_inside {
|
||||
pointer-events: all;
|
||||
transform: translate(0, -20%) scale(50%);
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.champion_small_foreignObject.active .champion_small_inside {
|
||||
z-index: 11;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,279 @@
|
||||
<script setup lang="ts">
|
||||
import { delay } from "~/netcode/events";
|
||||
import {
|
||||
CardEffects,
|
||||
CardType,
|
||||
type Card,
|
||||
type Container,
|
||||
type Effect,
|
||||
type PlayingTurn,
|
||||
} from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
container: Container;
|
||||
playingTurn?: PlayingTurn;
|
||||
wrapped?: boolean;
|
||||
hidden?: boolean;
|
||||
noEffects?: boolean;
|
||||
tokens?: Effect[];
|
||||
championsKillable?: boolean;
|
||||
discardable?: boolean;
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
wrapped: () => false,
|
||||
hidden: () => false,
|
||||
noEffects: () => false,
|
||||
tokens: () => [],
|
||||
championsKillable: () => false,
|
||||
discardable: () => false,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"cardClick",
|
||||
"effectClick",
|
||||
"tokenClick",
|
||||
"damageChampionClick",
|
||||
"discardCardClick",
|
||||
]);
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const cards = computed(() => {
|
||||
if (props.hidden) {
|
||||
return props.container.cards.map((_v, i) => ({
|
||||
cardId: undefined,
|
||||
})) as unknown as Card[];
|
||||
}
|
||||
return props.container.cards;
|
||||
});
|
||||
|
||||
const animationSpeed = computed(() => settingsStore.animationSpeed + "ms");
|
||||
const shuffleAnimationSpeed = computed(
|
||||
() => settingsStore.animationSpeed * 2 + "ms"
|
||||
);
|
||||
|
||||
function populateContainer() {}
|
||||
|
||||
// const rotate = ref(false);
|
||||
// async function shuffleContainer() {
|
||||
// rotate.value = true;
|
||||
// await delay(settingsStore.animationSpeed * 2);
|
||||
// rotate.value = false;
|
||||
// await delay(settingsStore.animationSpeed * 2);
|
||||
// }
|
||||
|
||||
function getCardsPosition(cards: string) {}
|
||||
|
||||
const animationCallbacks = ref<Array<() => void>>([]);
|
||||
|
||||
function startAnimation() {
|
||||
animationCallbacks.value.forEach((p) => p());
|
||||
animationCallbacks.value = [];
|
||||
}
|
||||
|
||||
function endAnimation() {
|
||||
animationCallbacks.value.forEach((p) => p());
|
||||
}
|
||||
|
||||
// watch(
|
||||
// props.container,
|
||||
// (newValue, oldValue) => {
|
||||
// if (oldValue) {
|
||||
// containerEvents.delete(oldValue._id);
|
||||
// }
|
||||
// if (newValue) {
|
||||
// containerEvents.set(newValue._id, {
|
||||
// populateContainer,
|
||||
// shuffleContainer,
|
||||
// getCardsPosition,
|
||||
// startAnimation,
|
||||
// endAnimation,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// { immediate: true }
|
||||
// );
|
||||
|
||||
// async function onEnter(_el: any, done: () => void) {
|
||||
// animationCallbacks.value.push(done);
|
||||
// }
|
||||
|
||||
// async function onLeave(_el: any, done: () => void) {
|
||||
// animationCallbacks.value.push(done);
|
||||
// }
|
||||
|
||||
onUnmounted(() => {
|
||||
// containerEvents.delete(props.container._id);
|
||||
});
|
||||
|
||||
const interval = ref<ReturnType<typeof setInterval>>();
|
||||
// function startMove() {
|
||||
// interval.value = setInterval(() => {
|
||||
// props.container.cards.sort((a, b) => Math.random() - 0.5);
|
||||
// }, 1000);
|
||||
// }
|
||||
// function stopMove() {
|
||||
// clearInterval(interval.value);
|
||||
// }
|
||||
|
||||
function tokenClick(token: Effect, card: Card) {
|
||||
emit("tokenClick", { token, card });
|
||||
}
|
||||
|
||||
function damageChampionClick(card: Card) {
|
||||
emit("damageChampionClick", card);
|
||||
}
|
||||
|
||||
function discardCardClick(card: Card) {
|
||||
emit("discardCardClick", card);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="cards_container"
|
||||
:class="{ wrapped, rotate: container.shuffling }"
|
||||
>
|
||||
<TransitionGroup name="list">
|
||||
<GameCard
|
||||
@click="$emit('cardClick', c._id)"
|
||||
@effectClick="$emit('effectClick', c._id, $event)"
|
||||
v-for="(c, i) in cards"
|
||||
:cardId="c?.cardId"
|
||||
:uuid="c._id"
|
||||
:wrapped="wrapped"
|
||||
:key="c?._id || i"
|
||||
:locked="
|
||||
locked ||
|
||||
!playingTurn ||
|
||||
playingTurn?.cardsLocked.includes(c._id.toString())
|
||||
"
|
||||
:used_effects_indexes="c?.effects?.reduce( (prev,e, i)=>{
|
||||
if(playingTurn?.lockedEffects.includes(e._id)){
|
||||
prev.push(i)
|
||||
}
|
||||
return prev
|
||||
},[] as number[])"
|
||||
:noEffects="noEffects"
|
||||
:discardable="discardable"
|
||||
>
|
||||
<template v-for="t in tokens">
|
||||
<GameToken
|
||||
:token="t"
|
||||
@useToken="tokenClick(t, c)"
|
||||
v-if="
|
||||
(t.effect == CardEffects.SACRIFICE &&
|
||||
playingTurn &&
|
||||
!playingTurn?.cardsLocked.includes(c._id.toString())) ||
|
||||
(t.effect == CardEffects.PREPARE &&
|
||||
c.cardType == CardType.CHAMPION) ||
|
||||
(t.effect == CardEffects.STACK_NEXT_ACTION_BOUGHT &&
|
||||
c.cardType == CardType.ACTION) ||
|
||||
(t.effect == CardEffects.STUN &&
|
||||
c.cardType == CardType.CHAMPION &&
|
||||
championsKillable) ||
|
||||
t.effect == CardEffects.PLAY_NEXT_CARD_BOUGHT ||
|
||||
t.effect == CardEffects.STACK_NEXT_CARD_BOUGHT ||
|
||||
t.effect == CardEffects.RESTACK_DISCARDED_CARD ||
|
||||
(t.effect == CardEffects.RESTACK_DISCARDED_ACTION &&
|
||||
c.cardType == CardType.ACTION) ||
|
||||
(t.effect == CardEffects.RESTACK_DISCARDED_CHAMPION &&
|
||||
c.cardType == CardType.CHAMPION)
|
||||
"
|
||||
></GameToken>
|
||||
</template>
|
||||
<button
|
||||
v-if="championsKillable && c.cardType == CardType.CHAMPION"
|
||||
@click.stop="damageChampionClick(c)"
|
||||
@mousedown.left.stop=""
|
||||
>
|
||||
Damage
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
discardable && !playingTurn?.cardsLocked.includes(c._id.toString())
|
||||
"
|
||||
@mousedown.left.stop=""
|
||||
@click.stop="discardCardClick(c)"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</GameCard>
|
||||
</TransitionGroup>
|
||||
<!-- <img :class="`card_image`" :src="'/cards/background.png'" draggable="false"> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all v-bind("animationSpeed") ease;
|
||||
}
|
||||
|
||||
/* .list-leave-to, */
|
||||
.list-enter-from {
|
||||
opacity: 0;
|
||||
/* transform: translateX(30px); */
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
rotate: 360deg;
|
||||
}
|
||||
|
||||
.cards_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
gap: 30px;
|
||||
min-width: 200px;
|
||||
flex-wrap: wrap;
|
||||
transition: rotate v-bind("shuffleAnimationSpeed") ease;
|
||||
}
|
||||
|
||||
.cards_container.wrapped {
|
||||
flex-direction: column-reverse;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.cards_container.wrapped:before {
|
||||
height: calc(250px - 10px);
|
||||
display: block;
|
||||
content: "";
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
max-height: 10px;
|
||||
}
|
||||
|
||||
.empty > .card_image {
|
||||
filter: brightness(0.5);
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
use * use[data-effect-id="0"] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cards_container .card #background rect:first-child {
|
||||
stroke-width: 0;
|
||||
}
|
||||
.cards_container .card.wrapped #background rect:first-child {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.cards_container .card.card.wrapped:nth-child(5n) #background rect:first-child {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { initCustomFormatter } from "vue";
|
||||
import { buyCard, buyGem, useToken } from "~/netcode";
|
||||
import {
|
||||
CardEffects,
|
||||
type Card,
|
||||
type Container,
|
||||
type Effect,
|
||||
type Game,
|
||||
type Player,
|
||||
} from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
game: Game;
|
||||
player: Player;
|
||||
}
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const isVisible = ref(false);
|
||||
|
||||
const discardTokens = computed(() =>
|
||||
props.game.currentTurn.tokens.filter((t) =>
|
||||
[
|
||||
CardEffects.RESTACK_DISCARDED_ACTION,
|
||||
CardEffects.RESTACK_DISCARDED_CARD,
|
||||
CardEffects.RESTACK_DISCARDED_CHAMPION,
|
||||
CardEffects.SACRIFICE,
|
||||
].includes(t.effect)
|
||||
)
|
||||
);
|
||||
|
||||
function tokenClick({ card, token }: { card: Card; token: Effect }) {
|
||||
useToken(props.game._id, token._id, card._id);
|
||||
}
|
||||
|
||||
function openDiscard() {
|
||||
isVisible.value = true;
|
||||
if (!settingsStore.saveDiscardSize) {
|
||||
settingsStore.discardFullscreen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="discard">
|
||||
<GameCardGrouped
|
||||
@cardClick="openDiscard"
|
||||
:container="player.discard"
|
||||
:wrapped="true"
|
||||
:no-effects="true"
|
||||
>
|
||||
</GameCardGrouped>
|
||||
</div>
|
||||
<div
|
||||
class="discard-animations discard-background"
|
||||
:class="{ visible: isVisible, fullscreen: settingsStore.discardFullscreen }"
|
||||
@click.stop="isVisible = false"
|
||||
></div>
|
||||
<div
|
||||
class="discard-animations discard-menu"
|
||||
:class="{ visible: isVisible, fullscreen: settingsStore.discardFullscreen }"
|
||||
@click.stop="isVisible = false"
|
||||
>
|
||||
<span>Discard of {{ player.user.username }}</span>
|
||||
<button
|
||||
class="discard-fullscreen-btn"
|
||||
@click.stop="
|
||||
settingsStore.discardFullscreen = !settingsStore.discardFullscreen
|
||||
"
|
||||
>
|
||||
TOGGLE FULLSCREEN
|
||||
</button>
|
||||
<GameCardGrouped
|
||||
:container="player.discard"
|
||||
:wrapped="false"
|
||||
:hidden="false"
|
||||
:tokens="discardTokens"
|
||||
:noEffects="true"
|
||||
:playingTurn="game.currentTurn"
|
||||
:locked="true"
|
||||
@tokenClick="tokenClick"
|
||||
></GameCardGrouped>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
background: white;
|
||||
}
|
||||
.discard-fullscreen-btn {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.discard-animations {
|
||||
transition: 0.3s all var(--animation-curve);
|
||||
}
|
||||
.discard-background {
|
||||
transition: 0.3s all linear;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
pointer-events: none;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.discard-background.visible {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
pointer-events: all;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.discard-menu {
|
||||
left: -75%;
|
||||
top: 0;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
padding-left: 50%;
|
||||
background: rgb(100, 53, 44);
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.discard-menu.visible {
|
||||
left: -50%;
|
||||
}
|
||||
|
||||
.discard-menu.fullscreen {
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.discard-menu.visible.fullscreen {
|
||||
left: 0%;
|
||||
}
|
||||
|
||||
.discard-menu > .cards_container {
|
||||
pointer-events: all;
|
||||
overflow: auto;
|
||||
padding: 30px;
|
||||
max-width: 100%;
|
||||
transition: 0.3s all var(--animation-curve);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.discard-menu.fullscreen > .cards_container {
|
||||
max-width: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="loading_screen">Loading...</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#loading_screen {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { Player } from '~/netcode/Player';
|
||||
|
||||
import type { Player } from "~/netcode/interfaces";
|
||||
|
||||
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>
|
||||
@@ -29,22 +27,24 @@ const close = () => {
|
||||
<h3 v-else>Défaite!</h3>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<PlayerIcon :player="players[0]"></PlayerIcon>
|
||||
<p v-if="won">
|
||||
Vous avez gagné!
|
||||
</p>
|
||||
<GamePlayerIcon :player="players[0]"></GamePlayerIcon>
|
||||
<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!
|
||||
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>
|
||||
@@ -66,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;
|
||||
@@ -82,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;
|
||||
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const isVisible = ref(false);
|
||||
|
||||
const _devicePixelRatio = computed(() => devicePixelRatio);
|
||||
const _window = computed(() => window);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="isVisible = true" class="dialog-open-button">
|
||||
Open Settings
|
||||
</button>
|
||||
<div
|
||||
:class="{ visible: isVisible }"
|
||||
class="dialog dialog-background"
|
||||
@click="isVisible = false"
|
||||
></div>
|
||||
<div class="dialog dialog-box" :class="{ visible: isVisible }" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h3>Game</h3>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<span>Save discard size</span>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<input type="checkbox" v-model="settingsStore.saveDiscardSize" />
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<template v-if="!settingsStore.saveDiscardSize"
|
||||
>Discard will always open small
|
||||
</template>
|
||||
<template v-else="!settingsStore.saveDiscardSize"
|
||||
>Discard will remember its size
|
||||
</template>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<h3>Visuals</h3>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<span>Toggle Card Style</span>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<button @click="settingsStore.switchCardStyles">Toggle Card style</button>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<span>{{ settingsStore.cardStyle }}</span>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<span>Animation Speed</span>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="250"
|
||||
step="25"
|
||||
v-model="settingsStore.animationSpeed"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<span>{{ settingsStore.animationSpeed }}ms</span>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<span>Fast mode</span>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<input type="checkbox" v-model="settingsStore.fastAnimations" />
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<h3>Graphics</h3>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<span>Shader Quality</span>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
v-model="settingsStore.shaderQuality"
|
||||
:disabled="settingsStore.shaderFramerate == 0"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<span v-if="_window"
|
||||
>{{
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<span v-if="settingsStore.shaderFramerate == 65">vsync</span>
|
||||
<span v-else>{{ settingsStore.shaderFramerate }} fps</span>
|
||||
</div>
|
||||
<div class="dialog-header">
|
||||
<span>Pixelated</span>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<input type="checkbox" v-model="settingsStore.shaderPixelated" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
transition: 0.3s all var(--animation-curve);
|
||||
}
|
||||
.dialog-open-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dialog-open-button:active {
|
||||
filter: drop-shadow(0px 0px 1px);
|
||||
}
|
||||
|
||||
.dialog-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
transition: 0.3s all linear;
|
||||
}
|
||||
|
||||
.dialog-background.visible {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 100%);
|
||||
padding: 0 20px;
|
||||
border-radius: 5px;
|
||||
width: 800px;
|
||||
border-radius: 30px;
|
||||
max-width: 100%;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
display: grid;
|
||||
grid-template-columns: 33% 33% 33%;
|
||||
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
|
||||
background: rgb(38, 62, 15);
|
||||
}
|
||||
|
||||
.dialog-box.visible {
|
||||
bottom: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
.dialog-box h3 {
|
||||
font-size: 30px;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
color: white;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.dialog-header,
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 10px 0;
|
||||
grid-column-start: 2;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
grid-column-start: 3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
user: User;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player-icon-container">
|
||||
<img
|
||||
class="avatar"
|
||||
v-if="props.user.avatar"
|
||||
:src="`https://cdn.discordapp.com/avatars/${props.user.userId}/${props.user.avatar}.webp`"
|
||||
draggable="false"
|
||||
/>
|
||||
<b class="player-name">{{ props.user.username }}</b>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.player-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 20px;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin-left: 10px;
|
||||
|
||||
color: white;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { damagePlayer, makeDiscard } from "~/netcode";
|
||||
import {
|
||||
CardEffects,
|
||||
CardType,
|
||||
type Game,
|
||||
type Player,
|
||||
} from "~/netcode/interfaces";
|
||||
|
||||
const emit = defineEmits(["click"]);
|
||||
|
||||
export interface Props {
|
||||
game: Game;
|
||||
player: Player;
|
||||
focused?: boolean;
|
||||
isSelfTurn: boolean;
|
||||
self?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
self: () => false,
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
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
|
||||
);
|
||||
|
||||
function makeDiscardClick() {
|
||||
makeDiscard(props.game._id, props.player._id);
|
||||
}
|
||||
function damage() {
|
||||
damagePlayer(props.game._id, props.player._id, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="player_list_element"
|
||||
@click="$emit('click')"
|
||||
:class="{
|
||||
self,
|
||||
focused,
|
||||
isSelfTurn,
|
||||
}"
|
||||
>
|
||||
<div id="player_banner" class="small">
|
||||
<!-- <div
|
||||
class="ready-bubble not-ready"
|
||||
v-if="!props.player.ready && !goStore.current_game?.started"
|
||||
>
|
||||
Pas encore prêt...
|
||||
</div>
|
||||
<div
|
||||
class="ready-bubble ready"
|
||||
v-if="props.player.ready && !goStore.current_game?.started"
|
||||
>
|
||||
Prêt!
|
||||
</div> -->
|
||||
<div id="player_info">
|
||||
<GamePlayerIcon :user="props.player.user"></GamePlayerIcon>
|
||||
</div>
|
||||
<div id="player_stats">
|
||||
<!-- <template v-if="goStore.current_game?.started"> -->
|
||||
<GamePlayerStats :game="props.game" :player="props.player">
|
||||
</GamePlayerStats>
|
||||
<!-- </template> -->
|
||||
</div>
|
||||
<template v-if="!self && isSelfTurn">
|
||||
<button
|
||||
v-if="
|
||||
game.currentTurn.tokens.some(
|
||||
(t) => t.effect == CardEffects.MAKE_DISCARD
|
||||
)
|
||||
"
|
||||
@click.stop="makeDiscardClick"
|
||||
>
|
||||
Make Discard
|
||||
</button>
|
||||
<button @click.stop="damage" :disabled="game.currentTurn.damage == 0">
|
||||
Damage
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="champion_zone">
|
||||
<GameCardChampionSmall
|
||||
v-for="c in player.board.cards.filter(
|
||||
(c) => c.cardType == CardType.CHAMPION
|
||||
)"
|
||||
:card="c"
|
||||
:key="c._id"
|
||||
></GameCardChampionSmall>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.champion_zone {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.player_list_element {
|
||||
border-radius: 10px;
|
||||
margin: 10px;
|
||||
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
|
||||
transform: translate(-2.5px, -7.5px);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.1s;
|
||||
min-width: 300px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.player_list_element::before {
|
||||
border-radius: 10px;
|
||||
background: rgb(15, 179, 255);
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.player_list_element.focused {
|
||||
filter: unset;
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
.player_list_element.isTurn::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.player_list_element.self::before {
|
||||
background: rgb(33, 177, 76);
|
||||
}
|
||||
|
||||
#player_banner {
|
||||
display: flex;
|
||||
grid-area: player_stats;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#player_banner.small {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player_banner > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#player_stats {
|
||||
display: flex;
|
||||
grid-area: player_stats;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ready-bubble {
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
.ready {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.not-ready {
|
||||
background-color: maroon;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import type { Container } from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
container: Container;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cards_container" id="self_hand">
|
||||
<!-- <CardPreview :card_id="c.cardId" v-for="c in container.cards "> -->
|
||||
<!-- <button v-if="c && goStore.self?.turn && (goStore.self.turn.fuck_u_markers ?? 0) > 0"
|
||||
@click.stop="discard(c)">DISCARD</button> -->
|
||||
<!-- </CardPreview> -->
|
||||
<GameCardGrouped :container="container" :wrapped="false"> </GameCardGrouped>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cards_container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stack_and_discard {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#play_all {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 10em;
|
||||
}
|
||||
|
||||
#turn_data {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: rgba(255, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
width: 60px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#end_turn {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#self_hand {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
left: 50%;
|
||||
bottom: -50px;
|
||||
max-width: 100vw;
|
||||
justify-content: flex-start;
|
||||
transition: 0.25s all var(--animation-curve);
|
||||
transform: translate(-50%, 150px);
|
||||
}
|
||||
|
||||
#self_hand:hover {
|
||||
transform: translate(-50%, 0px);
|
||||
}
|
||||
|
||||
#end_turn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
#self_hand > div.card {
|
||||
transition: 0.25s all var(--animation-curve);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#self_hand > .cards_container {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
#self_hand > div.card #contextMenuButtons {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
#self_hand div.card.active:hover #contextMenuButtons {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
#self_hand div.card:hover {
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
|
||||
#self_hand div.card.zoom:hover {
|
||||
transform: translateY(calc(-60px - 55%)) scale(200%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,468 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
damageChampion,
|
||||
discardCard,
|
||||
endTurn,
|
||||
joinGame,
|
||||
lockCard,
|
||||
lockEffect,
|
||||
startGame,
|
||||
useToken,
|
||||
} from "~/netcode";
|
||||
import {
|
||||
CardEffects,
|
||||
type Card,
|
||||
type Effect,
|
||||
type Game,
|
||||
type Player,
|
||||
} from "~/netcode/interfaces";
|
||||
import { useTurnStore } from "~/stores/turnStore";
|
||||
|
||||
export interface Props {
|
||||
player: Player;
|
||||
game: Game;
|
||||
hide_stack_and_discard?: boolean;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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 isSelf = computed(
|
||||
() => props.player.user.userId === authStore.selfUser.userId
|
||||
);
|
||||
const isSelfTurn = computed(() => isSelf && isPlayerTurn);
|
||||
|
||||
const cardTokens = computed(() =>
|
||||
props.game.currentTurn.tokens.filter(
|
||||
(t) =>
|
||||
[CardEffects.PREPARE, CardEffects.STUN].includes(t.effect) ||
|
||||
(CardEffects.SACRIFICE == t.effect && isSelf)
|
||||
)
|
||||
);
|
||||
|
||||
const warningText = ref("Effets restants!");
|
||||
|
||||
// const make_discard = (target: Player) =>
|
||||
// clientStore.findUseEffect(CardEffects.MAKE_DISCARD, target);
|
||||
// const sacrifice = (target: CardParser) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
|
||||
// const stun = (target: CardParser) => clientStore.findUseEffect(CardEffects.STUN, target)
|
||||
// const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
|
||||
// const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
|
||||
// const prepare_champion = (target: CardParser) => clientStore.findUseEffect(CardEffects.PREPARE, target)
|
||||
|
||||
let warning = ref(false);
|
||||
|
||||
// function has_actions_remaining(): boolean {
|
||||
// return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false)
|
||||
// || (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= (goStore.current_game?.market.reduce((min, card) => Math.min(min, card?.cost ?? Infinity), Infinity) ?? Infinity) : false)
|
||||
// || (goStore.self?.turn?.fuck_u_markers ? goStore.self?.turn?.fuck_u_markers > 0 : false)
|
||||
// || !goStore.self?.turn?.effects_availables.every(e => !e.usable || e.effect_type == EffectType.SUICIDE)
|
||||
// || !goStore.self?.board.every(c => (c.uuid in (goStore.self?.turn?.cards_locked ?? {})))
|
||||
// || false
|
||||
// }
|
||||
|
||||
// function can_buy_fire_gem(): boolean {
|
||||
// return (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= 2 : 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()
|
||||
// }
|
||||
|
||||
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 effectClick(cardUUID: string, effectIndex: number) {
|
||||
const card = props.player.board.cards.find((c) => c._id == cardUUID);
|
||||
if (!card) {
|
||||
console.error("card not found");
|
||||
return;
|
||||
}
|
||||
if (!props.game.currentTurn.cardsLocked.includes(cardUUID)) {
|
||||
return;
|
||||
}
|
||||
const effectUUID = card.effects[effectIndex]._id;
|
||||
lockEffect(props.game._id, effectUUID);
|
||||
}
|
||||
// function damage_team_all(team: Team) {
|
||||
// for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) {
|
||||
// damage_team(team)
|
||||
// }
|
||||
// }
|
||||
|
||||
// function check_for_guard(player: Player) {
|
||||
// return player.board.some(c => c.guard === true)
|
||||
// }
|
||||
|
||||
function tokenClick({ card, token }: { card: Card; token: Effect }) {
|
||||
useToken(props.game._id, token._id, card._id, props.player._id);
|
||||
}
|
||||
function damageChampionClick(card: Card) {
|
||||
damageChampion(props.game._id, props.player._id, card._id);
|
||||
}
|
||||
function discardCardClick(card: Card) {
|
||||
discardCard(props.game._id, card._id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player" :class="{ turn: isPlayerTurn, self: isSelf }">
|
||||
<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)">
|
||||
Damage
|
||||
</button>
|
||||
<button @click.stop="damage_team_all(props.player.team)"
|
||||
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team"
|
||||
class="damage_button all_damage_button"
|
||||
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
|
||||
(All)
|
||||
</button>
|
||||
<button v-if="goStore.self && isSelfTurn && isPlayerEnemy && goStore.self?.turn"
|
||||
@click.stop="make_discard(props.player)" class="make_discard_button"
|
||||
:disabled="!clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.MAKE_DISCARD) && e.usable)">
|
||||
make Discard
|
||||
</button>
|
||||
</div> -->
|
||||
<!-- <br /> -->
|
||||
|
||||
<GameCardGrouped
|
||||
:container="player.board"
|
||||
:wrapped="false"
|
||||
:playingTurn="props.game.currentTurn"
|
||||
@cardClick="boardCardClick"
|
||||
@effectClick="effectClick"
|
||||
:tokens="cardTokens"
|
||||
@tokenClick="tokenClick"
|
||||
:championsKillable="!isSelf && isSelfTurn.value"
|
||||
:discardable="
|
||||
(player.fuckUMarkers > 0 || player.discardMarkers > 0) && isSelf
|
||||
"
|
||||
@damageChampionClick="damageChampionClick"
|
||||
@discardCardClick="discardCardClick"
|
||||
>
|
||||
</GameCardGrouped>
|
||||
<br />
|
||||
<!-- <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"
|
||||
@click.stop="discard(c)">
|
||||
DISCARD
|
||||
</button>
|
||||
</CardPreview>
|
||||
</template> -->
|
||||
|
||||
<!-- 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">
|
||||
// Card Locked
|
||||
<template v-if="c.uuid in props.player.turn.cards_locked">
|
||||
<template v-for="e in clientStore.maskedServerEffects">
|
||||
<template v-if="e.card == c">
|
||||
<button v-if="!clientStore.effectsHandled.has(e.effect)" @click.stop="useEffect(e)"
|
||||
:disabled="!e.usable"
|
||||
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
|
||||
EFFECT<br>
|
||||
{{ e.effect + " " + (e.effect_type ?? '') }}
|
||||
</button>
|
||||
<button v-if="clientStore.effectsHandled.has(e.effect)"
|
||||
@click.stop="clientStore.activateEffect(e)" :disabled="!e.usable"
|
||||
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
|
||||
EFFECT<br>
|
||||
{{ e.effect + " " + (e.effect_type ?? '') }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<button
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PREPARE) && e.usable) && c.card_type == CardType.CHAMPION"
|
||||
@click.stop="prepare_champion(c)">PREPARE</button>
|
||||
</template>
|
||||
// Card Unlocked
|
||||
<template v-if="!(c.uuid in props.player.turn.cards_locked)">
|
||||
<button v-if="(props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
|
||||
@click.stop="discard(c)">
|
||||
DISCARD
|
||||
</button>
|
||||
<button
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
|
||||
@click.stop="sacrifice(c)">
|
||||
SACRIFICE
|
||||
</button>
|
||||
<button @click.stop="lockCard(c)"
|
||||
:disabled="((props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0 && c !== props.player.turn.discard_marker_card) || props.player.turn.fuck_u_markers > 0">
|
||||
LOCK
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<template
|
||||
v-if="c && goStore.self?.turn && isPlayerEnemy && isSelfTurn && (c.health_as_champion ?? 0) > 0">
|
||||
<button class="enemy_action"
|
||||
:disabled="(goStore.self?.turn.damage_reserve ?? 0) < (c.health_as_champion ?? Infinity) || (check_for_guard(props.player) && !c.guard)"
|
||||
@click.stop="damage_champion(c)">
|
||||
DAMAGE ({{ c.health_as_champion }})
|
||||
</button>
|
||||
<button class="enemy_action"
|
||||
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STUN) && e.usable)"
|
||||
@click.stop="stun(c)" :disabled="(check_for_guard(props.player) && !c.guard)">
|
||||
STUN
|
||||
</button>
|
||||
</template> -->
|
||||
<!-- </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;
|
||||
}
|
||||
|
||||
.warning.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.warning-fire-gem {
|
||||
background-color: rgba(255, 165, 0, 0.7);
|
||||
}
|
||||
|
||||
#turn_buttons {
|
||||
grid-area: turn_buttons;
|
||||
}
|
||||
|
||||
#end_turn {
|
||||
z-index: 10;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#end_turn.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.player {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: min-content 1fr;
|
||||
/* border-top: 5px solid black; */
|
||||
grid-template-areas:
|
||||
"player_stats"
|
||||
"player"
|
||||
"turn_buttons";
|
||||
justify-items: center;
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
#current_player_board {
|
||||
grid-area: player;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.damage_button:disabled {
|
||||
background-color: rgba(255, 77, 77, 0.5);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.damage_button:hover {
|
||||
background-color: #e60000;
|
||||
}
|
||||
|
||||
.damage_button:disabled:hover {
|
||||
background-color: rgba(230, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.damage_button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.all_damage_button {
|
||||
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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.make_discard_button:hover {
|
||||
background-color: #03883a;
|
||||
}
|
||||
|
||||
.make_discard_button:disabled:hover {
|
||||
background-color: rgba(3, 136, 58, 0.5);
|
||||
}
|
||||
|
||||
.make_discard_button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.turn_icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#player_info {
|
||||
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, 0.6));
|
||||
backdrop-filter: blur(5px) brightness(80%) grayscale(80%);
|
||||
}
|
||||
|
||||
#player_banner img {
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.turn_icon img {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.turn_icon_number {
|
||||
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);
|
||||
}
|
||||
|
||||
.end_turn_button: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 */
|
||||
}
|
||||
|
||||
.suicide_effect {
|
||||
background: linear-gradient(to bottom right, #666666, #ccc);
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { Transition } from "vue";
|
||||
import { playSound } from "~/helpers/audioHelpers";
|
||||
import type { Game, Player } from "~/netcode/interfaces";
|
||||
|
||||
export interface Props {
|
||||
game: Game;
|
||||
player: Player;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const team = computed(() =>
|
||||
props.game.teams.find((t) =>
|
||||
t.players.map((p) => p._id).includes(props.player._id)
|
||||
)
|
||||
);
|
||||
|
||||
const isTurn = computed(
|
||||
() => team.value && team.value?._id == props.game.currentTurn.currentTeam
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="turn_icon">
|
||||
<img src="/img/champion-shield.png" class="icon_img" draggable="false" />
|
||||
<span class="turn_icon_number">
|
||||
{{ team?.health }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="turn_icon">
|
||||
<img src="/img/fuck.svg" class="icon_img" draggable="false" />
|
||||
<span class="turn_icon_number">{{
|
||||
(player.discardMarkers ?? 0) + (player.fuckUMarkers ?? 0)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="turn_icon" v-show="isTurn">
|
||||
<img src="/img/gold.png" class="icon_img" draggable="false" />
|
||||
<span class="turn_icon_number">
|
||||
{{ game.currentTurn.gold }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="turn_icon" v-show="isTurn">
|
||||
<img src="/img/damage.png" class="icon_img" draggable="false" />
|
||||
<span class="turn_icon_number">
|
||||
{{ game.currentTurn.damage }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.turn_icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img.icon_img {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
span.icon_img {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.turn_icon_number {
|
||||
position: absolute;
|
||||
text-shadow: white 0 0 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useAuthStore } from "#imports";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const authStore = useAuthStore();
|
||||
const config = useRuntimeConfig();
|
||||
// if (to.path == '/') {
|
||||
// const code = to.query.code
|
||||
// if (code) {
|
||||
// if (!Array.isArray(code)) {
|
||||
// try {
|
||||
// await oauth2_register_discord(code)
|
||||
// return navigateTo('/lobby')
|
||||
// } catch {
|
||||
// return navigateTo('/login')
|
||||
// }
|
||||
// } else {
|
||||
// return navigateTo('/login')
|
||||
// }
|
||||
// }
|
||||
// return navigateTo("/lobby")
|
||||
// }
|
||||
|
||||
// if (!goStore.discordId) {
|
||||
// const heron_session = useCookie('heron_session')
|
||||
// if (!heron_session.value) { return navigateTo('/login') }
|
||||
|
||||
// try {
|
||||
// await heron_cookie_login(heron_session.value)
|
||||
// if (to.path == '/') {
|
||||
// return navigateTo("/lobby")
|
||||
// }
|
||||
// } catch {
|
||||
// return navigateTo('/login')
|
||||
// }
|
||||
// }
|
||||
|
||||
if (to.path.startsWith("/game")) {
|
||||
return;
|
||||
}
|
||||
if (to.path == "/login") {
|
||||
return;
|
||||
}
|
||||
if (to.path == "/lobby") {
|
||||
return;
|
||||
}
|
||||
if (to.path == "/rules") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (to.path == "/test") {
|
||||
return;
|
||||
}
|
||||
// In a real app you would probably not redirect every route to `/`
|
||||
// however it is important to check `to.path` before redirecting or you
|
||||
// might get an infinite redirect loop
|
||||
if (to.path !== "/") {
|
||||
return navigateTo("/");
|
||||
}
|
||||
return navigateTo("/lobby");
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { GameObjectRegister } from "."
|
||||
import type { Effect } from "./Effect"
|
||||
import type { GameObjectNetcode } from "./GameObject"
|
||||
import type { Turn } from "./Turn"
|
||||
|
||||
enum CardRole {
|
||||
PERSONAL = "personal",
|
||||
MARKET = "market",
|
||||
FIRE_GEM = "fire_gem"
|
||||
}
|
||||
|
||||
export enum CardType {
|
||||
HERO = "hero",
|
||||
HERO_ABILITY = "hero_ability",
|
||||
CURRENCY = "currency",
|
||||
WEAPON = "weapon",
|
||||
CHAMPION = "champion",
|
||||
ACTION = "action",
|
||||
ITEM = "item"
|
||||
}
|
||||
|
||||
enum CardFaction {
|
||||
BASE = "base",
|
||||
|
||||
BLUE = "blue",
|
||||
RED = "red",
|
||||
GREEN = "green",
|
||||
YELLOW = "yellow"
|
||||
}
|
||||
|
||||
export interface CardNetcode extends GameObjectNetcode {
|
||||
card_id: number
|
||||
sprite: string
|
||||
name: string
|
||||
cost?: number
|
||||
role: CardRole
|
||||
card_type: CardType
|
||||
faction: CardFaction
|
||||
effects: Array<string>
|
||||
defense?: number
|
||||
guard?: boolean
|
||||
}
|
||||
|
||||
export type Card = Omit<CardNetcode, 'effects'> & {
|
||||
effects: Array<Effect>
|
||||
health_as_champion?: number
|
||||
}
|
||||
export function create_card(input: CardNetcode, register: GameObjectRegister) {
|
||||
const card = new Proxy<any>(input, {
|
||||
get(target: CardNetcode, prop, reciever) {
|
||||
if (prop == "effects") {
|
||||
return target.effects.map((e) => register.effects.get(e))
|
||||
}
|
||||
if (prop == "health_as_champion") {
|
||||
return Array.from(register.turns.values()).find(e => e.champions_health.has(card.uuid))?.champions_health.get(card.uuid)
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Card
|
||||
return card
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { GameObjectRegister } from "."
|
||||
import { CardType, type Card } from "./Card"
|
||||
import type { GameObjectNetcode } from "./GameObject"
|
||||
|
||||
export enum CardEffects {
|
||||
GOLD = "gold",
|
||||
DAMAGE = "damage",
|
||||
HEAL = "heal",
|
||||
PREPARE = "prepare",
|
||||
STUN = "stun",
|
||||
SACRIFICE = "sacrifice",
|
||||
DRAW = "draw",
|
||||
DRAW_AND_DISCARD = "draw_and_discard",
|
||||
MAKE_DISCARD = "make_discard",
|
||||
RESTACK_DISCARDED_CHAMPION = "restack_discarded_champion",
|
||||
RESTACK_DISCARDED_CARD = "restack_discarded_card",
|
||||
STACK_NEXT_ACTION_BOUGHT = "stack_next_action_bought",
|
||||
STACK_NEXT_CARD_BOUGHT = "stack_next_card_bought",
|
||||
PLAY_NEXT_CARD_BOUGHT = "play_next_card_bought"
|
||||
}
|
||||
|
||||
export enum EffectTimes {
|
||||
PER_CHAMPION = "per_champion",
|
||||
PER_OTHER_CHAMPION = "per_other_champion",
|
||||
PER_OTHER_GUARD = "per_other_guard",
|
||||
PER_CARD_OF_SAME_FACTION = "per_card_of_same_faction",
|
||||
PER_OTHER_CARD_OF_SAME_FACTION = "per_other_card_of_same_faction"
|
||||
}
|
||||
|
||||
export enum EffectType {
|
||||
CHAMPION_ACTION = "champion_action",
|
||||
SUICIDE = "suicide",
|
||||
FACTION_COMBO = "faction_combo"
|
||||
}
|
||||
|
||||
export interface EffectNetcode extends GameObjectNetcode {
|
||||
effect: CardEffects
|
||||
amount: number
|
||||
sub_effects: Array<EffectNetcode>
|
||||
mutex: Array<string>
|
||||
card: string
|
||||
effect_type?: EffectType
|
||||
times?: EffectTimes
|
||||
}
|
||||
|
||||
export type Effect = Omit<EffectNetcode, 'sub_effects' | 'mutex' | 'card'> & {
|
||||
sub_effects: { [key: string]: Effect }
|
||||
mutex: { [key: string]: Effect }
|
||||
card: Card
|
||||
usable: boolean
|
||||
}
|
||||
|
||||
export function create_effect(input: EffectNetcode, register: GameObjectRegister) {
|
||||
const effect =
|
||||
new Proxy<any>(input, {
|
||||
get(target: EffectNetcode, prop, reciever) {
|
||||
if (prop == "sub_effects") {
|
||||
return Object.fromEntries(Object.entries(register.effects).filter(([i, v]) => i in target.sub_effects))
|
||||
}
|
||||
if (prop == "mutex") {
|
||||
return Object.fromEntries(Object.entries(register.effects).filter(([i, v]) => i in target.mutex))
|
||||
}
|
||||
if (prop == "usable") {
|
||||
// ugly bastard
|
||||
if (!register.self) { return false }
|
||||
|
||||
let cardLocked = register.self.value?.turn?.cards_locked
|
||||
if (!cardLocked) { return false }
|
||||
let cardList = Object.values(cardLocked)
|
||||
const card = cardList.find(c => c.effects.map(e => e.uuid).includes(effect.uuid))
|
||||
if (!card) { return false }
|
||||
|
||||
if (target.effect_type === EffectType.FACTION_COMBO) {
|
||||
if (!cardList.some(c => c !== card && c.faction === card.faction)) { return false }
|
||||
}
|
||||
if (target.times === EffectTimes.PER_OTHER_CARD_OF_SAME_FACTION) {
|
||||
if (!cardList.some(c => c !== card && c.faction === card.faction)) { return false }
|
||||
} else if (target.times === EffectTimes.PER_OTHER_CHAMPION) {
|
||||
if (!cardList.some(c => c !== card && c.card_type === CardType.CHAMPION)) { return false }
|
||||
} else if (target.times === EffectTimes.PER_OTHER_GUARD) {
|
||||
if (!cardList.some(c => c !== card && c.guard)) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (prop == "card") {
|
||||
return register.cards.get(target.card)
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Effect
|
||||
return effect
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { type GameObjectRegister } from "."
|
||||
import { type Team } from "./Team"
|
||||
import { type GameObjectNetcode } from "./GameObject"
|
||||
import type { Card } from "./Card"
|
||||
import type { Player } from "./Player"
|
||||
|
||||
export interface GameNetcode extends GameObjectNetcode {
|
||||
market_stack: Array<string>
|
||||
market: Array<string>
|
||||
gem_stack: Array<string>
|
||||
started: boolean
|
||||
current_turn: string
|
||||
players: Array<string>
|
||||
losers: Array<string>
|
||||
ended: boolean
|
||||
owner: string
|
||||
}
|
||||
|
||||
|
||||
export type Game = Omit<GameNetcode, 'market_stack' | 'market' | 'gem_stack' | 'current_turn' | 'players'> & {
|
||||
market_stack: Array<Card>
|
||||
market: Array<Card | null>
|
||||
gem_stack: Array<Card>
|
||||
current_turn: Team
|
||||
players: Array<Player>
|
||||
}
|
||||
export function create_game(input: GameNetcode, register: GameObjectRegister) {
|
||||
return new Proxy<any>(input, {
|
||||
get(target: GameNetcode, prop, reciever) {
|
||||
if (prop == "market_stack") {
|
||||
return target.market_stack.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "market") {
|
||||
// console.log(target)
|
||||
return target.market.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "gem_stack") {
|
||||
return target.gem_stack.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "current_turn") {
|
||||
return register.teams.get(input.current_turn)
|
||||
}
|
||||
if (prop == "players") {
|
||||
return target.players.map(e => register.players.get(e))
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Game
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type GameObjectNetcode = {
|
||||
uuid: string
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { type GameObjectRegister } from "."
|
||||
import { type GameObjectNetcode } from "./GameObject"
|
||||
import type { Game } from "./Game"
|
||||
|
||||
export interface LobbyNetcode extends GameObjectNetcode {
|
||||
games: Array<string>
|
||||
}
|
||||
|
||||
|
||||
export type Lobby = Omit<LobbyNetcode, 'games'> & {
|
||||
games: Array<Game>
|
||||
}
|
||||
export function create_lobby(input: LobbyNetcode, register: GameObjectRegister) {
|
||||
return new Proxy<any>(input, {
|
||||
get(target: LobbyNetcode, prop, reciever) {
|
||||
if (prop == "games") {
|
||||
return target.games.map(e => register.games.get(e))
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Lobby
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { type GameObjectRegister } from "."
|
||||
import type { Card } from "./Card"
|
||||
import type { GameObjectNetcode } from "./GameObject"
|
||||
import { type Team } from "./Team"
|
||||
import { type Turn } from "./Turn"
|
||||
export interface PlayerNetcode extends GameObjectNetcode {
|
||||
stack_pile: Array<string>
|
||||
discard_pile: Array<string>
|
||||
hand: Array<string>
|
||||
board: Array<string>
|
||||
team: string
|
||||
username: string
|
||||
turn: string
|
||||
discord_id?: string
|
||||
image?: string
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
|
||||
export type Player = Omit<PlayerNetcode, 'discard_pile' | 'stack_pile' | 'hand' | 'board' | 'team' | 'turn'> & {
|
||||
stack_pile: Array<Card | null>
|
||||
discard_pile: Array<Card>
|
||||
hand: Array<Card | null>
|
||||
board: Array<Card>
|
||||
team: Team | undefined
|
||||
turn: Turn | undefined
|
||||
}
|
||||
export function create_player(input: PlayerNetcode, register: GameObjectRegister) {
|
||||
return new Proxy<any>(input, {
|
||||
get(target: PlayerNetcode, prop, reciever) {
|
||||
if (prop == "stack_pile") {
|
||||
return target.stack_pile.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "discard_pile") {
|
||||
return target.discard_pile.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "hand") {
|
||||
return target.hand.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "board") {
|
||||
return target.board.map(e => register.cards.get(e))
|
||||
}
|
||||
if (prop == "team") {
|
||||
return register.teams.get(target.team)
|
||||
}
|
||||
if (prop == "turn") {
|
||||
return register.turns.get(target.turn)
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Player
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { type GameObjectRegister } from "."
|
||||
import type { GameObjectNetcode } from "./GameObject"
|
||||
import { type Player } from "./Player"
|
||||
|
||||
export interface TeamNetcode extends GameObjectNetcode {
|
||||
health: number
|
||||
color: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type Team = TeamNetcode & {
|
||||
players: Array<Player>
|
||||
}
|
||||
|
||||
export function create_team(input: TeamNetcode, register: GameObjectRegister) {
|
||||
const team = new Proxy<any>(input, {
|
||||
get(target: TeamNetcode, prop, reciever) {
|
||||
if (prop == "players") {
|
||||
return Object.values(register.players).filter((p) => p.team == team)
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Team
|
||||
|
||||
return team
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { registerRuntimeCompiler } from "vue"
|
||||
import { type GameObjectRegister } from "."
|
||||
import type { Card } from "./Card"
|
||||
import { type Effect, CardEffects } from "./Effect"
|
||||
import type { GameObjectNetcode } from "./GameObject"
|
||||
export interface TurnNetcode extends GameObjectNetcode {
|
||||
cards_locked: Array<string>
|
||||
effects_availables: Array<string>
|
||||
champions_health: { [key: string]: string }
|
||||
discard_markers: number
|
||||
discard_marker_card: string
|
||||
fuck_u_markers: number
|
||||
damage_reserve: number
|
||||
gold_reserve: number
|
||||
}
|
||||
|
||||
|
||||
export type Turn = Omit<TurnNetcode, 'cards_locked' | 'effects_availables' | 'champions_health'|"discard_marker_card"> & {
|
||||
cards_locked: { [key: string]: Card }
|
||||
effects_availables: Array<Effect>
|
||||
champions_health: Map<string, number>
|
||||
discard_marker_card: Card
|
||||
}
|
||||
export function create_turn(input: TurnNetcode, register: GameObjectRegister) {
|
||||
const turn = new Proxy<any>(input, {
|
||||
get(target: TurnNetcode, prop, reciever) {
|
||||
if (prop == "cards_locked") {
|
||||
return Object.fromEntries(target.cards_locked.map(e => [e, register.cards.get(e)]))
|
||||
}
|
||||
if (prop == "effects_availables") {
|
||||
return target.effects_availables.map(e => register.effects.get(e))
|
||||
}
|
||||
if (prop == "champions_health") {
|
||||
const healths = new Map()
|
||||
for (const c in target.champions_health) {
|
||||
healths.set(register.cards.get(c)?.uuid, target.champions_health[c])
|
||||
}
|
||||
return healths
|
||||
}
|
||||
if(prop == "discard_marker_card"){
|
||||
return register.cards.get(target.discard_marker_card)
|
||||
}
|
||||
return Reflect.get(target, prop, reciever);
|
||||
},
|
||||
set(target, prop, val, receiver) {
|
||||
return Reflect.set(target, prop, val, receiver);
|
||||
}
|
||||
}) as Turn
|
||||
return turn
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function delay(delay: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
|
||||
// export type ContainerHandler = {
|
||||
// populateContainer: () => void;
|
||||
// shuffleContainer: () => void;
|
||||
// getCardsPosition: (cards: string) => any;
|
||||
// startAnimation: () => void;
|
||||
// endAnimation: () => void;
|
||||
// };
|
||||
|
||||
// export const containerEvents = reactive(new Map<string, ContainerHandler>());
|
||||
+150
-339
@@ -1,375 +1,186 @@
|
||||
import { type Card, create_card, type CardNetcode } from "./Card";
|
||||
import {
|
||||
type Effect,
|
||||
create_effect,
|
||||
type EffectNetcode,
|
||||
EffectType,
|
||||
CardEffects,
|
||||
} from "./Effect";
|
||||
import { type Game, create_game, type GameNetcode } from "./Game";
|
||||
import type { GameObjectNetcode } from "./GameObject";
|
||||
import { type Player, create_player, type PlayerNetcode } from "./Player";
|
||||
import { type Team, create_team, type TeamNetcode } from "./Team";
|
||||
import { type Turn, create_turn } from "./Turn";
|
||||
import { type Lobby, create_lobby, type LobbyNetcode } from "./Lobby";
|
||||
import type { Game, User } from "./interfaces";
|
||||
|
||||
import { type Ref } from "vue";
|
||||
|
||||
let socket: WebSocket;
|
||||
|
||||
export class GameObjectRegister {
|
||||
readonly effects: Map<string, Effect>;
|
||||
readonly cards: Map<string, Card>;
|
||||
readonly teams: Map<string, Team>;
|
||||
readonly players: Map<string, Player>;
|
||||
readonly turns: Map<string, Turn>;
|
||||
readonly games: Map<string, Game>;
|
||||
|
||||
context: "lobby" | "login" | "pregame" | "game" | "rules" = "login";
|
||||
private readonly objects: Map<string, GameObjectNetcode>;
|
||||
current_game: Ref<Game | null>;
|
||||
lobby: Ref<Lobby | null>;
|
||||
self: Ref<Player | null>;
|
||||
zoomKeyPressed: Ref<boolean>
|
||||
hoveredCard: Ref<number | undefined>
|
||||
|
||||
clientMouseX: Ref<number>
|
||||
clientMouseY: Ref<number>
|
||||
discordId: Ref<string>
|
||||
constructor() {
|
||||
this.effects = reactive(new Map());
|
||||
this.cards = reactive(new Map());
|
||||
this.teams = reactive(new Map());
|
||||
this.players = reactive(new Map());
|
||||
this.turns = reactive(new Map());
|
||||
this.games = reactive(new Map());
|
||||
|
||||
this.objects = reactive(new Map());
|
||||
this.current_game = ref(null);
|
||||
this.lobby = ref(null);
|
||||
this.self = ref(null);
|
||||
this.zoomKeyPressed = ref(false);
|
||||
this.hoveredCard = ref()
|
||||
this.clientMouseX = ref(0)
|
||||
this.clientMouseY = ref(0)
|
||||
this.discordId = ref("")
|
||||
}
|
||||
|
||||
update_object(obj: GameObjectNetcode) {
|
||||
// console.log(obj);
|
||||
// console.log(obj.uuid);
|
||||
// console.log(this.objects.get(obj.uuid));
|
||||
Object.assign(this.objects.get(obj.uuid) as GameNetcode, obj);
|
||||
if (unref(this.current_game)?.uuid === obj.uuid) {
|
||||
triggerRef(this.current_game);
|
||||
} else if (unref(this.lobby)?.uuid === obj.uuid) {
|
||||
triggerRef(this.lobby);
|
||||
} else if (unref(this.self)?.uuid === obj.uuid) {
|
||||
triggerRef(this.self);
|
||||
} else if (unref(this.self)?.turn?.uuid === obj.uuid) {
|
||||
triggerRef(this.self);
|
||||
}
|
||||
}
|
||||
|
||||
createObject = {
|
||||
effect: (data: EffectNetcode) => {
|
||||
const effect = create_effect(data, this);
|
||||
this.effects.set(data.uuid, effect);
|
||||
this.objects.set(data.uuid, data);
|
||||
},
|
||||
card: (data: CardNetcode) => {
|
||||
const card = create_card(data, this);
|
||||
this.cards.set(data.uuid, card);
|
||||
this.objects.set(data.uuid, card);
|
||||
},
|
||||
player: (data: PlayerNetcode) => {
|
||||
const player = create_player(data, this);
|
||||
this.players.set(data.uuid, player);
|
||||
this.objects.set(data.uuid, player);
|
||||
},
|
||||
team: (data: TeamNetcode) => {
|
||||
const team = create_team(data, this);
|
||||
this.teams.set(data.uuid, team);
|
||||
this.objects.set(data.uuid, team);
|
||||
},
|
||||
game: (data: GameNetcode) => {
|
||||
const game = create_game(data, this);
|
||||
this.games.set(data.uuid, game);
|
||||
this.objects.set(data.uuid, game);
|
||||
},
|
||||
turn: (data: any) => {
|
||||
const turn = create_turn(data, this);
|
||||
this.turns.set(data.uuid, turn);
|
||||
this.objects.set(data.uuid, turn);
|
||||
},
|
||||
lobby: (data: LobbyNetcode) => {
|
||||
this.lobby.value = create_lobby(data, this);
|
||||
this.objects.set(data.uuid, this.lobby.value);
|
||||
},
|
||||
} as { [id: string]: (data: any) => any };
|
||||
|
||||
setSelf = (uuid: string) => {
|
||||
this.self.value = this.players.get(uuid) ?? null;
|
||||
};
|
||||
|
||||
setDiscordId = (username: string) => {
|
||||
this.discordId.value = username
|
||||
}
|
||||
setCurrentGame = (uuid: string) => {
|
||||
this.current_game.value = this.games.get(uuid) ?? null;
|
||||
};
|
||||
}
|
||||
|
||||
const goStore = reactive(new GameObjectRegister());
|
||||
|
||||
export const setup_websocket = () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
if (socket) {
|
||||
return resolve();
|
||||
}
|
||||
const config = useRuntimeConfig();
|
||||
const heron_session = useCookie("heron_session");
|
||||
socket = new WebSocket(config.public.websocketEndpoint);
|
||||
|
||||
socket.onopen = function (e) {
|
||||
if (socket.readyState == 1) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
console.log("[open] Connection established");
|
||||
};
|
||||
|
||||
socket.onclose = function (event) {
|
||||
if (event.wasClean) {
|
||||
console.warn(
|
||||
`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`
|
||||
);
|
||||
} else {
|
||||
// e.g. server process killed or network down
|
||||
// event.code is usually 1006 in this case
|
||||
console.error("[close] Connection died");
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = function (error) {
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type == "create" && data.data_type == "object") {
|
||||
if (!(data.data.object_type in goStore.createObject)) {
|
||||
throw new Error(
|
||||
`Object ${data.data.object_type} couldn't be created`
|
||||
);
|
||||
}
|
||||
goStore.createObject[data.data.object_type](data.data);
|
||||
} else if (data.type == "update" && data.data_type == "object") {
|
||||
goStore.update_object(data.data);
|
||||
} else if (data.type === "context") {
|
||||
goStore.context = data.data;
|
||||
// console.log(data.data);
|
||||
} else if (data.type === "set") {
|
||||
if (data.data_type == "self") {
|
||||
goStore.setSelf(data.data);
|
||||
} else if (data.data_type == "current_game") {
|
||||
goStore.setCurrentGame(data.data);
|
||||
} else if (data.data_type == "session_cookie") {
|
||||
heron_session.value = data.data;
|
||||
} else if (data.data_type == "self_discord_id") {
|
||||
goStore.setDiscordId(data.data)
|
||||
}
|
||||
} else {
|
||||
// console.log(data.type, data.data_type, data.data);
|
||||
}
|
||||
};
|
||||
export function subscribe(endpoint: string) {
|
||||
const config = useRuntimeConfig();
|
||||
const evtSource = new EventSource(config.public.endpoint + endpoint, {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export async function heron_cookie_login(cookie: string) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "cookie_login",
|
||||
data_type: "cookie",
|
||||
data: cookie,
|
||||
})
|
||||
return evtSource;
|
||||
}
|
||||
|
||||
export async function whoami() {
|
||||
const config = useRuntimeConfig();
|
||||
const req = await useFetch<User>(config.public.endpoint + "/whoami", {
|
||||
credentials: "include",
|
||||
server: false,
|
||||
});
|
||||
await req.execute();
|
||||
if (req.error.value && req.error.value.statusCode == 401) {
|
||||
// await navigateTo(config.public.discordLoginEndpoint, {
|
||||
// external: true,
|
||||
// });
|
||||
throw new Error("You are not logged in");
|
||||
}
|
||||
if (req.error.value) {
|
||||
throw new Error(
|
||||
"Something went wrong while trying to get user information"
|
||||
);
|
||||
}
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function createGame() {
|
||||
const config = useRuntimeConfig();
|
||||
const res = await $fetch<Game>(config.public.endpoint + "/games", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
navigateTo(`/game?id=${res._id}`);
|
||||
}
|
||||
|
||||
export async function startGame(game: string) {
|
||||
const config = useRuntimeConfig();
|
||||
const res = await $fetch<Game>(
|
||||
config.public.endpoint + `/games/${game}/start`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function oauth2_register_discord(code: string) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "discord_login",
|
||||
data_type: "code",
|
||||
data: code,
|
||||
})
|
||||
export async function joinGame(game: string) {
|
||||
const config = useRuntimeConfig();
|
||||
const res = await $fetch<Game>(
|
||||
config.public.endpoint + `/games/${game}/join`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function register(username: string) {
|
||||
await setup_websocket();
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "register",
|
||||
data_type: "username",
|
||||
data: username,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function login(username: string) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "login",
|
||||
data_type: "username",
|
||||
data: username,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function setReady(value: boolean) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "ready",
|
||||
data_type: "ready_state",
|
||||
data: value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function buy(card: Card) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "buy",
|
||||
data: card.uuid,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function endTurn() {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "end_turn",
|
||||
data: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function playAllCards(cards: Card[]) {
|
||||
cards.forEach((c: any) => {
|
||||
playCard(c);
|
||||
export async function endTurn(game: string) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(config.public.endpoint + `/games/${game}/endTurn`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
}
|
||||
|
||||
export async function playCard(card: Card) {
|
||||
// console.log(card.uuid);
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "play_card",
|
||||
data: card.uuid,
|
||||
})
|
||||
export async function lockCard(game: string, cardId: string) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/lockCard/${cardId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function lockCard(card: Card) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "lock_card",
|
||||
data: card.uuid,
|
||||
})
|
||||
export async function buyCard(game: string, cardId: string) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/buyCard/${cardId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function useEffect(
|
||||
effect: Effect,
|
||||
target?: Effect | Card | Player | Team
|
||||
export async function buyGem(game: string) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(config.public.endpoint + `/games/${game}/turn/buyGem`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
}
|
||||
|
||||
export async function useToken(
|
||||
game: string,
|
||||
tokenId: string,
|
||||
cardId?: string,
|
||||
playerId?: string
|
||||
) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "use_effect",
|
||||
data: { effect_id: effect.uuid, target: target?.uuid },
|
||||
})
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/useToken/${tokenId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: { cardId, playerId },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function discard(card: Card) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "discard",
|
||||
data: card.uuid,
|
||||
})
|
||||
);
|
||||
}
|
||||
h;
|
||||
export async function heal() {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "heal",
|
||||
data: 1,
|
||||
})
|
||||
export async function lockEffect(game: string, effectId: string) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/lockEffect/${effectId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function damage_team(target: Team) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "damage_team",
|
||||
data: target.uuid,
|
||||
})
|
||||
export async function makeDiscard(
|
||||
game: string,
|
||||
playerId: string,
|
||||
amount: number = 1
|
||||
) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/makeDiscard/${playerId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function damage_champion(target: Card) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "action",
|
||||
data_type: "damage_champion",
|
||||
data: target.uuid,
|
||||
})
|
||||
export async function damagePlayer(
|
||||
game: string,
|
||||
playerId: string,
|
||||
amount: number = 1
|
||||
) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/damagePlayer/${playerId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: { amount },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function netcode_create_game() {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "create",
|
||||
data_type: "game",
|
||||
data: "bite",
|
||||
})
|
||||
export async function damageChampion(
|
||||
game: string,
|
||||
playerId: string,
|
||||
cardId: string
|
||||
) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/damageChampion/${cardId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: { playerId },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function join_game(game_uuid: string) {
|
||||
await setup_websocket();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "join",
|
||||
data_type: "game",
|
||||
data: game_uuid,
|
||||
})
|
||||
export async function discardCard(game: string, cardId: string) {
|
||||
const config = useRuntimeConfig();
|
||||
await $fetch(
|
||||
config.public.endpoint + `/games/${game}/turn/discardCard/${cardId}`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
}
|
||||
export default goStore;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
export enum CardEffects {
|
||||
//Base
|
||||
GOLD = "gold",
|
||||
DAMAGE = "damage",
|
||||
HEAL = "heal",
|
||||
PREPARE = "prepare",
|
||||
STUN = "stun",
|
||||
SACRIFICE = "sacrifice",
|
||||
DRAW = "draw",
|
||||
DRAW_AND_DISCARD = "draw_and_discard",
|
||||
MAKE_DISCARD = "make_discard",
|
||||
RESTACK_DISCARDED_CHAMPION = "restack_discarded_champion",
|
||||
RESTACK_DISCARDED_CARD = "restack_discarded_card",
|
||||
STACK_NEXT_ACTION_BOUGHT = "stack_next_action_bought",
|
||||
STACK_NEXT_CARD_BOUGHT = "stack_next_card_bought",
|
||||
PLAY_NEXT_CARD_BOUGHT = "play_next_card_bought",
|
||||
|
||||
//DLCs
|
||||
BUY_FOR_FREE = "buy_for_free",
|
||||
|
||||
//Journeys
|
||||
DAMAGE_ALL_CHAMPIONS = "damage_all_champions",
|
||||
|
||||
//Journeys: Hunters
|
||||
DISCARD_X_AND_DRAW_X = "discard_x_and_draw_x",
|
||||
PREPARE_ANOTHER_CHAMPION = "prepare_another_champion",
|
||||
//Journey: Travelers
|
||||
PREPARE_ALL_CHAMPIONS = "prepare_all_champions",
|
||||
CHEAPER_CHAMPION = "cheaper_champion",
|
||||
CHEAPER_CHAMPIONS_PASSIVE = "cheaper_champions_passive",
|
||||
CHEAPER_ACTION = "cheaper_action",
|
||||
CONTROL_OPPOSING_CHAMPION_PASSIVE = "control_opposing_champion_passive",
|
||||
|
||||
RESTACK_DISCARDED_ACTION = "restack_discarded_action",
|
||||
|
||||
//Ancestry
|
||||
KEEP_IN_HAND = "keep_in_hand",
|
||||
BUY_GEM_FOR_FREE = "buy_gem_for_free",
|
||||
CHEAPER_SKILLS_PASSIVE = "cheaper_skills_passive",
|
||||
CHEAPER_CARD_IF_HIGHER_PRICE = "cheaper_card_if_higher_price",
|
||||
PICK_FACTION = "pick_faction",
|
||||
}
|
||||
|
||||
export enum CardType {
|
||||
HERO = "hero",
|
||||
HERO_ABILITY = "hero_ability",
|
||||
CURRENCY = "currency",
|
||||
WEAPON = "weapon",
|
||||
CHAMPION = "champion",
|
||||
ACTION = "action",
|
||||
ITEM = "item",
|
||||
}
|
||||
|
||||
export interface GameObject {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface PlayingTurn {
|
||||
currentTeam: string;
|
||||
cardsLocked: string[];
|
||||
lockedEffects: string[];
|
||||
tokens: Effect[];
|
||||
damage: number;
|
||||
gold: number;
|
||||
}
|
||||
|
||||
export interface Game extends GameObject {
|
||||
fireGems: Container;
|
||||
market: Container;
|
||||
marketStack: Container;
|
||||
teams: Team[];
|
||||
currentTurn: PlayingTurn;
|
||||
started: boolean;
|
||||
owner: User;
|
||||
}
|
||||
|
||||
export interface Team extends GameObject {
|
||||
players: Player[];
|
||||
health: number;
|
||||
}
|
||||
|
||||
export interface Player extends GameObject {
|
||||
board: Container;
|
||||
discard: Container;
|
||||
hand: Container;
|
||||
stack: Container;
|
||||
user: User;
|
||||
discardMarkers: number;
|
||||
fuckUMarkers: number;
|
||||
}
|
||||
|
||||
export interface User extends GameObject {
|
||||
userId: string;
|
||||
avatar: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface Container extends GameObject {
|
||||
cards: Card[];
|
||||
shuffling?: boolean;
|
||||
}
|
||||
|
||||
export interface Effect extends GameObject {
|
||||
effect: CardEffects;
|
||||
}
|
||||
export interface Card extends GameObject {
|
||||
cardId: number;
|
||||
cardType: CardType;
|
||||
effects: Effect[];
|
||||
defense?: number;
|
||||
guard?: boolean;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
#access_log /var/log/nginx/host.access.log main;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
error_page 404 /usr/share/nginx/html/404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
# error_page 500 502 503 504 /50x.html;
|
||||
# location = /50x.html {
|
||||
# root /usr/share/nginx/html;
|
||||
# }
|
||||
|
||||
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
|
||||
#
|
||||
#location ~ \.php$ {
|
||||
# proxy_pass http://127.0.0.1;
|
||||
#}
|
||||
|
||||
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
|
||||
#
|
||||
#location ~ \.php$ {
|
||||
# root html;
|
||||
# fastcgi_pass 127.0.0.1:9000;
|
||||
# fastcgi_index index.php;
|
||||
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
|
||||
# include fastcgi_params;
|
||||
#}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
#location ~ /\.ht {
|
||||
# deny all;
|
||||
#}
|
||||
}
|
||||
|
||||
+18
-8
@@ -1,14 +1,24 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
ssr: false,
|
||||
ssr: true,
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
websocketEndpoint: 'ws://127.0.0.1:8765', // can be overridden by NUXT_PUBLIC_API_BASE environment variable
|
||||
discordLoginEndpoint: ''
|
||||
}
|
||||
welcomeMessage: "Hello",
|
||||
endpoint: "https://playtest.legonzaur.fr/api", // can be overridden by NUXT_PUBLIC_ENDPOINT environment variable
|
||||
discordLoginEndpoint: process.env.NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT,
|
||||
devMode: false,
|
||||
},
|
||||
},
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
],
|
||||
})
|
||||
modules: ["@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"],
|
||||
vue: {
|
||||
runtimeCompiler: true,
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ["/rules", "/login", "/game", "/lobby"],
|
||||
|
||||
crawlLinks: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+35
@@ -8,10 +8,14 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"gsap": "^3.12.5",
|
||||
"nuxt": "^3.11.2",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pinia-plugin-persistedstate/nuxt": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -2093,6 +2097,21 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinia-plugin-persistedstate/nuxt": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinia-plugin-persistedstate/nuxt/-/nuxt-1.2.0.tgz",
|
||||
"integrity": "sha512-2rtgx5viGSMQMCoFYZMHguA2FhFKCUvw0PwETfqQegsWeBHlqk1/D0G/9xqep8Hq+c1BuFx+jNLJzoLXtYfivg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^3.8.0",
|
||||
"defu": "^6.1.2",
|
||||
"pinia-plugin-persistedstate": ">=3.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pinia/nuxt": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinia/nuxt": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.5.1.tgz",
|
||||
@@ -5652,6 +5671,12 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"node_modules/gsap": {
|
||||
"version": "3.12.5",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
|
||||
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==",
|
||||
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
|
||||
@@ -7803,6 +7828,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia-plugin-persistedstate": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz",
|
||||
"integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pinia": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/vue-demi": {
|
||||
"version": "0.14.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz",
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"cards-gen": "sh -c 'docker run --rm --pull=always -v ./assets/images/cards/heron-svg:/app/output/heron git.legonzaur.fr/heronfief/cards-gen:main' && sudo chown -R $USER:$USER ./assets/images/cards/heron-svg",
|
||||
"pull-images": "sh -c 'docker run --rm --pull=always -v ./public/img:/repo/images/skins git.legonzaur.fr/heronfief/cards-gen:main cp -r img/skins/ /repo/images/skins' && sudo chown -R $USER:$USER ./public/",
|
||||
"fetch": "npm run cards-gen && npm run pull-images",
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
@@ -11,9 +14,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"gsap": "^3.12.5",
|
||||
"nuxt": "^3.11.2",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pinia-plugin-persistedstate/nuxt": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
+583
@@ -0,0 +1,583 @@
|
||||
<script setup lang="ts">
|
||||
import LoadingScreen from "~/components/game/overlay/LoadingScreen.vue";
|
||||
import { endTurn, subscribe, useToken, startGame, joinGame } from "~/netcode";
|
||||
import { delay } from "~/netcode/events";
|
||||
import {
|
||||
type Container,
|
||||
type Effect,
|
||||
type Game,
|
||||
type Player,
|
||||
} from "~/netcode/interfaces";
|
||||
import { playSound } from "~/helpers/audioHelpers";
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const settingsStore = useSettingsStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const route = useRoute();
|
||||
const gameId = route.query.id;
|
||||
|
||||
const {
|
||||
data: game,
|
||||
pending,
|
||||
error,
|
||||
refresh,
|
||||
execute,
|
||||
} = await useFetch<Game>(config.public.endpoint + `/games/${gameId}`, {
|
||||
credentials: "include",
|
||||
server: false,
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
await execute();
|
||||
|
||||
const containers = computed<Record<string, Container>>(() => ({
|
||||
[game.value!.fireGems._id]: game.value!.fireGems,
|
||||
[game.value!.market._id]: game.value!.market,
|
||||
[game.value!.marketStack._id]: game.value!.marketStack,
|
||||
...Object.fromEntries(
|
||||
game
|
||||
.value!.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
|
||||
.map((p) => [
|
||||
[p.board._id, p.board],
|
||||
[p.discard._id, p.discard],
|
||||
[p.hand._id, p.hand],
|
||||
[p.stack._id, p.stack],
|
||||
])
|
||||
.flat(1)
|
||||
),
|
||||
}));
|
||||
|
||||
let eventSource: EventSource;
|
||||
|
||||
const actions = ref<(() => Promise<any>)[]>([]);
|
||||
|
||||
function queueAction(callback: () => Promise<any>) {
|
||||
actions.value.push(callback);
|
||||
if (actions.value.length == 1) {
|
||||
execAction(callback);
|
||||
}
|
||||
}
|
||||
|
||||
async function execAction(callback: () => Promise<any>) {
|
||||
await callback();
|
||||
actions.value.shift();
|
||||
if (actions.value.length > 0) {
|
||||
execAction(actions.value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
start: async (data: any, game: Game) => {
|
||||
game.started = true;
|
||||
game.currentTurn = data.game.currentTurn;
|
||||
},
|
||||
populateContainer: async (data: any) => {
|
||||
const fromContainer = containers.value[data.container._id as string];
|
||||
playSound("/sounds/distribute.mp3");
|
||||
for (const card of data.container.cards) {
|
||||
fromContainer.cards.push(card);
|
||||
// if (!settingsStore.fastAnimations) {
|
||||
// await delay(settingsStore.animationSpeed);
|
||||
// }
|
||||
}
|
||||
},
|
||||
shuffleContainer: async (data: any) => {
|
||||
playSound("/sounds/shuffle.mp3");
|
||||
const container = containers.value[data.container as string];
|
||||
container.shuffling = true;
|
||||
await delay(settingsStore.animationSpeed * 2);
|
||||
container.shuffling = false;
|
||||
await delay(settingsStore.animationSpeed * 2);
|
||||
},
|
||||
distributeCards: async (data: any) => {
|
||||
const fromContainer = containers.value[data.from as string];
|
||||
const toContainer = containers.value[data.to as string];
|
||||
if (settingsStore.fastAnimations) {
|
||||
playSound("/sounds/distribute.mp3");
|
||||
}
|
||||
for (const card of data.cards) {
|
||||
const index = fromContainer.cards.findIndex(
|
||||
(c) => !c || c._id == card._id
|
||||
);
|
||||
fromContainer.cards.splice(index, 1);
|
||||
if (!settingsStore.fastAnimations) {
|
||||
await delay(settingsStore.animationSpeed);
|
||||
playSound("/sounds/distribute.mp3");
|
||||
}
|
||||
toContainer.cards.push(card);
|
||||
if (!settingsStore.fastAnimations) {
|
||||
await delay(settingsStore.animationSpeed);
|
||||
}
|
||||
}
|
||||
await delay(settingsStore.animationSpeed * 2);
|
||||
},
|
||||
endTurn: async (data: any, game: Game) => {
|
||||
game.currentTurn = data.currentTurn;
|
||||
await delay(settingsStore.animationSpeed);
|
||||
const playingPlayer = game.teams.find(
|
||||
(t) => t._id == game.currentTurn.currentTeam
|
||||
)?.players[0];
|
||||
if (playingPlayer) {
|
||||
focusPlayer(playingPlayer);
|
||||
await delay(settingsStore.animationSpeed);
|
||||
}
|
||||
},
|
||||
lockCard: async (data: any, game: Game) => {
|
||||
playSound("/sounds/lock.mp3");
|
||||
game.currentTurn.cardsLocked.push(data.card);
|
||||
await delay(settingsStore.animationSpeed);
|
||||
},
|
||||
destroyCard: async (data: any) => {
|
||||
const fromContainer = containers.value[data.from as string];
|
||||
const index = fromContainer.cards.findIndex(
|
||||
(c) => !c || c._id == data.card
|
||||
);
|
||||
fromContainer.cards.splice(index, 1);
|
||||
playSound("/sounds/destroy.mp3");
|
||||
await delay(settingsStore.animationSpeed);
|
||||
},
|
||||
joinGame: async (data: any, game: Game) => {
|
||||
playSound("/sounds/teleport.mp3");
|
||||
game.teams.push(data.team);
|
||||
},
|
||||
lockEffect: async (data: any, game: Game) => {
|
||||
game.currentTurn.lockedEffects.push(data.effect);
|
||||
await delay(settingsStore.animationSpeed);
|
||||
},
|
||||
unlockEffect: async (data: any, game: Game) => {
|
||||
const effectIndex = game.currentTurn.lockedEffects.indexOf(data.effect);
|
||||
game.currentTurn.lockedEffects.splice(effectIndex, 1);
|
||||
await delay(settingsStore.animationSpeed);
|
||||
},
|
||||
"currentTurn.updateGold": async (data: any, game: Game) => {
|
||||
console.log(`adding gold amount : ` + data.operation);
|
||||
game.currentTurn.gold = data.gold;
|
||||
},
|
||||
"currentTurn.updateDamage": async (data: any, game: Game) => {
|
||||
console.log(`adding damage amount : ` + data.operation);
|
||||
game.currentTurn.damage = data.damage;
|
||||
},
|
||||
"currentTurn.addToken": async (data: any, game: Game) => {
|
||||
game.currentTurn.tokens.push(data.token);
|
||||
await delay(settingsStore.animationSpeed);
|
||||
},
|
||||
"currentTurn.useToken": async (data: any, game: Game) => {
|
||||
const index = game.currentTurn.tokens.findIndex(
|
||||
(t) => t._id == data.tokenId
|
||||
);
|
||||
game.currentTurn.tokens.splice(index, 1);
|
||||
if (!settingsStore.fastAnimations) {
|
||||
await delay(settingsStore.animationSpeed);
|
||||
}
|
||||
},
|
||||
"team.updateHealth": async (data: any, game: Game) => {
|
||||
const team = game.teams.find((t) => t._id == data.team);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
console.log(`adding health amount : ` + data.operation);
|
||||
team.health = data.health;
|
||||
},
|
||||
"player.addFuckUMarker": async (data: any, game: Game) => {
|
||||
console.log(`adding fuckUMarkers : ` + data.operation);
|
||||
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
||||
if (!targetPlayer) {
|
||||
throw new Error("Cannot add fuckU Markers : player not found");
|
||||
}
|
||||
targetPlayer.fuckUMarkers = data.fuckUMarkers;
|
||||
},
|
||||
"player.removeFuckUMarker": async (data: any, game: Game) => {
|
||||
console.log(`adding fuckUMarkers : ` + data.operation);
|
||||
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
||||
if (!targetPlayer) {
|
||||
throw new Error("Cannot remove fuckU Markers : player not found");
|
||||
}
|
||||
targetPlayer.fuckUMarkers = data.fuckUMarkers;
|
||||
},
|
||||
"player.addDiscardMarker": async (data: any, game: Game) => {
|
||||
console.log(`adding discardMarkers : ` + data.operation);
|
||||
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
||||
if (!targetPlayer) {
|
||||
throw new Error("Cannot add discard Markers : player not found");
|
||||
}
|
||||
targetPlayer.discardMarkers = data.discardMarkers;
|
||||
},
|
||||
"player.removeDiscardMarker": async (data: any, game: Game) => {
|
||||
console.log(`adding fuckUMarkers : ` + data.operation);
|
||||
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
|
||||
if (!targetPlayer) {
|
||||
throw new Error("Cannot remove discard Markers : player not found");
|
||||
}
|
||||
targetPlayer.discardMarkers = data.discardMarkers;
|
||||
},
|
||||
killTeam: async (data: any, game: Game) => {
|
||||
const index = game.teams.findIndex((t) => t._id == data.teamId);
|
||||
if (!index) {
|
||||
console.error("team index not found. Desyncs might happen");
|
||||
await refresh();
|
||||
return;
|
||||
}
|
||||
game.teams.splice(index, 1);
|
||||
},
|
||||
consumeEffect: async (data: any) => {
|
||||
console.warn("consumeEffect not implemented");
|
||||
},
|
||||
endGame: async () => {
|
||||
alert("game ended");
|
||||
},
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
if (game.value == null) {
|
||||
navigateTo("/lobby");
|
||||
throw new Error("game not found");
|
||||
}
|
||||
if (game.value.started) {
|
||||
focusedPlayer.value = game.value.teams.find(
|
||||
(t) => t._id == game.value?.currentTurn.currentTeam
|
||||
)?.players[0];
|
||||
} else {
|
||||
focusedPlayer.value = selfPlayer.value ?? playerList.value![0];
|
||||
}
|
||||
|
||||
eventSource = subscribe(`/games/${gameId}/subscribe`);
|
||||
eventSource.addEventListener("message", async (message) => {
|
||||
const current_game = game.value;
|
||||
if (!current_game) {
|
||||
throw new Error("game not found");
|
||||
}
|
||||
const data = JSON.parse(message.data);
|
||||
queueAction(async () => {
|
||||
if (data.type in handlers) {
|
||||
await handlers[data.type as keyof typeof handlers](data, current_game);
|
||||
} else {
|
||||
console.error(data.type + " not implemented");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const playerList = computed(
|
||||
() =>
|
||||
game.value &&
|
||||
game.value.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
|
||||
);
|
||||
const selfPlayer = computed(() =>
|
||||
authStore.logged
|
||||
? playerList.value?.find((p) => p.user.userId == authStore.selfUser.userId)
|
||||
: undefined
|
||||
);
|
||||
const isTurn = computed(
|
||||
() =>
|
||||
(game.value &&
|
||||
selfPlayer.value &&
|
||||
game.value.teams.find((t) => t.players.includes(selfPlayer.value!))
|
||||
?._id == game.value.currentTurn.currentTeam) ??
|
||||
false
|
||||
);
|
||||
|
||||
const turnChangeAnimationSpeed = computed(
|
||||
() => settingsStore.animationSpeed * 2 + "ms"
|
||||
);
|
||||
// TODO : current turn sound
|
||||
|
||||
// TODO : winner screen
|
||||
|
||||
// TODO : join sound
|
||||
|
||||
const showResults = ref(false);
|
||||
const isWinner = ref(false);
|
||||
const loaded = ref(false);
|
||||
const discard_unwrapped = ref(false);
|
||||
const focusedPlayer = ref<Player | undefined>(undefined);
|
||||
|
||||
function showLoserScreen() {
|
||||
// playSound('/sounds/ding.mp3')
|
||||
showResults.value = true;
|
||||
isWinner.value = false;
|
||||
}
|
||||
|
||||
function showWinnerScreen() {
|
||||
// playSound('/sounds/ding.mp3')
|
||||
showResults.value = true;
|
||||
isWinner.value = true;
|
||||
}
|
||||
|
||||
function focusPlayer(p: Player) {
|
||||
focusedPlayer.value = p;
|
||||
}
|
||||
|
||||
function useTokenClick(t: Effect) {
|
||||
console.log(t);
|
||||
useToken(game.value!._id, t._id);
|
||||
}
|
||||
|
||||
function showDiscard() {
|
||||
discard_unwrapped.value = true;
|
||||
}
|
||||
onUnmounted(() => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @contextmenu.prevent="false" id="game_board">
|
||||
<AnimatedBackground
|
||||
@ready="loaded = true"
|
||||
:isTurn="isTurn ?? false"
|
||||
></AnimatedBackground>
|
||||
<ClientOnly>
|
||||
<template v-if="game && focusedPlayer && playerList">
|
||||
<!-- <Transition name="slide-left" mode="out-in"> -->
|
||||
<div id="stack_discard" :key="focusedPlayer?._id">
|
||||
<div id="stack">
|
||||
<GameCardGrouped
|
||||
:container="focusedPlayer.stack"
|
||||
:wrapped="true"
|
||||
:hidden="true"
|
||||
>
|
||||
</GameCardGrouped>
|
||||
</div>
|
||||
|
||||
<GameOverlayDiscard
|
||||
:game="game"
|
||||
:player="focusedPlayer"
|
||||
></GameOverlayDiscard>
|
||||
</div>
|
||||
<!-- </Transition> -->
|
||||
|
||||
<GameMarket
|
||||
:game="game"
|
||||
:market="game.market"
|
||||
:fire_gems="game.fireGems"
|
||||
:marketStack="game.marketStack"
|
||||
>
|
||||
</GameMarket>
|
||||
<div>
|
||||
<GameToken
|
||||
:token="t"
|
||||
v-for="t in game.currentTurn.tokens"
|
||||
@useToken="useTokenClick"
|
||||
></GameToken>
|
||||
</div>
|
||||
<div id="player_list_container">
|
||||
<div id="player_list">
|
||||
<GamePlayerListElement
|
||||
v-for="p in playerList"
|
||||
:player="p"
|
||||
:game="game"
|
||||
@click="focusPlayer(p)"
|
||||
:focused="focusedPlayer == p"
|
||||
:is-self-turn="isTurn"
|
||||
:self="selfPlayer == p"
|
||||
>
|
||||
</GamePlayerListElement>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="current_player">
|
||||
<Transition name="slide-left" mode="out-in">
|
||||
<GamePlayerSlot
|
||||
v-if="focusedPlayer"
|
||||
:player="focusedPlayer"
|
||||
:game="game"
|
||||
:key="focusedPlayer._id"
|
||||
></GamePlayerSlot>
|
||||
</Transition>
|
||||
</div>
|
||||
<GameOverlayResultDialog
|
||||
:isVisible="showResults"
|
||||
:won="isWinner"
|
||||
:players="playerList"
|
||||
@close="showResults = false"
|
||||
>
|
||||
</GameOverlayResultDialog>
|
||||
<GamePlayerSelf
|
||||
v-if="selfPlayer"
|
||||
:container="selfPlayer.hand"
|
||||
></GamePlayerSelf>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
|
||||
<!-- <LoadingScreen v-if="!loaded"></LoadingScreen> -->
|
||||
<footer>
|
||||
<GameOverlayOptionsDialog></GameOverlayOptionsDialog>
|
||||
<ClientOnly>
|
||||
<div id="turn_buttons" v-if="game">
|
||||
<template v-if="game.started">
|
||||
<button
|
||||
id="end_turn"
|
||||
class="end_turn_button turn_icon"
|
||||
:class="{ hidden: !isTurn }"
|
||||
@click="endTurn(game._id)"
|
||||
v-if="isTurn"
|
||||
>
|
||||
END TURN
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="authStore.logged">
|
||||
<button
|
||||
class="end_turn_button turn_icon"
|
||||
@click="startGame(game._id)"
|
||||
v-if="game.owner.userId == authStore.selfUser.userId"
|
||||
>
|
||||
Start game
|
||||
</button>
|
||||
<button
|
||||
class="end_turn_button turn_icon"
|
||||
@click="joinGame(game._id)"
|
||||
v-else-if="
|
||||
playerList?.every(
|
||||
(p) => p.user.userId != authStore.selfUser.userId
|
||||
)
|
||||
"
|
||||
>
|
||||
Join game
|
||||
</button>
|
||||
<button v-else>Leave game</button>
|
||||
</template>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
|
||||
<button @click="navigateTo('/lobby')">Return to lobby</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
footer {
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
grid-area: footer;
|
||||
height: 50px;
|
||||
/* background: red; */
|
||||
justify-content: space-between;
|
||||
}
|
||||
#game_status {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
#game_board {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content 1fr;
|
||||
grid-template-rows: min-content min-content 1fr min-content;
|
||||
grid-template-areas:
|
||||
"stack_discard market turn_stats"
|
||||
"player_list player_list player_list"
|
||||
"current_player current_player current_player"
|
||||
"footer footer footer";
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#player_list {
|
||||
display: flex;
|
||||
flex-direction: inherit;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#current_player {
|
||||
grid-area: current_player;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow-x: clip;
|
||||
/* background: url('/img/bg-darkgrass.png'); */
|
||||
}
|
||||
|
||||
#stack_discard {
|
||||
grid-area: stack_discard;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#stack,
|
||||
#discard {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#discard {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.discard_overlay {
|
||||
z-index: 9;
|
||||
display: flex;
|
||||
/* background: blue; */
|
||||
pointer-events: none;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap-reverse;
|
||||
align-content: flex-end;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
padding-bottom: inherit;
|
||||
padding-left: 40px;
|
||||
height: calc(100% - 64px);
|
||||
padding-top: 64px;
|
||||
width: calc(100% -40px);
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#discard_unwrapped_bg {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 8;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
pointer-events: all;
|
||||
padding: inherit;
|
||||
}
|
||||
|
||||
#discard_unwrapped_bg.unwrapped {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#market {
|
||||
grid-area: market;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#player_list_container {
|
||||
grid-area: player_list;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
/* background-image: url("/img/bg-darkgrass.png"); */
|
||||
}
|
||||
|
||||
#player_list {
|
||||
/* border: 5px solid #000; */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active {
|
||||
transition: all v-bind("settingsStore.animationSpeed * 2 + 'ms'") ease-out;
|
||||
}
|
||||
|
||||
.slide-left-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import goStore, { setReady } from "@/netcode";
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root_page_div">
|
||||
<div id="game_status">
|
||||
|
||||
<template v-if="goStore.self && !goStore.current_game?.started">
|
||||
<input @click.prevent="setReady(($event.target as HTMLInputElement).checked)" type="checkbox" id="ready"
|
||||
:checked="goStore.self?.ready" />
|
||||
<label for="ready">Ready</label>
|
||||
</template>
|
||||
</div>
|
||||
<LoginView v-if="goStore.context === 'login'"></LoginView>
|
||||
<GameBoard v-if="goStore.context != 'lobby' && goStore.context != 'login'"></GameBoard>
|
||||
<LobbyView v-if="goStore.context == 'lobby'"></LobbyView>
|
||||
<RulesDisplay v-if="goStore.context == 'rules'"></RulesDisplay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.root_page_div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#game_status {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(255, 255, 255, .75);
|
||||
}
|
||||
</style>
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
<script setup lang="ts">
|
||||
// import { playSound } from '~/helpers/audioHelpers';
|
||||
import { playSound } from "~/helpers/audioHelpers";
|
||||
import { createGame, subscribe } from "~/netcode";
|
||||
import type { Game, Player, User } from "~/netcode/interfaces";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
let eventSource: EventSource;
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const req = await useFetch<Array<Game>>(config.public.endpoint + "/games", {
|
||||
credentials: "include",
|
||||
server: false,
|
||||
});
|
||||
|
||||
let games = req.data;
|
||||
|
||||
const erika_alive = ref(true);
|
||||
const router = useRouter();
|
||||
|
||||
function killErika() {
|
||||
playSound("/sounds/splat1.mp3");
|
||||
erika_alive.value = false;
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
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 || !index) {
|
||||
throw new Error("Couldn't delete game");
|
||||
}
|
||||
games.value?.splice(index, 1);
|
||||
} else if (data.type == "joinGame") {
|
||||
games.value?.find((g) => g._id == data.game)?.teams.push(data.team);
|
||||
} else if (data.type == "start") {
|
||||
const targetGame = games.value?.find((g) => g._id == data.game);
|
||||
if (targetGame) {
|
||||
targetGame.started = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventSource.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="background">
|
||||
<header>
|
||||
<ClientOnly>
|
||||
<GamePlayerIcon
|
||||
v-if="authStore.logged"
|
||||
:user="(authStore.selfUser as User)"
|
||||
/>
|
||||
<button v-else @click="navigateTo('/login')">Login</button>
|
||||
</ClientOnly>
|
||||
</header>
|
||||
<main>
|
||||
<div class="title">Entrez dans le Royaume de Héron !</div>
|
||||
<ClientOnly>
|
||||
<template v-if="authStore.logged">
|
||||
<div class="create-game-zone">
|
||||
<button
|
||||
@click="createGame"
|
||||
v-if="
|
||||
!games?.some(
|
||||
(g) => g.owner.userId === authStore.selfUser.userId
|
||||
)
|
||||
"
|
||||
>
|
||||
Create Game
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="games-list-zone">
|
||||
<div v-for="g in games">
|
||||
<button
|
||||
v-if="g"
|
||||
@click="router.push({ path: `/game`, query: { id: g._id } })"
|
||||
:class="
|
||||
authStore.selfUser.userId == g.owner.userId ? 'own_game' : ''
|
||||
"
|
||||
>
|
||||
<template
|
||||
v-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>
|
||||
</ClientOnly>
|
||||
</main>
|
||||
|
||||
<div class="overlay" v-if="erika_alive && !config.public.devMode">
|
||||
<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>
|
||||
</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: grid;
|
||||
grid-template-rows: 100px 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
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);
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 100px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.create-game-zone,
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
header {
|
||||
flex-direction: row;
|
||||
backdrop-filter: blur(50px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.create-game-zone {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.create-game-zone button {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.games-list-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.games-list-zone button {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
padding: 20px 24px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.games-list-zone button.own_game {
|
||||
border: solid yellow 5px;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
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;
|
||||
}
|
||||
|
||||
.overlay-image {
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.erika-zone:hover .overlay-button .kill-erika-button {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const code = route.query.code;
|
||||
|
||||
if (!code) {
|
||||
navigateTo(config.public.discordLoginEndpoint, { external: true });
|
||||
} else {
|
||||
navigateTo("/login");
|
||||
}
|
||||
</script>
|
||||
+391
-2
@@ -1,3 +1,392 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RulesDisplay></RulesDisplay>
|
||||
</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/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>​</p>
|
||||
<p>Et quoi de mieux pour faire ça que des cartes colorées?</p>
|
||||
<div class="card-row">
|
||||
<GameCard :cardId="6" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="19" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="36" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="46" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</p>
|
||||
<hr />
|
||||
<p>​</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>​</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">
|
||||
<GameCard :cardId="undefined" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</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>​</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">
|
||||
<GameCard :cardId="56" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="59" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="58" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="57" :tilted="false"></GameCard>
|
||||
</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>​</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">
|
||||
<GameCard :cardId="56" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="52" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="43" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</p>
|
||||
<h3>Objets & actions</h3>
|
||||
<div class="card-row">
|
||||
<GameCard :cardId="56" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="52" :tilted="false"></GameCard>
|
||||
</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">
|
||||
<GameCard :cardId="43" :tilted="false"></GameCard>
|
||||
</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">
|
||||
<GameCard :cardId="49" :tilted="false"></GameCard>
|
||||
</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>​</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">
|
||||
<GameCard :cardId="6" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="19" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="36" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="46" :tilted="false"></GameCard>
|
||||
</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>​</p>
|
||||
<div class="card-row">
|
||||
<GameCard :cardId="13" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="3" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="1" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</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>​</p>
|
||||
<div class="card-row">
|
||||
<GameCard :cardId="39" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="34" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="40" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</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>​</p>
|
||||
<div class="card-row">
|
||||
<GameCard :cardId="52" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="46" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="47" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</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>​</p>
|
||||
<div class="card-row">
|
||||
<GameCard :cardId="24" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="25" :tilted="false"></GameCard>
|
||||
<GameCard :cardId="19" :tilted="false"></GameCard>
|
||||
</div>
|
||||
<p>​</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>​</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">
|
||||
<GameCard :cardId="14" :tilted="false"></GameCard>
|
||||
</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>​</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>​</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("/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, 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;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.overlay-2 {
|
||||
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;
|
||||
}
|
||||
|
||||
.overlay-image {
|
||||
position: sticky;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
const config = useRuntimeConfig();
|
||||
config.public.welcomeMessage;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ config.public.welcomeMessage }}
|
||||
<GameCard :cardId="6" :tilted="false"></GameCard>
|
||||
</div>
|
||||
</template>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user