Compare commits

...
139 Commits
Author SHA1 Message Date
legonzaur 68ffb4ba08 fix : attempt to bring back images
release-tag / release-image (push) Successful in 29s
2024-12-21 19:26:48 +01:00
legonzaur b3d535e122 attempt to fix index html
release-tag / release-image (push) Successful in 28s
2024-12-21 19:25:06 +01:00
legonzaur dd4a938632 fix :attemt to fix visuals
release-tag / release-image (push) Successful in 41s
2024-12-21 19:22:35 +01:00
legonzaur d8aa87cf97 chore : add token gen script for local dev 2024-12-21 19:12:52 +01:00
legonzaur 79e28b5006 fix: some token display errors
release-tag / release-image (push) Successful in 30s
2024-12-21 19:03:25 +01:00
legonzaur e1784dffca fix : render game configuration button only on client
release-tag / release-image (push) Successful in 47s
2024-12-21 14:56:49 +01:00
legonzaur 39e4855b91 fix : tokens incorrectly showing on opponent's board
Closes #107
2024-12-21 11:32:41 +01:00
legonzaur c30d23e45f feat : show winning players on game end
related to #71
2024-12-21 11:23:18 +01:00
legonzaur c3bb7d555c chore: formatting 2024-12-21 11:21:57 +01:00
legonzaur d0ebd3746e fix : sacrifice tokens on opponent discard 2024-12-21 11:21:46 +01:00
legonzaur f0387cebc8 chore: update nuxt 2024-12-21 11:21:34 +01:00
legonzaur a330806afe feat: redirect to lobby when game ends 2024-12-21 00:12:40 +01:00
legonzaur 25672130cd feat: logout button 2024-12-20 21:56:37 +01:00
legonzaur a6490fc180 Feat : add more tokens descriptions
release-tag / release-image (push) Successful in 30s
2024-08-27 14:59:48 +02:00
legonzaur 19bed27f78 Fix : token compilation
release-tag / release-image (push) Successful in 30s
2024-08-27 14:05:54 +02:00
legonzaur 29cd28953d Feat : add surrender button
release-tag / release-image (push) Successful in 51s
2024-08-27 13:08:15 +02:00
legonzaur b5d89d4fe9 Feat : add game deletion 2024-08-27 12:05:17 +02:00
legonzaur 0fb4877266 Feat : add Leave, kick and game configuration 2024-08-27 11:51:31 +02:00
legonzaur ddcb6c6849 chore/cleanup : move game handlers into netcode directory 2024-08-06 14:41:22 +02:00
legonzaur afebf1d9de chore : fix ide error 2024-08-06 14:28:16 +02:00
legonzaur 4e8a619cb9 fix : dockerfile
release-tag / release-image (push) Successful in 30s
2024-08-06 13:41:06 +02:00
legonzaur cb74d78928 fix : update dockerfile
release-tag / release-image (push) Failing after 11s
2024-08-06 13:38:40 +02:00
legonzaur a3ff9596d7 ci : add tokens to dockerfile
release-tag / release-image (push) Failing after 10s
2024-08-06 13:36:00 +02:00
legonzaur d18a2ab4f2 feat : token visuals draft 2024-08-06 13:30:43 +02:00
legonzaur 517f1bef5d chore: optimize loading by using defineComponent instead of defineAsyncComponent 2024-08-06 11:48:21 +02:00
legonzaur 77724d5539 ci : remove public directory
release-tag / release-image (push) Successful in 30s
2024-08-04 11:39:41 +02:00
legonzaur 21c41bb9fd ci : fix image directory
release-tag / release-image (push) Successful in 28s
2024-08-04 11:36:52 +02:00
legonzaur bb625b1fee ci : copy pngs into production stage
release-tag / release-image (push) Successful in 29s
2024-08-04 11:35:20 +02:00
legonzaur fd3cf0ba23 ci : fix image not being copied
release-tag / release-image (push) Successful in 28s
2024-08-04 11:33:10 +02:00
legonzaur 4ec29cf811 fix : champion polygon
release-tag / release-image (push) Successful in 29s
2024-08-04 11:21:41 +02:00
legonzaur b92eb4b901 fix : unify font for champion preview 2024-08-04 10:29:25 +02:00
legonzaur e36c916e0b feat : add health point to champion preview 2024-08-04 10:25:39 +02:00
legonzaur ca8e38624e fix : focused player card zone clip 2024-08-03 21:56:14 +02:00
legonzaur b00fffd314 feat : champion visualization, first draft
release-tag / release-image (push) Successful in 39s
2024-08-03 20:03:11 +02:00
legonzaur 11dd342b8e fix : remove damage and make discard buttons when not relevant 2024-08-03 17:50:58 +02:00
legonzaur 9e1af6f3df fix : focus active player automatically on game load 2024-08-03 17:50:33 +02:00
legonzaur 5e992122d8 fix : various css and layout fixes 2024-08-03 17:50:12 +02:00
legonzaur aa58e6c4f8 fix : zoom on hand and market 2024-08-03 17:36:16 +02:00
legonzaur 1a05229d0b chore : add commands to fetch images for dev 2024-08-03 17:03:56 +02:00
legonzaur 62d7346617 Chore : remove cards images from repo 2024-08-03 17:03:36 +02:00
legonzaur 9708abbf81 Fix : remove scroll bars when focused player changes 2024-08-03 16:30:02 +02:00
legonzaur 8f300394ab fix : implement game start event 2024-08-03 15:30:41 +02:00
legonzaur 2c986fbd97 Fix : remove authstore persistence 2024-08-03 15:11:32 +02:00
legonzaur 3a88d1c107 ci : create svgs at build time 2024-08-02 21:20:49 +02:00
legonzaur 7690292567 Chore : remove svgs from repository 2024-08-02 21:00:14 +02:00
legonzaur 5623f7c91f Feat : update assets
release-tag / release-image (push) Successful in 31s
2024-08-02 17:59:10 +02:00
legonzaur 08fb23e729 Fix : prevent fetching back data on build
release-tag / release-image (push) Successful in 29s
2024-08-02 09:08:59 +02:00
legonzaur 2876f7b91e Feat: better discard overlay
release-tag / release-image (push) Successful in 28s
2024-08-01 22:53:30 +02:00
legonzaur 775712371d Merge remote-tracking branch 'origin/main' into feat/netcode-rework 2024-08-01 20:08:32 +02:00
legonzaur 6edadc3ef3 Feat : add return to lobby button 2024-08-01 20:06:34 +02:00
legonzaur 7713faa4d0 Feat : add login button 2024-08-01 19:48:15 +02:00
legonzaur f9197d470e Feat : settings dialog animation 2024-08-01 18:28:27 +02:00
legonzaur de38416bcd Chore : cleanup 2 2024-08-01 17:38:01 +02:00
legonzaur 23e5f9f432 Chore : components cleanup 2024-08-01 17:31:09 +02:00
legonzaur 240ab7e1d4 Fix : add login page 2024-08-01 15:54:39 +02:00
legonzaur c68446bf25 Ci : was it this simple
release-tag / release-image (push) Successful in 31s
2024-08-01 12:43:20 +02:00
legonzaur 63519429e6 CI : fuckit manual test
release-tag / release-image (push) Successful in 30s
2024-08-01 12:33:52 +02:00
legonzaur 5282ca3c4a Ci : update build yml
release-tag / release-image (push) Successful in 30s
2024-08-01 12:31:19 +02:00
legonzaur f5e4b10d1b Ci : fix build
release-tag / release-image (push) Successful in 31s
2024-08-01 12:23:25 +02:00
legonzaur efebc79d35 Ci : dockerfile ARG NUXT_PUBLIC_WELCOME_MESSAGE
release-tag / release-image (push) Successful in 32s
2024-08-01 12:19:43 +02:00
legonzaur ad324a9a97 CI : switch to args instead of secrets
release-tag / release-image (push) Successful in 28s
2024-08-01 12:06:52 +02:00
legonzaur 4145cacf1a Chore : add test page 2024-08-01 12:05:27 +02:00
legonzaur 05130ad97e Ci : add more tags
release-tag / release-image (push) Successful in 33s
2024-08-01 10:10:23 +02:00
legonzaur e905731bc2 Ci : fix repo name in tag
release-tag / release-image (push) Successful in 34s
2024-08-01 10:01:40 +02:00
legonzaur 9638bcf1dd Merge branch 'main' into feat/netcode-rework
release-tag / release-image (push) Failing after 11s
2024-08-01 09:57:11 +02:00
legonzaur 5ce9770e8c remove build on push 2024-07-31 22:23:23 +02:00
legonzaur 174eacc417 Chore : add 404 page
release-tag / release-image (push) Successful in 31s
2024-07-31 21:44:56 +02:00
legonzaur f68304cf22 Fix : switch to localStorage for persistent session data
release-tag / release-image (push) Successful in 32s
2024-07-31 19:50:05 +02:00
legonzaur 8dbb64a540 aaaaaa
release-tag / release-image (push) Successful in 32s
2024-07-31 19:25:22 +02:00
legonzaur c2674be596 fuckit ci env not working
release-tag / release-image (push) Successful in 33s
2024-07-31 19:22:55 +02:00
legonzaur 94613b2d20 Ci test
release-tag / release-image (push) Successful in 32s
2024-07-31 19:20:53 +02:00
legonzaur c0ce9b4b33 Chore : remove useless declaration from Dockerfile
release-tag / release-image (push) Successful in 1m52s
2024-07-31 16:48:54 +02:00
legonzaur 4768a09405 Chore : Update build configuration
release-tag / release-image (push) Successful in 29s
2024-07-31 16:00:42 +02:00
legonzaur 2e8046f81c Feat : add fast animations option, move netcode handlers inside dict
release-tag / release-image (push) Successful in 29s
2024-07-31 11:11:19 +02:00
legonzaur c1bbf7dce4 Feat : add toggle to disable dragging 2024-07-31 11:10:51 +02:00
legonzaur 302890a4ac Fix : change route param to query arg in create game function 2024-07-31 10:31:51 +02:00
legonzaur 0669619543 Chore : attempt to reduce loading times
release-tag / release-image (push) Successful in 51s
2024-07-31 10:20:48 +02:00
legonzaur cfbaeca52b Feat : EXPERIMENTAL : add SSR
release-tag / release-image (push) Successful in 36s
2024-07-21 19:01:07 +02:00
legonzaur ba48570833 Feat : Allow speeding up animation speed even further
release-tag / release-image (push) Successful in 32s
2024-07-16 23:42:32 +02:00
Erzangel ff182b66ae Merge branch 'feat/netcode-rework' of https://git.legonzaur.fr/heronfief/front into feat/netcode-rework
release-tag / release-image (push) Successful in 28s
2024-07-16 23:27:28 +02:00
Erzangel ad6af3724d feat(assets): CARDS fr fr 2024-07-16 23:27:23 +02:00
legonzaur 44b840e2bc Fix : allow sacrifice on discard
release-tag / release-image (push) Successful in 29s
2024-07-16 18:14:48 +02:00
legonzaur 7a338fe5fc Fix : sacrifice on Discard
release-tag / release-image (push) Successful in 28s
2024-07-15 18:07:40 +02:00
legonzaur c48363e9c3 Fix : add stack next card token to shop 2024-07-15 17:55:06 +02:00
legonzaur 90dfe57a49 Fix : add missing slash on routes
release-tag / release-image (push) Successful in 27s
2024-07-14 17:05:11 +02:00
legonzaur 0f771b12d5 Fix : game page not using correct endpoint
release-tag / release-image (push) Successful in 28s
2024-07-14 16:57:24 +02:00
legonzaur baf2fc27a9 Fix : allow correct api endpoint to be set
release-tag / release-image (push) Successful in 28s
2024-07-14 16:46:44 +02:00
legonzaur 3330704670 Chore : force to use env vars
release-tag / release-image (push) Successful in 32s
2024-07-14 16:43:14 +02:00
legonzaur 8d4ae03519 Merge branch 'feat/netcode-rework' of git.legonzaur.fr:heronfief/front into feat/netcode-rework
release-tag / release-image (push) Successful in 48s
2024-07-14 16:24:25 +02:00
legonzaur 7fd07b5a6a Chore : add .env.example 2024-07-14 16:24:23 +02:00
legonzaur 3e3284d8c4 Update .gitea/workflows/build.yml
release-tag / release-image (push) Successful in 1m39s
2024-07-14 14:12:38 +00:00
legonzaur df9e7a2e13 Feat : add discard view 2024-07-14 15:54:22 +02:00
legonzaur 8dc263ab50 Chore : various fixes 2024-07-14 14:49:05 +02:00
legonzaur fde462166c Fix : fuckU markers not showing 2024-07-14 00:19:24 +02:00
legonzaur 2e201fa14e Feat : allow damage champion 2024-07-14 00:19:16 +02:00
legonzaur 3c5b30be9e Feat : add most token effects 2024-07-14 00:05:55 +02:00
legonzaur b1c9644941 Feat : add ability to use tokens 2024-07-13 21:37:55 +02:00
legonzaur 2709bb799c Feat : add prepare token 2024-07-13 16:49:22 +02:00
legonzaur 6bef7778e8 Feat : move card and keep position on array reorder 2024-07-08 11:22:29 +02:00
legonzaur 6cdd846d3f Feat : add card drag 2024-07-08 10:46:44 +02:00
legonzaur 9e985ad471 feat(img): ooooh i doubt you could ever imagine 2024-07-07 22:01:18 +02:00
legonzaur 50ea1d456f Feat : add buy gem action 2024-07-07 21:21:54 +02:00
legonzaur 3eba056253 Feat : implement per effects 2024-07-07 18:26:19 +02:00
Erzangel 9ce64ab591 Merge branch 'feat/netcode-rework' of https://git.legonzaur.fr/heronfief/front into feat/netcode-rework 2024-07-07 14:46:21 +02:00
Erzangel bceae83341 feat: 1st ver of new sounds for cards 2024-07-07 14:45:55 +02:00
legonzaur db7959d7f1 Feat : add tokens, handle mutex effects 2024-07-07 10:45:55 +02:00
legonzaur 12d6de5074 Fix : add delay to lockCard 2024-07-07 00:39:40 +02:00
legonzaur 6112a7d6ba Feat: effect used style 2024-07-07 00:28:15 +02:00
legonzaur 0facc4694f Add some additional card images 2024-07-07 00:27:17 +02:00
legonzaur b3df241313 Feat : allow buying cards 2024-07-05 11:26:41 +02:00
legonzaur 694e8429d1 Fix : add background z index 2024-07-05 00:10:37 +02:00
legonzaur 22d1d804f9 Feat : add damage netcode 2024-07-05 00:10:26 +02:00
legonzaur b1a874aaeb Fix : switch to state based shuffle animation 2024-07-04 22:07:48 +02:00
legonzaur 25cf8a55e9 Feat : various fixes, sounds and multiplayer additions 2024-07-03 23:27:10 +02:00
legonzaur 866a537924 feat: card lock 2024-07-03 00:04:23 +02:00
legonzaur d5dd147fba chore: move public/image to public/img 2024-07-03 00:03:58 +02:00
legonzaur 221f96860f Feat : clickable effect icons 2024-07-02 11:45:12 +02:00
legonzaur 6db8410ccc Add first svg cards 2024-07-01 12:52:13 +02:00
legonzaur 28bc888cb1 Feat : Beta animations 2024-06-30 14:51:06 +02:00
legonzaur 1fbe9a57ee Feat : add local reference to fbm.glsl 2024-06-30 14:50:58 +02:00
legonzaur 6cc91d58ce Feat : end turn 2024-06-27 19:40:29 +02:00
legonzaur 0a8313b69d Chore : add format on save 2024-06-27 18:40:34 +02:00
legonzaur 90a636b58a remove interface submodule 2024-06-27 18:40:23 +02:00
legonzaur a22c6e1352 Feat : card distribute 2024-06-13 22:39:46 -04:00
legonzaur 4a56f3ed62 Wip : Lobby 2024-06-13 10:00:29 -04:00
legonzaur 8a3e2babc9 Feat : lobby 2024-06-13 09:46:40 -04:00
legonzaur 7c98da1df8 WIP 2024-06-01 18:35:10 +02:00
legonzaur c0e02defa8 Merge branch 'feat/panels-visuals' into feat/netcode-rework 2024-05-30 19:28:30 +02:00
legonzaur 60ab679756 Merge branch 'main' into feat/netcode-rework 2024-05-30 19:25:14 +02:00
legonzaur 83fe97a427 Merge branch 'main' into feat/panels-visuals 2024-05-30 00:23:05 +02:00
legonzaur 4d66dea14c WIP : better layout 2024-05-30 00:21:39 +02:00
legonzaur 6ae1ce701d Feat : move code into pages for routing 2024-05-30 00:21:24 +02:00
legonzaur 7d7f22a82c Fix : make player image not draggable 2024-05-30 00:20:34 +02:00
legonzaur aa8332cb20 Feat : use an svg for the fuck icon 2024-05-30 00:20:16 +02:00
legonzaur aa610a58a5 Feat : update shader 2024-05-30 00:19:50 +02:00
legonzaur 851c9a5190 WIP 2024-05-29 16:46:58 +02:00
legonzaur f8205f374a add interfaces 2024-05-26 15:23:21 +02:00
legonzaur 3df2aee04c Feat : remove old netcode 2024-05-26 15:02:04 +02:00
legonzaur dd09ab3c8c WIP 2024-05-22 09:22:32 +02:00
92 changed files with 10078 additions and 8284 deletions
+3
View File
@@ -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
+5
View File
@@ -22,3 +22,8 @@ logs
.env
.env.*
!.env.example
assets/images/*
public/img/skins
public/img/icon
public/img/tokens
+34
View File
@@ -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"
]
}
]
}
+3 -1
View File
@@ -3,5 +3,7 @@
"**/.nuxt/": true,
"**/node_modules": true
},
"vue.server.hybridMode": true
"vue.server.hybridMode": true,
"editor.formatOnSave": true,
"javascript.updateImportsOnFileMove.enabled": "never"
}
+26 -10
View File
@@ -1,16 +1,32 @@
FROM node:lts-alpine as build-stage
FROM node:lts-alpine AS install-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm ci
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 card-svg-stage
RUN python3 -u ./src/generate_cards_mvp.py
FROM git.legonzaur.fr/heronfief/tokens-gen:main AS token-svg-stage
RUN python3 -u ./src/generate_tokens_mvp.py
FROM install-stage AS build-stage
COPY --from=card-svg-stage /app/output/heron ./assets/images/cards/heron-svg
COPY --from=card-svg-stage /app/img/skins ./public/img
COPY --from=token-svg-stage /app/output/heron ./assets/images/tokens
COPY --from=token-svg-stage /app/img/icon ./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=card-svg-stage /app/img/skins /usr/share/nginx/html/img/skins
COPY --from=token-svg-stage /app/img/icon /usr/share/nginx/html/img/icon
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
+65 -84
View File
@@ -1,101 +1,40 @@
<script setup lang="ts">
import { compileCards, compileTokens } from "./services/loadCards";
import { getCurrentInstance } from "vue";
// import goStore, { oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
import goStore, { 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();
await compileTokens();
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;
}
@@ -105,6 +44,48 @@ 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;
}
button.yellow {
background-color: rgba(255, 165, 0, 0.7);
}
button.red {
background-color: rgba(255, 0, 0, 0.7);
}
button:hover:enabled {
background-color: #45a049;
}
button.yellow:hover {
background-color: rgba(255, 165, 0, 0.8);
}
button.red:hover {
background-color: rgba(255, 0, 0, 0.8);
}
#__nuxt {
height: 100%;
user-select: none;
+3
View File
@@ -0,0 +1,3 @@
<html>
I LOVE LOADING SCREENS
</html>
+46
View File
@@ -0,0 +1,46 @@
{
"prepare":{
"title":"Prepare",
"description":"Repeat a champion's effects"
},
"stun":{
"title":"Stun",
"description":"Stun (kill) an ennemy's champion"
},
"sacrifice":{
"title": "Sacrifice",
"description": "Destroy a card, forever"
},
"discard":{
"title":"Discard",
"description":"Oops ! You must put a card in your discard..."
},
"make_discard":{
"title": "Make Discard",
"description": "Give a fuck to your opponent"
},
"restack_discarded_champion":{
"title":"Restack Discarded Champion",
"description":"Take a champion from your discard and put it on top of your stack"
},
"restack_discarded_card":{
"title":"Restack Discarded Card",
"description":"Take a card from your discard and put it on top of your stack"
},
"stack_next_action_bought":{
"title":"Stack Next Action Bought",
"description":"When you buy an action, you may put it on top of your stack"
},
"stack_next_card_bought":{
"title":"Stack Next Card Bought",
"description":"When you buy a card, you may put it on top of your stack"
},
"play_next_card_bought":{
"title":"Play Next Card Bought",
"description":"When you buy a card, you may put it in play"
},
"draw_and_discard":{
"title":"Draw And Discard",
"description":"You may draw a card, then discard a card"
}
}
+3 -3
View File
@@ -23,12 +23,12 @@ void main(){
q=q*.5+.5;
vec2 r=vec2(0.);
r.x=fbm(st+1.*q+vec2(1.7,9.2)+.00515*u_time);
r.y=fbm(st+1.*q+vec2(8.3,2.8)+.0126*u_time);
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.);
+217 -188
View File
@@ -1,214 +1,243 @@
import noise from "~/assets/shaders/noise.frag?raw"
import noise from "~/assets/shaders/noise.frag?raw";
import resolveLygia from "~/assets/workers/resolve-lygia.esm"
import resolveLygia from "~/assets/workers/resolve-lygia.esm";
let interval: number
let timeout: number
let interval: number;
let timeout: number;
const shaderOptions = { quality: 1, framerate: 1000 / 60 }
const screen = { width: 0, height: 0, devicePixelRatio: 0 }
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]
const _colorStart = [0, 0, 0];
const _colorEnd = [0, 0, 0];
let fragmentShader = resolveLygia(noise);
let canvas: OffscreenCanvas
let canvas: OffscreenCanvas;
let render: (time: number) => void
let render: (time: number) => void;
let must_resize = false
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 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 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;
}
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
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);
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)
}
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)
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.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") {
console.log("killShader")
self.clearInterval(interval)
self.clearTimeout(timeout)
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;
}
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
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
}
}
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");
}
+7 -3
View File
@@ -1,4 +1,7 @@
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();
@@ -8,6 +11,8 @@ function getFile(url) {
return "";
}
const lygiaURL = "https://lygia.xyz"
function resolveLygia(lines) {
if ( !Array.isArray(lines) ) {
lines = lines.split(/\r?\n/);
@@ -18,14 +23,13 @@ function resolveLygia(lines) {
const line_trim = line.trim();
if (line_trim.startsWith('#include \"lygia') ) {
let include_url = line_trim.substring(15);
include_url = "https://lygia.xyz" + include_url.replace(/\"|\;|\s/g,'');
include_url = lygiaURL + include_url.replace(/\"|\;|\s/g,'');
src += getFile(include_url) + '\n';
}
else {
src += line + '\n';
}
});
return src;
}
@@ -39,7 +43,7 @@ async function resolveLygiaAsync(lines) {
const line_trim = line.trim();
if (line_trim.startsWith('#include "lygia')) {
let include_url = line_trim.substring(15);
include_url = "https://lygia.xyz" + include_url.replace(/\"|\;|\s/g, "");
include_url = lygiaURL + include_url.replace(/\"|\;|\s/g, "");
return fetch(include_url).then((res) => res.text());
}
else
+123 -80
View File
@@ -1,133 +1,176 @@
<script setup lang="ts">
const settingsStore = useSettingsStore()
const settingsStore = useSettingsStore();
import gsap from 'gsap';
import ShaderWorker from '~/assets/workers/ShaderWorker?worker'
import goStore from '~/netcode';
const worker = new ShaderWorker()
import gsap from "gsap";
import ShaderWorker from "~/assets/workers/ShaderWorker?worker";
const canvas = ref<HTMLCanvasElement | null>(null)
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null)
const emit = defineEmits<{
ready: [];
}>();
let worker: Worker;
const canvas = ref<HTMLCanvasElement | null>(null);
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null);
export interface Props {
colorStart?: [number, number, number]
colorEnd?: [number, number, number]
isTurn?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
colorStart: () => [0, .3, 0],
colorEnd: () => [0, 1, 0]
})
isTurn: () => false,
});
const _colorStart = reactive(props.colorStart)
const _colorEnd = reactive(props.colorEnd)
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() {
if (!canvas.value) { return }
const offscreen = canvas.value?.transferControlToOffscreen()
worker.postMessage({
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])
screen: {
height: window.innerHeight * devicePixelRatio,
width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
},
[offscreen]
);
if (settingsStore.shaderFramerate == 0) {
killShader()
killShader();
}
resizeShader()
resizeShader();
worker.onmessage = (e) => {
if (e.data == "ready") {
resolve();
}
};
});
}
function recolorShader() {
worker.postMessage({
command: 'recolorShader',
colorStart: [..._colorStart],
colorEnd: [..._colorEnd]
})
worker.postMessage({
command: "recolorShader",
colorStart: [..._colorStart],
colorEnd: [..._colorEnd],
});
}
function resizeShader() {
worker.postMessage({
command: 'resizeShader',
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
worker.postMessage({
command: "resizeShader",
screen: {
height: window.innerHeight * devicePixelRatio,
width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
}
function killShader() {
worker.postMessage({
command: 'killShader'
})
worker.postMessage({
command: "killShader",
});
}
function resumeShader() {
worker.postMessage({
command: 'resumeShader',
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
worker.postMessage({
command: "resumeShader",
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
}
watch(() => [settingsStore.shaderFramerate], () => {
watch(
() => [settingsStore.shaderFramerate],
() => {
if (settingsStore.shaderFramerate == 0) {
killShader()
return
killShader();
return;
}
resumeShader()
resumeShader();
}
);
})
watch(
() => [settingsStore.shaderQuality],
() => {
resizeShader();
}
);
watch(() => [settingsStore.shaderQuality], () => {
resizeShader()
})
watch(() => [goStore.current_game?.current_turn], () => {
if (goStore.current_game?.current_turn != goStore.self?.team) {
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: .2, 2: .3 })
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 1 })
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: .3, 2: 0 })
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 0 })
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 });
}
}, { immediate: true })
}
);
watch([_colorEnd, _colorStart], () => {
recolorShader()
})
recolorShader();
});
onMounted(async () => {
nextTick(() => {
recolorShader()
initShader()
window.addEventListener("resize", resizeShader, true)
})
})
onMounted(() => {
worker = new ShaderWorker();
recolorShader();
initShader().then(() => {
emit("ready");
});
window.addEventListener("resize", resizeShader, true);
});
onUnmounted(() => {
window.removeEventListener("resize", resizeShader)
killShader()
})
window.removeEventListener("resize", resizeShader);
killShader();
});
</script>
<template>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
height="270">test</canvas>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
height="270"></canvas>
</template>
<style scoped>
#background {
left: 0;
position: absolute;
height: 100%;
width: 100%;
image-rendering: smooth;
left: 0;
position: fixed;
height: 100%;
width: 100%;
z-index: -999999;
}
#background.pixelated {
image-rendering: pixelated;
image-rendering: pixelated;
}
</style>
</style>
-51
View File
@@ -1,51 +0,0 @@
<script setup lang="ts">
import goStore from '~/netcode';
export interface Props {
card_id: number | undefined
}
const settingsStore = useSettingsStore()
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/${settingsStore.cardStyle}/` + 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>
-167
View File
@@ -1,167 +0,0 @@
<script setup lang="ts">
import goStore from '~/netcode';
import { useSettingsStore } from '~/stores/settingsStore';
export interface Props {
card_id: number | undefined
locked?: boolean
wrapped?: boolean
tilted?: boolean
}
const props = withDefaults(defineProps<Props>(), {
wrapped: () => false,
tilted: () => true,
locked: () => true
})
const emit = defineEmits(["click"])
const settingsStore = useSettingsStore()
const childCount = ref<number>(0)
const card = ref<HTMLElement>()
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 })
onMounted(() => {
card.value?.style.setProperty("--random-seed", -Math.random() * 10 + 's')
})
</script>
<template>
<div ref="card" class="card" :class="{ active: childCount > 0, wrapped, unlocked: !props.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/${settingsStore.cardStyle}/` + 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 {
--max-height: 250px;
--margin-bottom: calc(var(--max-height) * 0.05);
max-width: 256px;
position: relative;
display: flex;
margin-bottom: 0px;
margin-top: var(--margin-bottom);
transition: filter .125s cubic-bezier(0.075, 0.82, 0.165, 1), rotate 0.075s cubic-bezier(1, 1.81, .59, 1.42);
isolation: isolate;
pointer-events: all;
max-height: var(--max-height);
}
.card.active {
cursor: pointer
}
.card_image {
max-height: var(--max-height);
z-index: 1;
}
.card_image.outlined {
outline: 2px solid red;
outline-offset: -2px;
}
@keyframes unlocked-float {
from {
transform: none;
filter: drop-shadow(calc(var(--margin-bottom)*.3) var(--margin-bottom) 1px rgba(0, 0, 0, .60));
}
to {
transform: translateY(calc(var(--margin-bottom)*-.25));
filter: drop-shadow(calc(var(--margin-bottom)*.3) calc(var(--margin-bottom)*1.5) 1px rgba(0, 0, 0, .60));
}
}
@keyframes unlocked-rotate {
from {
rotate: -2deg;
}
to {
rotate: 2deg;
}
}
.card.unlocked {
margin-top: 0px;
margin-bottom: var(--margin-bottom);
filter: drop-shadow(calc(var(--margin-bottom)*.3) var(--margin-bottom) 1px rgba(0, 0, 0, .60));
animation: 2s infinite alternate ease-in-out unlocked-float, 3.1s infinite alternate ease-in-out unlocked-rotate;
animation-delay: var(--random-seed);
animation-play-state: running;
}
.card:not(.unlocked):hover {
animation-play-state: paused;
rotate: none !important;
}
.card.wrapped {
max-width: 10px;
}
#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(1, 1.81, .59, 1.42);
}
.card.active:hover #contextMenuButtons {
transform: translateY(100%);
}
</style>
<style>
#contextMenuButtons>* {
flex: 1;
border-radius: 12px;
min-height: 50px;
}
</style>
-56
View File
@@ -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>
-144
View File
@@ -1,144 +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 showOptions = 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">
<AnimatedBackground></AnimatedBackground>
<div id="market">
<MarketCards></MarketCards>
</div>
<div id="player_list_container">
<div id="player_list">
<PlayerListElement v-for="p in goStore.current_game?.players" :player="p">
</PlayerListElement>
</div>
</div>
<div id="current_player">
<template v-for="p in current_players">
<!-- <AnimatedBackground :color-start="goStore.self == p ? [.0, .3, .3] : undefined"
:color-end="goStore.self == p ? [.0, 1, 1] : undefined" /> -->
<PlayerSlot :player="p"></PlayerSlot>
</template>
</div>
</div>
<ResultDialog :isVisible="showResults" :won="isWinner" :players="goStore.current_game?.players"
@close="showResults = false">
</ResultDialog>
<GlobalOverlay></GlobalOverlay>
<SelfView></SelfView>
</template>
<style scoped>
#game_board {
display: grid;
grid-template-columns: 1fr 450px;
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;
/* background: url('/images/bg-darkgrass.png'); */
}
#current_player {
grid-area: current_player;
}
#market {
display: flex;
flex-direction: row;
justify-content: center;
}
#player_list_container {
grid-area: player_list;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
/* background-image: url("/images/bg-darkgrass.png"); */
}
#player_list {
min-height: 90%;
background: rgba(0, 0, 0, 0.35);
border: 5px solid #000;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60));
margin: 10px;
backdrop-filter: blur(10px) contrast(90%) brightness(165%);
background: rgba(26, 70, 91, 0.7);
}
</style>
<style>
#player_list .card {
--max-height: 90px;
}
</style>
-31
View File
@@ -1,31 +0,0 @@
<script setup lang="ts">
import goStore from '~/netcode';
const displayOptions = ref(false)
</script>
<template>
<div :class="`card_overlay`" ref="card">
<img class="settings_button" src="/images/settings.png" alt="settings" @click="displayOptions = true"/>
</div>
<OptionsDialog :isVisible="displayOptions" @close="displayOptions = false" />
</template>
<style scoped>
.card_overlay {
/* display: none; */
position: fixed;
z-index: 999;
display: flex;
pointer-events: none;
/* top: left: */
top: 0;
}
.settings_button {
pointer-events: all;
width: 30px;
padding: 5px;
}
</style>
-244
View File
@@ -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>
-106
View File
@@ -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>
-79
View File
@@ -1,79 +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 {
width: min-content;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 2;
position: relative;
gap: 2px;
margin: 10px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60));
backdrop-filter: blur(5px) brightness(80%);
background: rgba(155, 62, 62, 0.46);
}
.separator {
width: 50px;
}
</style>
-130
View File
@@ -1,130 +0,0 @@
<script setup lang="ts">
const settingsStore = useSettingsStore()
export interface Props {
isVisible: boolean;
}
const _devicePixelRatio = computed(() => devicePixelRatio)
const _window = computed(() => window)
const props = withDefaults(defineProps<Props>(), {
})
const emit = defineEmits(['close'])
const close = () => {
emit('close')
}
</script>
<template>
<div v-if="isVisible" class="dialog-overlay" @click="close">
<div class="dialog-box" @click.stop>
<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-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>{{ 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>
</div>
</template>
<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
display: grid;
}
.dialog-box {
background-image: url('../images/bg-grass.png');
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%;
}
.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>
-40
View File
@@ -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>
View File
-159
View File
@@ -1,159 +0,0 @@
<script setup lang="ts">
import goStore from '@/netcode';
import type { Player } from "~/netcode/Player";
const settingsStore = useSettingsStore()
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' : ''}`">
<!-- <AnimatedBackground v-if="goStore.self === props.player" :color-start="[.0, .3, .3]" :color-end="[.0, 1, 1]" /> -->
<div class="button" id="player_list_buttons">
<div class="main_button" @click="show('main')">
<div class="tooltip">Board</div>
<img :src="`/cards/${settingsStore.cardStyle}/${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/${settingsStore.cardStyle}/${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: hidden;
position: absolute;
transition: .1s transform;
background: white;
right: -0px;
border-top-right-radius: 1em;
border-bottom-right-radius: 1em;
padding-right: 1em;
transition-property: visibility, transform;
transition-duration: 0.5s, 0.1s;
transition-timing-function: ease-in-out, ease-in-out;
}
.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;
display: block;
transform: translateX(100%);
transition-duration: 0, 0.1s;
}
.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: 5px solid black;
min-height: 200px;
position: relative;
}
.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>
-567
View File
@@ -1,567 +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
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" :class="{ turn: isPlayerTurn, discard_unwrapped, self: isSelf }">
<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" class="end_turn_button turn_icon" :class="{ hidden: !isSelfTurn }"
@click="checkEndTurn">END TURN</button>
<div class="warning"
:class="{ 'warning-fire-gem': warningText == 'P\'tite Fire gem?', 'visible': warning }">
/!\<br>{{ warningText }}
</div>
</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%;
}
#end_turn.hidden {
visibility: hidden
}
.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;
min-width: 200px;
}
#player_banner.big {
padding: 0;
left: 50%;
transform: translateX(-50%);
max-width: min-content;
border: 5px solid black;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60));
backdrop-filter: blur(5px) brightness(80%) grayscale(80%);
}
#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>
-65
View File
@@ -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>
-319
View File
@@ -1,319 +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>&#8203;</p>
<p>Et quoi de mieux pour faire ça que des cartes colorées?</p>
<div class="card-row">
<CardPreview :card_id="6" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
<CardPreview :card_id="36" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<hr>
<p>&#8203;</p>
<h2>Objectif</h2>
<p>L'unique objectif du jeu est d'être le dernier en lice. Pour y arriver, il faut descendre les Points
de Vie de vos adversaires à 0 en partant de 50.</p>
<p>
Le moyen principal d'y arriver, c'est de faire des <span style="color: red">Dégats</span> <img
src="/images/damage.png" class="icon_img" draggable="false" />. Tout le reste des mécaniques du
jeu sont là pour vous faciliter la tâche,
ou pour mettre des bâtons dans les roues de vos adversaires.
</p>
<p>&#8203;</p>
<hr>
<h2>Deckbuilding</h2>
<p>Comme c'est un jeu de "deckbuilding", on va expliquer ce qu'est le "deck" et ce qui nous permet de le
"build".</p>
<div class="card-row">
<CardPreview :card_id="undefined" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>
Le deck d'un joueur est-ce qui constitue sa pioche, qui lui permettra ensuite de constituer sa main
(la main est préparée à la fin du tour et permet de jouer le tour d'après).
</p>
<p>
Quand la pioche est vide, elle est recyclée à partir de sa défausse.
</p>
<p>
La défausse est ce qui contient les cartes déjà jouées. Mais aussi celles qui sont achetées! Elle
finit par être mélangée pour former la nouvelle pioche.
</p>
<p>&#8203;</p>
<hr>
<h2>Deck de base</h2>
<p>
Tout le monde commence avec 10 cartes divisées en 4 types:
</p>
<ul>
<li>7x Or (1)</li>
<li>1x Or (2)</li>
<li>1x Dégats (1)</li>
<li>1x Dégats (2)</li>
</ul>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="59" :tilted="false"></CardPreview>
<CardPreview :card_id="58" :tilted="false"></CardPreview>
<CardPreview :card_id="57" :tilted="false"></CardPreview>
</div>
<p>
La pioche est formée au hasard: on peut donc avoir une main de départ avec 5 or et 2 dégats, ou 3 or
et 4 dégats, ou 4 or et 3 dégats, etc.
</p>
<p>
A partir de sa main, on peut jouer des cartes. Ces cartes peuvent déclencher des effets: on peut
considérer qu'ils vont dans un "pool commun", et qu'on peut les utiliser dans
l'ordre qu'on souhaite. 1 de dégats et 2 de dégats me donnent 3 ressources de dégats que je split
comme je veux ensuite. Pareil pour l'or et pour le soin, qui sont les 3 ressources
principales du jeu.
</p>
<p>&#8203;</p>
<hr>
<h2>Types de cartes</h2>
<p>
Les cartes sont divisées en 3 catégories: objets, actions et champions.
</p>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="52" :tilted="false"></CardPreview>
<CardPreview :card_id="43" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<h3>Objets & actions</h3>
<div class="card-row">
<CardPreview :card_id="56" :tilted="false"></CardPreview>
<CardPreview :card_id="52" :tilted="false"></CardPreview>
</div>
<p>
Par simplicité, disons que les objets et les actions sont pareil: on les joue, ils font un effet
disponible durant le tour actuel, et ils sont défaussés.
</p>
<h3>Champions (& gardes)</h3>
<div class="card-row">
<CardPreview :card_id="43" :tilted="false"></CardPreview>
</div>
<p>
Les champions sont un peu plus complexes: ils restent sur le terrain, et on peut les utiliser à
chaque tour (en les mobilisant).
</p>
<p>Ils ont une défense <img src="/images/champion-shield.png" style="height: 30px" draggable="false" />
indiquée en bas à droite et ils ne sont assommés (défaussés) que si on leur fait des dégats =
défense.</p>
<p>Ces dégats doivent être faits d'un coup. Je ne peux pas faire un peu de dégats pendant un tour, et un
peu le tour d'après pour les tuer.</p>
<div class="card-row">
<CardPreview :card_id="49" :tilted="false"></CardPreview>
</div>
<p>
Par ailleurs, certains champions sont des Gardes <img src="/images/champion-shield-guard.png"
style="height: 30px" draggable="false" /> (ils ont un bouclier noir), ce qui veut dire qu'on est
obligés de les tuer pour faire des dégats à des champions non-gardes ou au joueur.
</p>
<p>
(J'en profite pour préciser qu'on peut en effet, sans garde, choisir d'attaquer le joueur
directement ou ses champions.)
Incidentellement, ça veut dire qu'avec seulement 2 de dégats, je ne peux rien faire à un joueur avec
un garde à 3 de défense.
</p>
<p>&#8203;</p>
<hr>
<h2>Building</h2>
<p>
Passons au "building": on peut acheter des cartes pour les ajouter à notre défausse qui, si vous
avez suivi, constituera la prochaine pioche.
Avec le jeu de base, <span style="color: darkgoldenrod">l'or</span> <img src="/images/gold.png"
class="icon_img" draggable="false" /> sert exclusivement à accomplir ce but: acheter des cartes.
</p>
<div class="card-row">
<CardPreview :card_id="6" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
<CardPreview :card_id="36" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
</div>
<p>
Un marché est mis à disposition de tous les joueurs, mettant à disposition 5 cartes (pas ici parce
que j'ai pas la place).
</p>
<p>
Ces cartes ont chacune un coût indiqué en haut à droite.
Il faut au moins autant d'or que ce qui est indiqué pour acheter une carte. Une carte achetée laisse
la place à une nouvelle carte directement, qu'on peut
décider d'acheter juste après (tant qu'on a l'argent).
</p>
<p>
Il ne nous reste plus qu'à comprendre ce que font les cartes du marché, puisqu'elles sont plus
complexes (et bien plus fortes) que votre deck de base.
Prenons des exemples!
</p>
<hr>
<h2>Couleurs</h2>
<p>
La première chose qui ne vous échappera pas, c'est qu'on a 4 couleurs différentes pour les cartes.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="13" :tilted="false"></CardPreview>
<CardPreview :card_id="3" :tilted="false"></CardPreview>
<CardPreview :card_id="1" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: darkgoldenrod">jaunes</span> sont les cartes Empire.</p>
<p>Elles font beaucoup de soin, et activent souvent des effets de pioche.</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="39" :tilted="false"></CardPreview>
<CardPreview :card_id="34" :tilted="false"></CardPreview>
<CardPreview :card_id="40" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: red">rouges</span> sont les cartes Necros.</p>
<p>Elles sont les seules à pouvoir sacrifier des cartes pour les sortir de son deck de façon permanente.
</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="52" :tilted="false"></CardPreview>
<CardPreview :card_id="46" :tilted="false"></CardPreview>
<CardPreview :card_id="47" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: darkgreen">vertes</span> sont les cartes Sauvages.</p>
<p>Elles font beaucoup de dégats, et forcent vos adversaires à jouer moins de cartes.</p>
<p>&#8203;</p>
<div class="card-row">
<CardPreview :card_id="24" :tilted="false"></CardPreview>
<CardPreview :card_id="25" :tilted="false"></CardPreview>
<CardPreview :card_id="19" :tilted="false"></CardPreview>
</div>
<p>&#8203;</p>
<p>Les cartes <span style="color: darkblue">bleues</span> sont les cartes Guilde.</p>
<p>Elles donnent beaucoup d'or, assomment les champions adverses et manipulent votre pioche.</p>
<p>&#8203;</p>
<hr>
<h2>Déclencheurs</h2>
<p>Les effets de cartes peuvent se déclencher pour 3 raisons différentes.</p>
<div class="card-row">
<CardPreview :card_id="14" :tilted="false"></CardPreview>
</div>
<p>Sur cette carte, on peut voir les trois en même temps.</p>
<p>La 1ère ligne s'active tout le temps.</p>
<p>La 2ème ligne est une capacité Allié: il faut une autre carte de la même couleur en jeu pour pouvoir
en profiter.</p>
<p>La 3ème ligne est un Effet Poubelle (différent du sacrifice des cartes rouges): vous pouvez retirer
cette carte de votre jeu pour bénéficier, en plus des autres effets, de l'effet marqué. Mais la
carte sera défaussée à tout jamais.</p>
<p>&#8203;</p>
<hr>
<h2>Effets</h2>
<p>Une variété d'effets peuvent être inclus dans les cartes.</p>
<p>Il y en a plein et les expliquer ici serait relou, alors je vous laisse poser la question en vocal si
vous n'en comprenez pas un.</p>
<p>&#8203;</p>
<hr>
<h2>Activer les cartes dans la version en ligne</h2>
<p>Il suffit de cliquer sur la carte que vous voulez activer dans votre main pour la "LOCK".</p>
<p>Une carte "LOCKED" est considérée en jeu, & peut activer des effets Allié pour d'autres cartes, etc.
</p>
<p>Si vous avez des choix à faire sur la carte, un menu apparaîtra pour vous laisser choisir.</p>
<p>Pour faire des dommages, il y aura un bouton "Damage" sur vos ennemis. Pareil sur leurs champions.
</p>
<p>Quand vous avez un effet "Sacrifice", vous pouvez fouiller votre défausse pour choisir la carte à
sacrifier. Vous pouvez également sacrifier une carte de la main qui n'est pas encore locked!</p>
<h2>Zoomer sur les cartes</h2>
<p>Pour zoomer sur une carte, maintenez votre clic droit.</p>
</section>
</div>
</div>
</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>
-110
View File
@@ -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>
+261
View File
@@ -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>
+119
View File
@@ -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>
+77
View File
@@ -0,0 +1,77 @@
<script setup lang="ts">
import type { Effect } from "~/netcode/interfaces";
import { tokens } from "~/services/loadCards";
import effect_locales from "~/assets/locales/effects.json";
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="" class="token_container">
<Component
:is="tokens[props.token.effect]"
v-if="props.token.effect in tokens"
/>
<span v-else>{{ props.token.effect }}</span>
<div class="token_description" v-if="props.token.effect in effect_locales">
<span
><b>{{
effect_locales[props.token.effect as keyof typeof effect_locales]
.title
}}</b></span
>
<span>{{
effect_locales[props.token.effect as keyof typeof effect_locales]
.description
}}</span>
</div>
</div>
</template>
<style>
.token_container {
margin: 15px;
/* backdrop-filter: blur(60px); */
background: rgba(201, 212, 38, 1);
border: 5px solid black;
width: min-content;
cursor: pointer;
transition: 0.2s all;
border-radius: 10px;
display: flex;
flex-direction: row;
transition: 0.1s all var(--animation-curve);
position: relative;
z-index: 50;
}
.token_container:hover {
border: 5px solid red;
transform: scale(110%);
}
.token_description {
min-width: max-content;
min-height: 45px;
position: absolute;
right: -10px;
transform: translateX(100%);
display: none;
flex-direction: column;
background: rgb(168, 175, 74);
border: 5px solid red;
border-radius: 10px;
}
.token_container:hover .token_description {
display: flex;
}
</style>
+30
View File
@@ -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>
+138
View File
@@ -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>
+254
View File
@@ -0,0 +1,254 @@
<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)) && ((wrapped && i == (cards.length - 1)) || !wrapped)
"></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>
+166
View File
@@ -0,0 +1,166 @@
<script setup lang="ts">
import { deleteGame, kickPlayer } from "~/netcode";
import type { Game } from "~/netcode/interfaces";
export interface Props {
game: Game;
}
const deletionConfirmation = ref("Delete Game");
const props = defineProps<Props>();
const isVisible = ref(false);
watch(isVisible, () => {
deletionConfirmation.value = "Delete Game";
});
function deleteButtonClick() {
if (deletionConfirmation.value == "Delete Game") {
deletionConfirmation.value = "Are you sure?";
} else {
deleteGame(props.game._id);
}
}
</script>
<template>
<button @click="isVisible = true" class="dialog-open-button yellow">
Game Configuration
</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>Extensions</h3>
</div>
<div class="dialog-header">
<span>Enable extensions</span>
</div>
<div class="dialog-body dialog-flex">
<span v-for="extension in props.game.packs"
><input type="checkbox" checked="true" disabled />{{ extension }}</span
>
</div>
<div class="dialog-header">
<h3>Players</h3>
</div>
<template v-for="team in game.teams">
<template v-for="player in team.players">
<div class="dialog-header">
<span>{{ player.user.username }}</span>
</div>
<div class="dialog-body">
<button
:disabled="game.owner._id == player.user._id"
@click="kickPlayer(game._id, player._id)"
>
Kick
</button>
</div>
</template>
</template>
<div class="dialog-header">
<h3>Danger Zone</h3>
</div>
<div class="dialog-header">
<button class="red" @click="deleteButtonClick">
{{ deletionConfirmation }}
</button>
</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-flex {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.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(124, 85, 13);
}
.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>
@@ -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,25 +27,25 @@ 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 :user="players[0].user"></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!
<img src="/images/so_back.png" alt="Une image décrivant le meme 'We are so back'" />
Vous avez perdu face à ce magnifique individu qui a très clairement
beaucoup plus de bitches que vous! Cela dit, je pense qu'il n'y a pas
de bonnes ou de mauvaises situations, juste des situations
différentes. Vous avez perdu, mais vous avez gagné en expérience!
C'est ça le plus important dans la vie, n'est-ce pas? Apprendre de ses
erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez heureux
d'avoir eu l'opportunité d'apprendre quelque chose aujourd'hui! Et
puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend plus
fort"! Alors, allez-y, soyez fort! Et n'oubliez pas, la prochaine
fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un
champion! Vous êtes le meilleur! Vous êtes le roi du monde!
<img
src="/img/so_back.png"
alt="Une image décrivant le meme 'We are so back'"
/>
</p>
</div>
</div>
</div>
@@ -68,7 +66,7 @@ const close = () => {
}
.dialog-box {
background-image: url('/images/bg-grass.png');
background-image: url("/img/bg-grass.png");
padding: 0 20px;
border-radius: 5px;
width: 800px;
@@ -84,7 +82,7 @@ const close = () => {
.dialog-box h3 {
font-size: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
width: 100%;
padding: 10px 0;
@@ -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>
+147
View File
@@ -0,0 +1,147 @@
<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(() => {
// Only check for tokens if it's current player's turn
const team = props.game.teams.find((t) => t._id == props.game.currentTurn.currentTeam);
if (team?.players.some(p => p._id == props.player._id)) {
return props.game.currentTurn.tokens.filter((t) =>
[
CardEffects.RESTACK_DISCARDED_ACTION,
CardEffects.RESTACK_DISCARDED_CARD,
CardEffects.RESTACK_DISCARDED_CHAMPION,
CardEffects.SACRIFICE,
].includes(t.effect)
)
}
return []
}
);
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>
+17
View File
@@ -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>
+43
View File
@@ -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>
+191
View File
@@ -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>
+115
View File
@@ -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>
+443
View File
@@ -0,0 +1,443 @@
<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(() => {
if (!isPlayerTurn.value) {
return props.game.currentTurn.tokens.filter((t) => t.effect == CardEffects.STUN);
}
else {
return props.game.currentTurn.tokens.filter(
(t) =>
[CardEffects.PREPARE, CardEffects.SACRIFICE].includes(t.effect)
)
}
}
);
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: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>
+74
View File
@@ -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>
+57
View File
@@ -0,0 +1,57 @@
import { useAuthStore } from "#imports";
const validPaths = ["/login", "/lobby", "/rules", "/test"];
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 == "/logout") {
// const session = useCookie("heron.session");
// session.value = null;
// return navigateTo("/lobby");
return;
}
if (validPaths.includes(to.path)) {
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");
});
-64
View File
@@ -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
}
-95
View File
@@ -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
}
-52
View File
@@ -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
}
-3
View File
@@ -1,3 +0,0 @@
export type GameObjectNetcode = {
uuid: string
}
-25
View File
@@ -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
}
-55
View File
@@ -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
}
-29
View File
@@ -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
}
-50
View File
@@ -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
}
+202
View File
@@ -0,0 +1,202 @@
import { playSound } from "~/helpers/audioHelpers";
import type { Container, Game, Player } from "./interfaces";
export function delay(delay: number) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
type handlerInput = {
game: Game;
containers: Ref<Record<string, Container>>;
settingsStore: ReturnType<typeof useSettingsStore>;
playerList: Ref<Array<Player>>;
refresh: () => Promise<void>;
focusPlayer: (player: Player) => void;
};
export const handlers: {
[key: string]: (data: any, input: handlerInput) => Promise<void>;
} = {
start: async (data: any, { game }) => {
game.started = true;
game.currentTurn = data.game.currentTurn;
},
populateContainer: async (data: any, { containers }) => {
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, { containers, settingsStore }) => {
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, { containers, settingsStore }) => {
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, settingsStore, focusPlayer }) => {
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, settingsStore }) => {
playSound("/sounds/lock.mp3");
game.currentTurn.cardsLocked.push(data.card);
await delay(settingsStore.animationSpeed);
},
destroyCard: async (data: any, { containers, settingsStore }) => {
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 }) => {
playSound("/sounds/teleport.mp3");
game.teams.push(data.team);
},
leaveTeam: async (data: any, { game }) => {
const team = game.teams.find((t) => t._id == data.team._id);
if (team == undefined) {
throw new Error("Cannot find team");
}
const playerIndex = team.players.findIndex((p) => p._id == data.playerId);
if (playerIndex == -1) {
throw new Error("Cannot find player in team");
}
team.players.splice(playerIndex, 1);
if (team.players.length === 0) {
const index = game.teams.findIndex((t) => t._id == data.team._id);
game.teams.splice(index, 1);
}
},
lockEffect: async (data: any, { game, settingsStore }) => {
game.currentTurn.lockedEffects.push(data.effect);
await delay(settingsStore.animationSpeed);
},
unlockEffect: async (data: any, { game, settingsStore }) => {
const effectIndex = game.currentTurn.lockedEffects.indexOf(data.effect);
game.currentTurn.lockedEffects.splice(effectIndex, 1);
await delay(settingsStore.animationSpeed);
},
"currentTurn.updateGold": async (data: any, { game }) => {
console.log(`adding gold amount : ` + data.operation);
game.currentTurn.gold = data.gold;
},
"currentTurn.updateDamage": async (data: any, { game }) => {
console.log(`adding damage amount : ` + data.operation);
game.currentTurn.damage = data.damage;
},
"currentTurn.addToken": async (data: any, { game, settingsStore }) => {
game.currentTurn.tokens.push(data.token);
await delay(settingsStore.animationSpeed);
},
"currentTurn.useToken": async (data: any, { game, settingsStore }) => {
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 }) => {
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, { playerList }) => {
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, { playerList }) => {
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, { playerList }) => {
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, { playerList }) => {
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, refresh }) => {
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 (data: any, { game }) => {
const winningPlayers = game.teams
.find((t) => t._id == data.winner)
?.players.map((p) => p.user.username)
.join(", ");
alert(`game ended. Winner : ${winningPlayers}`);
},
delete: async () => {
alert("game deleted");
navigateTo("/lobby");
},
};
+194 -342
View File
@@ -1,375 +1,227 @@
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 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 deleteGame(game: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(config.public.endpoint + `/games/${game}`, {
method: "DELETE",
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 joinGame(game: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(
config.public.endpoint + `/games/${game}/join`,
{
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 leaveGame(game: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(
config.public.endpoint + `/games/${game}/leave`,
{
method: "POST",
credentials: "include",
}
);
}
export async function useEffect(
effect: Effect,
target?: Effect | Card | Player | Team
export async function surrender(game: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(
config.public.endpoint + `/games/${game}/surrender`,
{
method: "POST",
credentials: "include",
}
);
}
export async function kickPlayer(game: string, playerId: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(
config.public.endpoint + `/games/${game}/kick/${playerId}`,
{
method: "POST",
credentials: "include",
}
);
}
export async function endTurn(game: string) {
const config = useRuntimeConfig();
await $fetch(config.public.endpoint + `/games/${game}/endTurn`, {
method: "POST",
credentials: "include",
});
}
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 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 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;
+113
View File
@@ -0,0 +1,113 @@
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;
packs: string[];
}
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
View File
@@ -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 lobby/index.html;
try_files $uri $uri/ /lobby/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 -9
View File
@@ -1,15 +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',
'@pinia-plugin-persistedstate/nuxt',
],
})
modules: ["@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"],
vue: {
runtimeCompiler: true,
},
nitro: {
prerender: {
routes: ["/rules", "/login", "/game", "/lobby"],
crawlLinks: true,
},
},
});
+4284 -4721
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -3,6 +3,11 @@
"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",
"tokens-gen": "sh -c 'docker run --rm --pull=always -v ./assets/images/tokens:/app/output/heron git.legonzaur.fr/heronfief/tokens-gen:main' && sudo chown -R $USER:$USER ./assets/images/tokens",
"pull-token-images": "sh -c 'docker run --rm --pull=always -v ./public/img:/repo/images/tokens git.legonzaur.fr/heronfief/tokens-gen:main cp -r img/icon/ /repo/images/tokens' && sudo chown -R $USER:$USER ./public/",
"pull-card-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-card-images && npm run tokens-gen && npm run pull-token-images",
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
@@ -12,12 +17,12 @@
"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"
"@pinia-plugin-persistedstate/nuxt": "^1.2.0",
"nuxt": "^3.14.1592"
}
}
+403
View File
@@ -0,0 +1,403 @@
<script setup lang="ts">
import LoadingScreen from "~/components/game/overlay/LoadingScreen.vue";
import {
endTurn,
subscribe,
useToken,
startGame,
joinGame,
deleteGame,
leaveGame,
surrender,
} from "~/netcode";
import {
type Container,
type Effect,
type Game,
type Player,
} from "~/netcode/interfaces";
import { handlers } from "~/netcode/events";
const config = useRuntimeConfig();
const settingsStore = useSettingsStore();
const authStore = useAuthStore();
const route = useRoute();
const gameId = route.query.id;
const {
data: game,
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]);
}
}
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, {
game: current_game,
containers,
playerList,
refresh,
focusPlayer,
settingsStore,
});
} 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>
<GameModalResultDialog :isVisible="showResults" :won="isWinner" :players="playerList"
@close="showResults = false">
</GameModalResultDialog>
<GamePlayerSelf v-if="selfPlayer" :container="selfPlayer.hand"></GamePlayerSelf>
</template>
</ClientOnly>
<!-- <LoadingScreen v-if="!loaded"></LoadingScreen> -->
<footer>
<div>
<GameModalOptionsDialog></GameModalOptionsDialog>
<ClientOnly>
<GameModalConfigureDialog v-if="game && game.owner.userId == authStore.selfUser.userId" :game="game">
</GameModalConfigureDialog>
</ClientOnly>
</div>
<ClientOnly v-if="game">
<div id="turn_buttons">
<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">
<template v-if="game.owner.userId == authStore.selfUser.userId">
<button class="end_turn_button turn_icon" @click="startGame(game._id)">
Start game
</button>
</template>
<button class="end_turn_button turn_icon" @click="joinGame(game._id)" v-else-if="!selfPlayer">
Join game
</button>
<button v-else @click="leaveGame(game._id)">Leave game</button>
</template>
</div>
<div>
<button v-if="game.started && selfPlayer && game.owner._id != selfPlayer.user._id" class="red"
@click="surrender(game._id)">
Surrender
</button>
<button @click="navigateTo('/lobby')">Return to lobby</button>
</div>
</ClientOnly>
</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>
-37
View File
@@ -1,37 +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>
</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>
+326
View File
@@ -0,0 +1,326 @@
<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 == undefined) {
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 == "leaveTeam") {
const game = games.value?.find((g) => g._id == data.game);
if (game == undefined) {
throw new Error("Cannot find game");
}
const team = game.teams.find((t) => t._id == data.team._id);
if (team == undefined) {
throw new Error("Cannot find team");
}
const playerIndex = team.players.findIndex((p) => p._id == data.playerId);
if (playerIndex == -1) {
throw new Error("Cannot find player in team");
}
team.players.splice(playerIndex, 1);
if (team.players.length === 0) {
const index = game.teams.findIndex((t) => t._id == data.team._id);
game.teams.splice(index, 1);
}
} 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>
<template v-if="authStore.logged">
<GamePlayerIcon
:user="(authStore.selfUser as User)"
/>
<button @click="navigateTo('/logout')">Logout</button>
</template>
<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>
+12
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
<script setup lang="ts">
const route = useRoute();
const config = useRuntimeConfig();
const code = route.query.code;
navigateTo(config.public.endpoint + "/logout", { external: true });
</script>
+391 -2
View File
@@ -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>&#8203;</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>&#8203;</p>
<hr />
<p>&#8203;</p>
<h2>Objectif</h2>
<p>
L'unique objectif du jeu est d'être le dernier en lice. Pour y
arriver, il faut descendre les Points de Vie de vos adversaires à 0 en
partant de 50.
</p>
<p>
Le moyen principal d'y arriver, c'est de faire des
<span style="color: red">Dégats</span>
<img src="/img/damage.png" class="icon_img" draggable="false" />. Tout
le reste des mécaniques du jeu sont là pour vous faciliter la tâche,
ou pour mettre des bâtons dans les roues de vos adversaires.
</p>
<p>&#8203;</p>
<hr />
<h2>Deckbuilding</h2>
<p>
Comme c'est un jeu de "deckbuilding", on va expliquer ce qu'est le
"deck" et ce qui nous permet de le "build".
</p>
<div class="card-row">
<GameCard :cardId="undefined" :tilted="false"></GameCard>
</div>
<p>&#8203;</p>
<p>
Le deck d'un joueur est-ce qui constitue sa pioche, qui lui permettra
ensuite de constituer sa main (la main est préparée à la fin du tour
et permet de jouer le tour d'après).
</p>
<p>
Quand la pioche est vide, elle est recyclée à partir de sa défausse.
</p>
<p>
La défausse est ce qui contient les cartes déjà jouées. Mais aussi
celles qui sont achetées! Elle finit par être mélangée pour former la
nouvelle pioche.
</p>
<p>&#8203;</p>
<hr />
<h2>Deck de base</h2>
<p>Tout le monde commence avec 10 cartes divisées en 4 types:</p>
<ul>
<li>7x Or (1)</li>
<li>1x Or (2)</li>
<li>1x Dégats (1)</li>
<li>1x Dégats (2)</li>
</ul>
<div class="card-row">
<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>&#8203;</p>
<hr />
<h2>Types de cartes</h2>
<p>
Les cartes sont divisées en 3 catégories: objets, actions et
champions.
</p>
<div class="card-row">
<GameCard :cardId="56" :tilted="false"></GameCard>
<GameCard :cardId="52" :tilted="false"></GameCard>
<GameCard :cardId="43" :tilted="false"></GameCard>
</div>
<p>&#8203;</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>&#8203;</p>
<hr />
<h2>Building</h2>
<p>
Passons au "building": on peut acheter des cartes pour les ajouter à
notre défausse qui, si vous avez suivi, constituera la prochaine
pioche. Avec le jeu de base,
<span style="color: darkgoldenrod">l'or</span>
<img src="/img/gold.png" class="icon_img" draggable="false" /> sert
exclusivement à accomplir ce but: acheter des cartes.
</p>
<div class="card-row">
<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>&#8203;</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>&#8203;</p>
<p>
Les cartes <span style="color: darkgoldenrod">jaunes</span> sont les
cartes Empire.
</p>
<p>
Elles font beaucoup de soin, et activent souvent des effets de pioche.
</p>
<p>&#8203;</p>
<div class="card-row">
<GameCard :cardId="39" :tilted="false"></GameCard>
<GameCard :cardId="34" :tilted="false"></GameCard>
<GameCard :cardId="40" :tilted="false"></GameCard>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: red">rouges</span> sont les cartes
Necros.
</p>
<p>
Elles sont les seules à pouvoir sacrifier des cartes pour les sortir
de son deck de façon permanente.
</p>
<p>&#8203;</p>
<div class="card-row">
<GameCard :cardId="52" :tilted="false"></GameCard>
<GameCard :cardId="46" :tilted="false"></GameCard>
<GameCard :cardId="47" :tilted="false"></GameCard>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: darkgreen">vertes</span> sont les
cartes Sauvages.
</p>
<p>
Elles font beaucoup de dégats, et forcent vos adversaires à jouer
moins de cartes.
</p>
<p>&#8203;</p>
<div class="card-row">
<GameCard :cardId="24" :tilted="false"></GameCard>
<GameCard :cardId="25" :tilted="false"></GameCard>
<GameCard :cardId="19" :tilted="false"></GameCard>
</div>
<p>&#8203;</p>
<p>
Les cartes <span style="color: darkblue">bleues</span> sont les cartes
Guilde.
</p>
<p>
Elles donnent beaucoup d'or, assomment les champions adverses et
manipulent votre pioche.
</p>
<p>&#8203;</p>
<hr />
<h2>Déclencheurs</h2>
<p>
Les effets de cartes peuvent se déclencher pour 3 raisons différentes.
</p>
<div class="card-row">
<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>&#8203;</p>
<hr />
<h2>Effets</h2>
<p>Une variété d'effets peuvent être inclus dans les cartes.</p>
<p>
Il y en a plein et les expliquer ici serait relou, alors je vous
laisse poser la question en vocal si vous n'en comprenez pas un.
</p>
<p>&#8203;</p>
<hr />
<h2>Activer les cartes dans la version en ligne</h2>
<p>
Il suffit de cliquer sur la carte que vous voulez activer dans votre
main pour la "LOCK".
</p>
<p>
Une carte "LOCKED" est considérée en jeu, & peut activer des effets
Allié pour d'autres cartes, etc.
</p>
<p>
Si vous avez des choix à faire sur la carte, un menu apparaîtra pour
vous laisser choisir.
</p>
<p>
Pour faire des dommages, il y aura un bouton "Damage" sur vos ennemis.
Pareil sur leurs champions.
</p>
<p>
Quand vous avez un effet "Sacrifice", vous pouvez fouiller votre
défausse pour choisir la carte à sacrifier. Vous pouvez également
sacrifier une carte de la main qui n'est pas encore locked!
</p>
<h2>Zoomer sur les cartes</h2>
<p>Pour zoomer sur une carte, maintenez votre clic droit.</p>
</section>
</div>
</div>
</template>
<style scoped>
.background {
background-color: #f0f0f0;
background-image: url("/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>
+11
View File
@@ -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
View File
Binary file not shown.
+620
View File
@@ -0,0 +1,620 @@
/*
contributors: [Stefan Gustavson, Ian McEwan]
description: modulus of 289
use: <float|vec2|vec3|vec4> mod289(<float|vec2|vec3|vec4> x)
*/
#ifndef FNC_MOD289
#define FNC_MOD289
float mod289(const in float x) { return x - floor(x * (1. / 289.)) * 289.; }
vec2 mod289(const in vec2 x) { return x - floor(x * (1. / 289.)) * 289.; }
vec3 mod289(const in vec3 x) { return x - floor(x * (1. / 289.)) * 289.; }
vec4 mod289(const in vec4 x) { return x - floor(x * (1. / 289.)) * 289.; }
#endif
/*
contributors: [Stefan Gustavson, Ian McEwan]
description: permute
use: <float|vec2|vec3|vec4> permute(<float|vec2|vec3|vec4> x)
examples:
- https://raw.githubusercontent.com/patriciogonzalezvivo/lygia_examples/main/math_functions.frag
*/
#ifndef FNC_PERMUTE
#define FNC_PERMUTE
float permute(const in float v) { return mod289(((v * 34.0) + 1.0) * v); }
vec2 permute(const in vec2 v) { return mod289(((v * 34.0) + 1.0) * v); }
vec3 permute(const in vec3 v) { return mod289(((v * 34.0) + 1.0) * v); }
vec4 permute(const in vec4 v) { return mod289(((v * 34.0) + 1.0) * v); }
#endif
/*
contributors: [Stefan Gustavson, Ian McEwan]
description: Fast, accurate inverse square root.
use: <float|vec2|vec3|vec4> taylorInvSqrt(<float|vec2|vec3|vec4> x)
*/
#ifndef FNC_TAYLORINVSQRT
#define FNC_TAYLORINVSQRT
float taylorInvSqrt(in float r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec2 taylorInvSqrt(in vec2 r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec3 taylorInvSqrt(in vec3 r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec4 taylorInvSqrt(in vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
#endif
/*
contributors: [Stefan Gustavson, Ian McEwan]
description: grad4, used for snoise(vec4 v)
use: grad4(<float> j, <vec4> ip)
*/
#ifndef FNC_GRAD4
#define FNC_GRAD4
vec4 grad4(float j, vec4 ip) {
const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
vec4 p,s;
p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
s = vec4(lessThan(p, vec4(0.0)));
p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www;
return p;
}
#endif
/*
contributors: [Stefan Gustavson, Ian McEwan]
description: Simplex Noise https://github.com/stegu/webgl-noise
use: snoise(<vec2|vec3|vec4> pos)
license: |
Copyright 2021-2023 by Stefan Gustavson and Ian McEwan.
Published under the terms of the MIT license:
https://opensource.org/license/mit/
examples:
- /shaders/generative_snoise.frag
*/
#ifndef FNC_SNOISE
#define FNC_SNOISE
float snoise(in vec2 v) {
const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
-0.577350269189626, // -1.0 + 2.0 * C.x
0.024390243902439); // 1.0 / 41.0
// First corner
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
// Other corners
vec2 i1;
//i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
//i1.y = 1.0 - i1.x;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
// x0 = x0 - 0.0 + 0.0 * C.xx ;
// x1 = x0 - i1 + 1.0 * C.xx ;
// x2 = x0 - 1.0 + 2.0 * C.xx ;
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
// Permutations
i = mod289(i); // Avoid truncation effects in permutation
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
// Gradients: 41 points uniformly over a line, mapped onto a diamond.
// The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
// Normalise gradients implicitly by scaling m
// Approximation of: m *= inversesqrt( a0*a0 + h*h );
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
// Compute final noise value at P
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
float snoise(in vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
// First corner
vec3 i = floor(v + dot(v, C.yyy) );
vec3 x0 = v - i + dot(i, C.xxx) ;
// Other corners
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min( g.xyz, l.zxy );
vec3 i2 = max( g.xyz, l.zxy );
// x0 = x0 - 0.0 + 0.0 * C.xxx;
// x1 = x0 - i1 + 1.0 * C.xxx;
// x2 = x0 - i2 + 2.0 * C.xxx;
// x3 = x0 - 1.0 + 3.0 * C.xxx;
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
// Permutations
i = mod289(i);
vec4 p = permute( permute( permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
// Gradients: 7x7 points over a square, mapped onto an octahedron.
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
float n_ = 0.142857142857; // 1.0/7.0
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4( x.xy, y.xy );
vec4 b1 = vec4( x.zw, y.zw );
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
vec3 p0 = vec3(a0.xy,h.x);
vec3 p1 = vec3(a0.zw,h.y);
vec3 p2 = vec3(a1.xy,h.z);
vec3 p3 = vec3(a1.zw,h.w);
//Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
// Mix final noise value
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
dot(p2,x2), dot(p3,x3) ) );
}
float snoise(in vec4 v) {
const vec4 C = vec4( 0.138196601125011, // (5 - sqrt(5))/20 G4
0.276393202250021, // 2 * G4
0.414589803375032, // 3 * G4
-0.447213595499958); // -1 + 4 * G4
// First corner
vec4 i = floor(v + dot(v, vec4(.309016994374947451)) ); // (sqrt(5) - 1)/4
vec4 x0 = v - i + dot(i, C.xxxx);
// Other corners
// Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI)
vec4 i0;
vec3 isX = step( x0.yzw, x0.xxx );
vec3 isYZ = step( x0.zww, x0.yyz );
// i0.x = dot( isX, vec3( 1.0 ) );
i0.x = isX.x + isX.y + isX.z;
i0.yzw = 1.0 - isX;
// i0.y += dot( isYZ.xy, vec2( 1.0 ) );
i0.y += isYZ.x + isYZ.y;
i0.zw += 1.0 - isYZ.xy;
i0.z += isYZ.z;
i0.w += 1.0 - isYZ.z;
// i0 now contains the unique values 0,1,2,3 in each channel
vec4 i3 = clamp( i0, 0.0, 1.0 );
vec4 i2 = clamp( i0-1.0, 0.0, 1.0 );
vec4 i1 = clamp( i0-2.0, 0.0, 1.0 );
// x0 = x0 - 0.0 + 0.0 * C.xxxx
// x1 = x0 - i1 + 1.0 * C.xxxx
// x2 = x0 - i2 + 2.0 * C.xxxx
// x3 = x0 - i3 + 3.0 * C.xxxx
// x4 = x0 - 1.0 + 4.0 * C.xxxx
vec4 x1 = x0 - i1 + C.xxxx;
vec4 x2 = x0 - i2 + C.yyyy;
vec4 x3 = x0 - i3 + C.zzzz;
vec4 x4 = x0 + C.wwww;
// Permutations
i = mod289(i);
float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x);
vec4 j1 = permute( permute( permute( permute (
i.w + vec4(i1.w, i2.w, i3.w, 1.0 ))
+ i.z + vec4(i1.z, i2.z, i3.z, 1.0 ))
+ i.y + vec4(i1.y, i2.y, i3.y, 1.0 ))
+ i.x + vec4(i1.x, i2.x, i3.x, 1.0 ));
// Gradients: 7x7x6 points over a cube, mapped onto a 4-cross polytope
// 7*7*6 = 294, which is close to the ring size 17*17 = 289.
vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ;
vec4 p0 = grad4(j0, ip);
vec4 p1 = grad4(j1.x, ip);
vec4 p2 = grad4(j1.y, ip);
vec4 p3 = grad4(j1.z, ip);
vec4 p4 = grad4(j1.w, ip);
// Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
p4 *= taylorInvSqrt(dot(p4,p4));
// Mix contributions from the five corners
vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0);
vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0);
m0 = m0 * m0;
m1 = m1 * m1;
return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 )))
+ dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ;
}
vec2 snoise2( vec2 x ){
float s = snoise(vec2( x ));
float s1 = snoise(vec2( x.y - 19.1, x.x + 47.2 ));
return vec2( s , s1 );
}
vec3 snoise3( vec3 x ){
float s = snoise(vec3( x ));
float s1 = snoise(vec3( x.y - 19.1 , x.z + 33.4 , x.x + 47.2 ));
float s2 = snoise(vec3( x.z + 74.2 , x.x - 124.5 , x.y + 99.4 ));
return vec3( s , s1 , s2 );
}
vec3 snoise3( vec4 x ){
float s = snoise(vec4( x ));
float s1 = snoise(vec4( x.y - 19.1 , x.z + 33.4 , x.x + 47.2, x.w ));
float s2 = snoise(vec4( x.z + 74.2 , x.x - 124.5 , x.y + 99.4, x.w ));
return vec3( s , s1 , s2 );
}
#endif
/*
contributors: ["Patricio Gonzalez Vivo", "David Hoskins", "Inigo Quilez"]
description: Pass a value and get some random normalize value between 0 and 1
use: float random[2|3](<float|vec2|vec3> value)
options:
- RANDOM_HIGHER_RANGE: for working with a range over 0 and 1
- RANDOM_SINLESS: Use sin-less random, which tolerates bigger values before producing pattern. From https://www.shadertoy.com/view/4djSRW
- RANDOM_SCALE: by default this scale if for number with a big range. For producing good random between 0 and 1 use bigger range
examples:
- /shaders/generative_random.frag
license:
- MIT License (MIT) Copyright 2014, David Hoskins
*/
#ifndef RANDOM_SCALE
#ifdef RANDOM_HIGHER_RANGE
#define RANDOM_SCALE vec4(.1031, .1030, .0973, .1099)
#else
#define RANDOM_SCALE vec4(443.897, 441.423, .0973, .1099)
#endif
#endif
#ifndef FNC_RANDOM
#define FNC_RANDOM
float random(in float x) {
#ifdef RANDOM_SINLESS
x = fract(x * RANDOM_SCALE.x);
x *= x + 33.33;
x *= x + x;
return fract(x);
#else
return fract(sin(x) * 43758.5453);
#endif
}
float random(in vec2 st) {
#ifdef RANDOM_SINLESS
vec3 p3 = fract(vec3(st.xyx) * RANDOM_SCALE.xyz);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
#else
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
#endif
}
float random(in vec3 pos) {
#ifdef RANDOM_SINLESS
pos = fract(pos * RANDOM_SCALE.xyz);
pos += dot(pos, pos.zyx + 31.32);
return fract((pos.x + pos.y) * pos.z);
#else
return fract(sin(dot(pos.xyz, vec3(70.9898, 78.233, 32.4355))) * 43758.5453123);
#endif
}
float random(in vec4 pos) {
#ifdef RANDOM_SINLESS
pos = fract(pos * RANDOM_SCALE);
pos += dot(pos, pos.wzxy + 33.33);
return fract((pos.x + pos.y) * (pos.z + pos.w));
#else
float dot_product = dot(pos, vec4(12.9898,78.233,45.164,94.673));
return fract(sin(dot_product) * 43758.5453);
#endif
}
vec2 random2(float p) {
vec3 p3 = fract(vec3(p) * RANDOM_SCALE.xyz);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.xx + p3.yz) * p3.zy);
}
vec2 random2(vec2 p) {
vec3 p3 = fract(p.xyx * RANDOM_SCALE.xyz);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.xx + p3.yz) * p3.zy);
}
vec2 random2(vec3 p3) {
p3 = fract(p3 * RANDOM_SCALE.xyz);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.xx + p3.yz) * p3.zy);
}
vec3 random3(float p) {
vec3 p3 = fract(vec3(p) * RANDOM_SCALE.xyz);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.xxy + p3.yzz) * p3.zyx);
}
vec3 random3(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * RANDOM_SCALE.xyz);
p3 += dot(p3, p3.yxz + 19.19);
return fract((p3.xxy + p3.yzz) * p3.zyx);
}
vec3 random3(vec3 p) {
p = fract(p * RANDOM_SCALE.xyz);
p += dot(p, p.yxz + 19.19);
return fract((p.xxy + p.yzz) * p.zyx);
}
vec4 random4(float p) {
vec4 p4 = fract(p * RANDOM_SCALE);
p4 += dot(p4, p4.wzxy + 19.19);
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
}
vec4 random4(vec2 p) {
vec4 p4 = fract(p.xyxy * RANDOM_SCALE);
p4 += dot(p4, p4.wzxy + 19.19);
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
}
vec4 random4(vec3 p) {
vec4 p4 = fract(p.xyzx * RANDOM_SCALE);
p4 += dot(p4, p4.wzxy + 19.19);
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
}
vec4 random4(vec4 p4) {
p4 = fract(p4 * RANDOM_SCALE);
p4 += dot(p4, p4.wzxy + 19.19);
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
}
#endif
/*
contributors: Patricio Gonzalez Vivo
description: Signed Random
use: srandomX(<vec2|vec3> x)
license:
- Copyright (c) 2021 Patricio Gonzalez Vivo under Prosperity License - https://prosperitylicense.com/versions/3.0.0
- Copyright (c) 2021 Patricio Gonzalez Vivo under Patron License - https://lygia.xyz/license
*/
#ifndef FNC_SRANDOM
#define FNC_SRANDOM
float srandom(in float x) {
return -1. + 2. * fract(sin(x) * 43758.5453);
}
float srandom(in vec2 st) {
return -1. + 2. * fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
float srandom(in vec3 pos) {
return -1. + 2. * fract(sin(dot(pos.xyz, vec3(70.9898, 78.233, 32.4355))) * 43758.5453123);
}
float srandom(in vec4 pos) {
float dot_product = dot(pos, vec4(12.9898,78.233,45.164,94.673));
return -1. + 2. * fract(sin(dot_product) * 43758.5453);
}
vec2 srandom2(in vec2 st) {
const vec2 k = vec2(.3183099, .3678794);
st = st * k + k.yx;
return -1. + 2. * fract(16. * k * fract(st.x * st.y * (st.x + st.y)));
}
vec3 srandom3(in vec3 p) {
p = vec3( dot(p, vec3(127.1, 311.7, 74.7)),
dot(p, vec3(269.5, 183.3, 246.1)),
dot(p, vec3(113.5, 271.9, 124.6)));
return -1. + 2. * fract(sin(p) * 43758.5453123);
}
vec2 srandom2(in vec2 p, const in float tileLength) {
p = mod(p, vec2(tileLength));
return srandom2(p);
}
vec3 srandom3(in vec3 p, const in float tileLength) {
p = mod(p, vec3(tileLength));
return srandom3(p);
}
#endif
/*
contributors: Inigo Quiles
description: cubic polynomial https://iquilezles.org/articles/smoothsteps/
use: <float|vec2|vec3|vec4> cubic(<float|vec2|vec3|vec4> value[, <float> in, <float> out]);
examples:
- https://raw.githubusercontent.com/patriciogonzalezvivo/lygia_examples/main/math_functions.frag
*/
#ifndef FNC_CUBIC
#define FNC_CUBIC
float cubic(const in float v) { return v*v*(3.0-2.0*v); }
vec2 cubic(const in vec2 v) { return v*v*(3.0-2.0*v); }
vec3 cubic(const in vec3 v) { return v*v*(3.0-2.0*v); }
vec4 cubic(const in vec4 v) { return v*v*(3.0-2.0*v); }
float cubic(const in float v, in float slope0, in float slope1) {
float a = slope0 + slope1 - 2.;
float b = -2. * slope0 - slope1 + 3.;
float c = slope0;
float v2 = v * v;
float v3 = v * v2;
return a * v3 + b * v2 + c * v;
}
vec2 cubic(const in vec2 v, in float slope0, in float slope1) {
float a = slope0 + slope1 - 2.;
float b = -2. * slope0 - slope1 + 3.;
float c = slope0;
vec2 v2 = v * v;
vec2 v3 = v * v2;
return a * v3 + b * v2 + c * v;
}
vec3 cubic(const in vec3 v, in float slope0, in float slope1) {
float a = slope0 + slope1 - 2.;
float b = -2. * slope0 - slope1 + 3.;
float c = slope0;
vec3 v2 = v * v;
vec3 v3 = v * v2;
return a * v3 + b * v2 + c * v;
}
vec4 cubic(const in vec4 v, in float slope0, in float slope1) {
float a = slope0 + slope1 - 2.;
float b = -2. * slope0 - slope1 + 3.;
float c = slope0;
vec4 v2 = v * v;
vec4 v3 = v * v2;
return a * v3 + b * v2 + c * v;
}
#endif
/*
contributors: Inigo Quiles
description: quintic polynomial https://iquilezles.org/articles/smoothsteps/
use: <float|vec2|vec3|vec4> quintic(<float|vec2|vec3|vec4> value);
examples:
- https://raw.githubusercontent.com/patriciogonzalezvivo/lygia_examples/main/math_functions.frag
*/
#ifndef FNC_QUINTIC
#define FNC_QUINTIC
float quintic(const in float v) { return v*v*v*(v*(v*6.0-15.0)+10.0); }
vec2 quintic(const in vec2 v) { return v*v*v*(v*(v*6.0-15.0)+10.0); }
vec3 quintic(const in vec3 v) { return v*v*v*(v*(v*6.0-15.0)+10.0); }
vec4 quintic(const in vec4 v) { return v*v*v*(v*(v*6.0-15.0)+10.0); }
#endif
/*
contributors: Patricio Gonzalez Vivo
description: Gradient Noise
use: gnoise(<float> x)
license:
- Copyright (c) 2021 Patricio Gonzalez Vivo under Prosperity License - https://prosperitylicense.com/versions/3.0.0
- Copyright (c) 2021 Patricio Gonzalez Vivo under Patron License - https://lygia.xyz/license
*/
#ifndef FNC_GNOISE
#define FNC_GNOISE
float gnoise(float x) {
float i = floor(x); // integer
float f = fract(x); // fraction
return mix(random(i), random(i + 1.0), smoothstep(0.,1.,f));
}
float gnoise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = cubic(f);
return mix( a, b, u.x) +
(c - a)* u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
float gnoise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = quintic(f);
return -1.0 + 2.0 * mix( mix( mix( random(i + vec3(0.0,0.0,0.0)),
random(i + vec3(1.0,0.0,0.0)), u.x),
mix( random(i + vec3(0.0,1.0,0.0)),
random(i + vec3(1.0,1.0,0.0)), u.x), u.y),
mix( mix( random(i + vec3(0.0,0.0,1.0)),
random(i + vec3(1.0,0.0,1.0)), u.x),
mix( random(i + vec3(0.0,1.0,1.0)),
random(i + vec3(1.0,1.0,1.0)), u.x), u.y), u.z );
}
float gnoise(vec3 p, float tileLength) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = quintic(f);
return mix( mix( mix( dot( srandom3(i + vec3(0.0,0.0,0.0), tileLength), f - vec3(0.0,0.0,0.0)),
dot( srandom3(i + vec3(1.0,0.0,0.0), tileLength), f - vec3(1.0,0.0,0.0)), u.x),
mix( dot( srandom3(i + vec3(0.0,1.0,0.0), tileLength), f - vec3(0.0,1.0,0.0)),
dot( srandom3(i + vec3(1.0,1.0,0.0), tileLength), f - vec3(1.0,1.0,0.0)), u.x), u.y),
mix( mix( dot( srandom3(i + vec3(0.0,0.0,1.0), tileLength), f - vec3(0.0,0.0,1.0)),
dot( srandom3(i + vec3(1.0,0.0,1.0), tileLength), f - vec3(1.0,0.0,1.0)), u.x),
mix( dot( srandom3(i + vec3(0.0,1.0,1.0), tileLength), f - vec3(0.0,1.0,1.0)),
dot( srandom3(i + vec3(1.0,1.0,1.0), tileLength), f - vec3(1.0,1.0,1.0)), u.x), u.y), u.z );
}
vec3 gnoise3(vec3 x) {
return vec3(gnoise(x+vec3(123.456, 0.567, 0.37)),
gnoise(x+vec3(0.11, 47.43, 19.17)),
gnoise(x) );
}
#endif
/*
contributors: Patricio Gonzalez Vivo
description: Fractal Brownian Motion
use: fbm(<vec2> pos)
options:
FBM_OCTAVES: numbers of octaves. Default is 4.
FBM_NOISE_FNC(UV): noise function to use Default 'snoise(UV)' (simplex noise)
FBM_VALUE_INITIAL: initial value. Default is 0.
FBM_SCALE_SCALAR: scalar. Defualt is 2.
FBM_AMPLITUD_INITIAL: initial amplitud value. Default is 0.5
FBM_AMPLITUD_SCALAR: amplitud scalar. Default is 0.5
examples:
- /shaders/generative_fbm.frag
license:
- Copyright (c) 2021 Patricio Gonzalez Vivo under Prosperity License - https://prosperitylicense.com/versions/3.0.0
- Copyright (c) 2021 Patricio Gonzalez Vivo under Patron License - https://lygia.xyz/license
*/
#ifndef FBM_OCTAVES
#define FBM_OCTAVES 4
#endif
#ifndef FBM_NOISE_FNC
#define FBM_NOISE_FNC(UV) snoise(UV)
#endif
#ifndef FBM_NOISE2_FNC
#define FBM_NOISE2_FNC(UV) FBM_NOISE_FNC(UV)
#endif
#ifndef FBM_NOISE3_FNC
#define FBM_NOISE3_FNC(UV) FBM_NOISE_FNC(UV)
#endif
#ifndef FBM_NOISE_TILABLE_FNC
#define FBM_NOISE_TILABLE_FNC(UV, TILE) gnoise(UV, TILE)
#endif
#ifndef FBM_NOISE3_TILABLE_FNC
#define FBM_NOISE3_TILABLE_FNC(UV, TILE) FBM_NOISE_TILABLE_FNC(UV, TILE)
#endif
#ifndef FBM_NOISE_TYPE
#define FBM_NOISE_TYPE float
#endif
#ifndef FBM_VALUE_INITIAL
#define FBM_VALUE_INITIAL 0.0
#endif
#ifndef FBM_SCALE_SCALAR
#define FBM_SCALE_SCALAR 2.0
#endif
#ifndef FBM_AMPLITUD_INITIAL
#define FBM_AMPLITUD_INITIAL 0.5
#endif
#ifndef FBM_AMPLITUD_SCALAR
#define FBM_AMPLITUD_SCALAR 0.5
#endif
#ifndef FNC_FBM
#define FNC_FBM
FBM_NOISE_TYPE fbm(in vec2 st) {
// Initial values
FBM_NOISE_TYPE value = FBM_NOISE_TYPE(FBM_VALUE_INITIAL);
float amplitud = FBM_AMPLITUD_INITIAL;
// Loop of octaves
for (int i = 0; i < FBM_OCTAVES; i++) {
value += amplitud * FBM_NOISE2_FNC(st);
st *= FBM_SCALE_SCALAR;
amplitud *= FBM_AMPLITUD_SCALAR;
}
return value;
}
FBM_NOISE_TYPE fbm(in vec3 pos) {
// Initial values
FBM_NOISE_TYPE value = FBM_NOISE_TYPE(FBM_VALUE_INITIAL);
float amplitud = FBM_AMPLITUD_INITIAL;
// Loop of octaves
for (int i = 0; i < FBM_OCTAVES; i++) {
value += amplitud * FBM_NOISE3_FNC(pos);
pos *= FBM_SCALE_SCALAR;
amplitud *= FBM_AMPLITUD_SCALAR;
}
return value;
}
FBM_NOISE_TYPE fbm(vec3 p, float tileLength) {
const float persistence = 0.5;
const float lacunarity = 2.0;
float amplitude = 0.5;
FBM_NOISE_TYPE total = FBM_NOISE_TYPE(0.0);
float normalization = 0.0;
for (int i = 0; i < FBM_OCTAVES; ++i) {
float noiseValue = FBM_NOISE3_TILABLE_FNC(p, tileLength * lacunarity * 0.5) * 0.5 + 0.5;
total += noiseValue * amplitude;
normalization += amplitude;
amplitude *= persistence;
p = p * lacunarity;
}
return total / normalization;
}
#endif
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="88.73304mm"
height="134.13795mm"
viewBox="0 0 88.73304 134.13795"
version="1.1"
id="svg1"
xml:space="preserve"
sodipodi:docname="fuck.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="0.71891835"
inkscape:cx="134.22943"
inkscape:cy="468.06428"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="1680"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-59.418203,-70.094122)"><path
style="opacity:1;fill:#e5d600;fill-opacity:1;stroke:none;stroke-width:3.49999;stroke-opacity:1;paint-order:stroke fill markers"
d="m 96.352499,125.0496 v -5.30232 c -2.421262,-8.41348 -13.713084,-12.75856 -18.744229,-0.044 v 5.33484 4.73839 0 l -6.716543,0.0313 c -3.042502,0.51774 -4.401994,2.29774 -5.382434,4.36203 l -5.635456,19.68959 c -0.536458,2.04722 -0.674646,4.17979 0,6.48653 11.431564,61.83841 86.704933,55.09416 88.200633,0 0,0 0.17274,-17.73214 0,-18.58223 -1.91851,-9.4414 -12.83795,-12.2172 -15.51392,-4.15694 0.48306,-9.71109 -7.76736,-14.78186 -17.64884,-10.18956 0,0 0.0508,-48.350297 0,-49.216216 -0.60205,-10.268778 -18.30446,-11.336423 -18.559211,0 V 125.0496"
id="path1"
sodipodi:nodetypes="cccccccccccsccscc" /><path
style="opacity:1;fill:none;fill-opacity:1;stroke:#eea200;stroke-width:3.9172;stroke-opacity:1;paint-order:stroke fill markers"
d="m 75.857311,129.7732 v 39.46359"
id="path2" /></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+157
View File
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="59.500008mm"
height="79.500008mm"
viewBox="0 0 59.500008 79.500008"
version="1.1"
id="svg1"
sodipodi:docname="Unnamed document 1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="true">
<inkscape:grid
id="grid1"
units="mm"
originx="-75.249997"
originy="-72.249997"
spacingx="0.99999998"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true"
enabled="true" />
</sodipodi:namedview>
<defs
id="defs1">
<inkscape:path-effect
effect="mirror_symmetry"
start_point="62.136614,73.509798"
end_point="62.136614,153.00981"
center_point="62.136614,113.2598"
id="path-effect21"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="vertical"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" />
<inkscape:path-effect
effect="mirror_symmetry"
start_point="62.136614,73.509798"
end_point="62.136614,153.00981"
center_point="62.136614,113.2598"
id="path-effect19"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="vertical"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" />
<inkscape:path-effect
effect="mirror_symmetry"
start_point="62.136614,73.509798"
end_point="62.136614,153.00981"
center_point="62.136614,113.2598"
id="path-effect7"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="vertical"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" />
</defs>
<g
inkscape:label="Stroke"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(-75.249994,-72.249998)">
<path
style="display:inline;opacity:1;fill:#a0a0a0;fill-opacity:1;stroke:none;stroke-width:3.5;stroke-dasharray:none;stroke-opacity:1"
d="m 62.136618,151.2598 h 15.000001 c 0,0 13,-12 13,-18 0,-13 -1,-20 -18,-34.000003 v -8 h -4 4 c 5,0 5,-7 0,-7 h -10.000001 7.000001 l 3,-8.999999 H 62.136618 m -8e-6,76.000002 H 47.136609 c 0,0 -13,-12 -13,-18 0,-13 1,-20 18,-34.000003 v -8 h 4 -4 c -5,0 -5,-7 0,-7 H 62.13661 55.136609 l -3,-8.999999 H 62.13661"
id="Fill"
sodipodi:nodetypes="ccsccccccccc"
transform="translate(42.863384,-1.2597997)"
inkscape:original-d="m 62.136618,151.2598 h 15.000001 c 0,0 13,-12 13,-18 0,-13 -1,-20 -18,-34.000003 v -8 h -4 4 c 5,0 5,-7 0,-7 h -10.000001 7.000001 l 3,-8.999999 H 62.136618"
inkscape:path-effect="#path-effect7"
inkscape:highlight-color="#47e661"
inkscape:label="Fill" />
<path
style="display:inline;opacity:1;fill:#83b229;fill-opacity:1;stroke:none;stroke-width:3.5;stroke-dasharray:none;stroke-opacity:1"
d="m 62.136618,151.2598 h 15.000001 c 0,0 13,-12 13,-18 0,-13 -2,-22 -18,-34.000003 -2.345162,0.366674 -4.685658,0.58873 -7.000001,0 -1.716931,-0.618631 -2.013396,-0.567824 -3.000002,-0.649209 m -6e-6,52.649212 H 47.136609 c 0,0 -13,-12 -13,-18 0,-13 2,-22 18,-34.000003 2.345162,0.366674 4.685658,0.58873 7.000001,0 1.716931,-0.618631 2.013396,-0.567824 3.000002,-0.649209"
id="path19"
sodipodi:nodetypes="ccsccc"
transform="translate(42.863384,-1.2597997)"
inkscape:original-d="m 62.136618,151.2598 h 15.000001 c 0,0 13,-12 13,-18 0,-13 -2,-22 -18,-34.000003 -2.345162,0.366674 -4.685658,0.58873 -7.000001,0 -1.716931,-0.618631 -2.013396,-0.567824 -3.000002,-0.649209"
inkscape:path-effect="#path-effect21"
inkscape:highlight-color="#000000"
inkscape:label="path19" />
<path
style="display:inline;opacity:1;fill:#967a3e;fill-opacity:1;stroke:none;stroke-width:3.49999;stroke-dasharray:none;stroke-opacity:1"
d="M 95.000002,73.999997 H 115 l -3,9 H 97.999996 l -2.999994,-9"
id="Bouchon"
inkscape:highlight-color="#9f633c" />
<path
style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3.5;stroke-dasharray:none;stroke-opacity:1"
d="m 62.136618,151.2598 h 15.000001 c 0,0 13,-12 13,-18 0,-13 -1,-20 -18,-34.000003 v -8 h -4 4 c 5,0 5,-7 0,-7 h -10.000001 7.000001 l 3,-8.999999 H 62.136618 m -8e-6,76.000002 H 47.136609 c 0,0 -13,-12 -13,-18 0,-13 1,-20 18,-34.000003 v -8 h 4 -4 c -5,0 -5,-7 0,-7 H 62.13661 55.136609 l -3,-8.999999 H 62.13661"
id="Stroke"
sodipodi:nodetypes="ccsccccccccc"
transform="translate(42.863384,-1.2597997)"
inkscape:original-d="m 62.136618,151.2598 h 15.000001 c 0,0 13,-12 13,-18 0,-13 -1,-20 -18,-34.000003 v -8 h -4 4 c 5,0 5,-7 0,-7 h -10.000001 7.000001 l 3,-8.999999 H 62.136618"
inkscape:path-effect="#path-effect19"
inkscape:highlight-color="#000000" />
<ellipse
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.99998;stroke-dasharray:none;stroke-opacity:1"
id="path23"
cx="101.0117"
cy="77.499992"
rx="0.99999881"
ry="1.0000011" />
<ellipse
style="display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.99998;stroke-dasharray:none;stroke-opacity:1"
id="path23-9"
cx="105"
cy="79"
rx="0.99999881"
ry="1.0000011" />
<ellipse
style="display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.99998;stroke-dasharray:none;stroke-opacity:1"
id="path23-9-4"
cx="108.9883"
cy="77.499992"
rx="0.99999881"
ry="1.0000011" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+116 -59
View File
@@ -30,17 +30,18 @@
*/
/* global define */
(function(root, factory) { // eslint-disable-line
if (typeof define === 'function' && define.amd) {
(function (root, factory) {
// eslint-disable-line
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define([], function() {
define([], function () {
return factory.call(root);
});
} else {
// Browser globals
root.webglUtils = factory.call(root);
}
}(this, function() {
})(this, function () {
"use strict";
const topWindow = this;
@@ -53,8 +54,8 @@
}
if (!isInIFrame()) {
console.log("%c%s", 'color:blue;font-weight:bold;', 'for more about webgl-utils.js see:'); // eslint-disable-line
console.log("%c%s", 'color:blue;font-weight:bold;', 'http://webgl2fundamentals.org/webgl/lessons/webgl-boilerplate.html'); // eslint-disable-line
// console.log("%c%s", 'color:blue;font-weight:bold;', 'for more about webgl-utils.js see:'); // eslint-disable-line
// console.log("%c%s", 'color:blue;font-weight:bold;', 'http://webgl2fundamentals.org/webgl/lessons/webgl-boilerplate.html'); // eslint-disable-line
}
/**
@@ -72,23 +73,27 @@
}
const errorRE = /ERROR:\s*\d+:(\d+)/gi;
function addLineNumbersWithError(src, log = '') {
function addLineNumbersWithError(src, log = "") {
// Note: Error message formats are not defined by any spec so this may or may not work.
const matches = [...log.matchAll(errorRE)];
const lineNoToErrorMap = new Map(matches.map((m, ndx) => {
const lineNo = parseInt(m[1]);
const next = matches[ndx + 1];
const end = next ? next.index : log.length;
const msg = log.substring(m.index, end);
return [lineNo - 1, msg];
}));
return src.split('\n').map((line, lineNo) => {
const err = lineNoToErrorMap.get(lineNo);
return `${lineNo + 1}: ${line}${err ? `\n\n^^^ ${err}` : ''}`;
}).join('\n');
const lineNoToErrorMap = new Map(
matches.map((m, ndx) => {
const lineNo = parseInt(m[1]);
const next = matches[ndx + 1];
const end = next ? next.index : log.length;
const msg = log.substring(m.index, end);
return [lineNo - 1, msg];
})
);
return src
.split("\n")
.map((line, lineNo) => {
const err = lineNoToErrorMap.get(lineNo);
return `${lineNo + 1}: ${line}${err ? `\n\n^^^ ${err}` : ""}`;
})
.join("\n");
}
/**
* Error Callback
* @callback ErrorCallback
@@ -120,7 +125,12 @@
if (!compiled) {
// Something went wrong during compilation; get the error
const lastError = gl.getShaderInfoLog(shader);
errFn(`Error compiling shader: ${lastError}\n${addLineNumbersWithError(shaderSource, lastError)}`);
errFn(
`Error compiling shader: ${lastError}\n${addLineNumbersWithError(
shaderSource,
lastError
)}`
);
gl.deleteShader(shader);
return null;
}
@@ -139,18 +149,24 @@
* @memberOf module:webgl-utils
*/
function createProgram(
gl, shaders, opt_attribs, opt_locations, opt_errorCallback) {
gl,
shaders,
opt_attribs,
opt_locations,
opt_errorCallback
) {
const errFn = opt_errorCallback || error;
const program = gl.createProgram();
shaders.forEach(function(shader) {
shaders.forEach(function (shader) {
gl.attachShader(program, shader);
});
if (opt_attribs) {
opt_attribs.forEach(function(attrib, ndx) {
opt_attribs.forEach(function (attrib, ndx) {
gl.bindAttribLocation(
program,
opt_locations ? opt_locations[ndx] : ndx,
attrib);
program,
opt_locations ? opt_locations[ndx] : ndx,
attrib
);
});
}
gl.linkProgram(program);
@@ -158,18 +174,20 @@
// Check the link status
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
// something went wrong with the link
const lastError = gl.getProgramInfoLog(program);
errFn(`Error in program linking: ${lastError}\n${
shaders.map(shader => {
// something went wrong with the link
const lastError = gl.getProgramInfoLog(program);
errFn(
`Error in program linking: ${lastError}\n${shaders
.map((shader) => {
const src = addLineNumbersWithError(gl.getShaderSource(shader));
const type = gl.getShaderParameter(shader, gl.SHADER_TYPE);
return `${glEnumToString(gl, type)}:\n${src}`;
}).join('\n')
}`);
})
.join("\n")}`
);
gl.deleteProgram(program);
return null;
gl.deleteProgram(program);
return null;
}
return program;
}
@@ -184,12 +202,16 @@
* @return {WebGLShader} The created shader.
*/
function createShaderFromScript(
gl, scriptId, opt_shaderType, opt_errorCallback) {
gl,
scriptId,
opt_shaderType,
opt_errorCallback
) {
let shaderSource = "";
let shaderType;
const shaderScript = document.getElementById(scriptId);
if (!shaderScript) {
throw ("*** Error: unknown script element" + scriptId);
throw "*** Error: unknown script element" + scriptId;
}
shaderSource = shaderScript.text;
@@ -198,20 +220,23 @@
shaderType = gl.VERTEX_SHADER;
} else if (shaderScript.type === "x-shader/x-fragment") {
shaderType = gl.FRAGMENT_SHADER;
} else if (shaderType !== gl.VERTEX_SHADER && shaderType !== gl.FRAGMENT_SHADER) {
throw ("*** Error: unknown shader type");
} else if (
shaderType !== gl.VERTEX_SHADER &&
shaderType !== gl.FRAGMENT_SHADER
) {
throw "*** Error: unknown shader type";
}
}
return loadShader(
gl, shaderSource, opt_shaderType ? opt_shaderType : shaderType,
opt_errorCallback);
gl,
shaderSource,
opt_shaderType ? opt_shaderType : shaderType,
opt_errorCallback
);
}
const defaultShaderType = [
"VERTEX_SHADER",
"FRAGMENT_SHADER",
];
const defaultShaderType = ["VERTEX_SHADER", "FRAGMENT_SHADER"];
/**
* Creates a program from 2 script tags.
@@ -229,13 +254,30 @@
* @memberOf module:webgl-utils
*/
function createProgramFromScripts(
gl, shaderScriptIds, opt_attribs, opt_locations, opt_errorCallback) {
gl,
shaderScriptIds,
opt_attribs,
opt_locations,
opt_errorCallback
) {
const shaders = [];
for (let ii = 0; ii < shaderScriptIds.length; ++ii) {
shaders.push(createShaderFromScript(
gl, shaderScriptIds[ii], gl[defaultShaderType[ii]], opt_errorCallback));
shaders.push(
createShaderFromScript(
gl,
shaderScriptIds[ii],
gl[defaultShaderType[ii]],
opt_errorCallback
)
);
}
return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback);
return createProgram(
gl,
shaders,
opt_attribs,
opt_locations,
opt_errorCallback
);
}
/**
@@ -254,13 +296,30 @@
* @memberOf module:webgl-utils
*/
function createProgramFromSources(
gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) {
gl,
shaderSources,
opt_attribs,
opt_locations,
opt_errorCallback
) {
const shaders = [];
for (let ii = 0; ii < shaderSources.length; ++ii) {
shaders.push(loadShader(
gl, shaderSources[ii], gl[defaultShaderType[ii]], opt_errorCallback));
shaders.push(
loadShader(
gl,
shaderSources[ii],
gl[defaultShaderType[ii]],
opt_errorCallback
)
);
}
return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback);
return createProgram(
gl,
shaders,
opt_attribs,
opt_locations,
opt_errorCallback
);
}
/**
@@ -273,10 +332,10 @@
*/
function resizeCanvasToDisplaySize(canvas, multiplier) {
multiplier = multiplier || 1;
const width = canvas.clientWidth * multiplier | 0;
const height = canvas.clientHeight * multiplier | 0;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
const width = (canvas.clientWidth * multiplier) | 0;
const height = (canvas.clientHeight * multiplier) | 0;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
return true;
}
@@ -289,6 +348,4 @@
createProgramFromSources: createProgramFromSources,
resizeCanvasToDisplaySize: resizeCanvasToDisplaySize,
};
}));
});
+98
View File
@@ -0,0 +1,98 @@
import type { App, Component, DefineComponent } from "vue";
export const cards = {} as Record<string, Component>;
export const tokens = {} as Record<string, Component>;
export async function compileCards() {
const svgs = import.meta.glob("@/assets/images/cards/heron-svg/*.svg", {
eager: true,
});
const components = await Promise.all(
Object.entries(svgs).map(async ([k, v]) => {
const regex = /([\d]+)(?=\.svg)/gm;
const url = (v as any).default as string;
try {
let template: string;
if (process.dev && !process.client) {
template = (await import(/* @vite-ignore */ k + "?raw")).default;
} else {
template = await (await fetch(url)).text();
}
template = template.replace(/\<\?.*\?\>/gm, "");
return [
Number(k.match(regex)![0]).toString(),
// markRaw(
// defineAsyncComponent({
// loader: async () =>
// defineComponent({
// props: ["used_effects_indexes", "cardId"],
// methods: {
// effectClick(effect: number) {
// this.$emit("effectClick", effect);
// },
// },
// template: template,
// }),
// })
// ),
markRaw(
defineComponent({
props: ["used_effects_indexes", "cardId"],
methods: {
effectClick(effect: number) {
this.$emit("effectClick", effect);
},
},
template: template,
})
),
];
} catch (e) {
console.error(e, k);
return [];
}
// let arrayTemplate = template.split("\n");
// arrayTemplate.splice(0, 1);
// template = arrayTemplate.join("\n");
})
);
Object.assign(cards, Object.fromEntries(components));
}
export async function compileTokens() {
const svgs = import.meta.glob("@/assets/images/tokens/*.svg", {
eager: true,
});
const components = await Promise.all(
Object.entries(svgs).map(async ([k, v]) => {
const regex = /([\w]+)(?=\.svg)/gm;
const url = (v as any).default as string;
try {
let template: string;
if (process.dev && !process.client) {
template = (await import(/* @vite-ignore */ k + "?raw")).default;
} else {
template = await (await fetch(url)).text();
}
template = template.replace(/\<\?.*\?\>/gm, "");
return [
k.match(regex)![0].toString(),
markRaw(
defineComponent({
template: template,
})
),
];
} catch (e) {
console.error(e, k);
return [];
}
})
);
Object.assign(tokens, Object.fromEntries(components));
}
+24
View File
@@ -0,0 +1,24 @@
import { defineStore } from "pinia";
import { whoami } from "~/netcode";
export const useAuthStore = defineStore("auth", {
state: () => ({
selfUser: {} as { userId?: string; image?: string; username?: string },
logged: false,
}),
actions: {
async whoami() {
try {
const data = await whoami().then((data) => {
this.logged = true;
return data;
});
Object.assign(this.selfUser, data.value);
} catch (e) {}
},
oauth2_register_discord: () => {
throw Error("not implemented");
},
},
});
+27 -18
View File
@@ -1,25 +1,34 @@
import { defineStore } from "pinia";
enum CardStyles {
HERON = "heron",
ORIGINAL = "original"
export enum CardStyles {
HERON = "heron",
ORIGINAL = "original",
SVG = "heron-svg",
}
export const useSettingsStore = defineStore("settingstore", {
state: () => ({
cardStyle: CardStyles.HERON,
shaderQuality: 3,
shaderFramerate: 0,
shaderPixelated: true
}),
actions: {
switchCardStyles() {
if (this.cardStyle === CardStyles.HERON) {
this.cardStyle = CardStyles.ORIGINAL
} else {
this.cardStyle = CardStyles.HERON
}
}
state: () => ({
cardStyle: CardStyles.SVG,
animationSpeed: 100,
fastAnimations: false,
shaderQuality: 3,
shaderFramerate: 10,
shaderPixelated: true,
saveDiscardSize: false,
discardFullscreen: false,
}),
actions: {
switchCardStyles() {
if (this.cardStyle === CardStyles.HERON) {
this.cardStyle = CardStyles.ORIGINAL;
} else if (this.cardStyle === CardStyles.ORIGINAL) {
this.cardStyle = CardStyles.SVG;
} else {
this.cardStyle = CardStyles.HERON;
}
},
persist: true
},
persist: {
storage: persistedState.localStorage,
},
});
-55
View File
@@ -1,55 +0,0 @@
import { defineStore } from "pinia";
import goStore, { useEffect } from "~/netcode";
import type { Card } from "~/netcode/Card";
import { CardEffects, type Effect } from "~/netcode/Effect";
import type { Player } from "~/netcode/Player";
watch(
() => goStore.current_game?.current_turn?.uuid,
() => {
useClientSideStore().activatedClientside = []
})
export const useClientSideStore = defineStore("clientside", {
state: () => ({
activatedClientside: [] as Effect[],
effectsHandled: new Set([
CardEffects.MAKE_DISCARD,
CardEffects.PLAY_NEXT_CARD_BOUGHT,
CardEffects.RESTACK_DISCARDED_CARD,
CardEffects.RESTACK_DISCARDED_CHAMPION,
CardEffects.SACRIFICE,
CardEffects.STACK_NEXT_ACTION_BOUGHT,
CardEffects.STACK_NEXT_CARD_BOUGHT,
CardEffects.STUN,
CardEffects.PREPARE
]),
}),
getters: {
maskedServerEffects(state) {
return goStore.self?.turn?.effects_availables.filter(e => !state.activatedClientside.includes(e));
},
},
actions: {
activateEffect(effect: Effect) {
const amount =
goStore.self?.turn?.effects_availables.filter((e) => e == effect)
.length ?? 0;
for (let i = 0; i < amount; i++) {
this.activatedClientside.push(effect);
}
},
useEffect(effect: Effect, target: Player | Card) {
const index = this.activatedClientside.indexOf(effect);
if (index > -1) {
this.activatedClientside.splice(index, 1);
useEffect(effect, target);
}
},
findUseEffect(cardEffect: CardEffects, target: Player | Card) {
const effect = this.activatedClientside.find(e => (e.effect == cardEffect) && e.usable)
if (!effect) { return }
this.useEffect(effect, target)
}
},
});
+56
View File
@@ -0,0 +1,56 @@
import { defineStore } from "pinia";
import type { Game } from "~/netcode/interfaces";
// import goStore, { useEffect } from "~/netcode";
// import type { Card } from "~/netcode/Card";
// import { CardEffects, type Effect } from "~/netcode/Effect";
// import type { Player } from "~/netcode/Player";
// watch(
// () => goStore.current_game?.current_turn?.uuid,
// () => {
// useClientSideStore().activatedClientside = []
// })
export const useTurnStore = defineStore("turn", {
// state: () => ({
// activatedClientside: [] as Effect[],
// effectsHandled: new Set([
// CardEffects.MAKE_DISCARD,
// CardEffects.PLAY_NEXT_CARD_BOUGHT,
// CardEffects.RESTACK_DISCARDED_CARD,
// CardEffects.RESTACK_DISCARDED_CHAMPION,
// CardEffects.SACRIFICE,
// CardEffects.STACK_NEXT_ACTION_BOUGHT,
// CardEffects.STACK_NEXT_CARD_BOUGHT,
// CardEffects.STUN,
// CardEffects.PREPARE
// ]),
// }),
// getters: {
// maskedServerEffects(state) {
// return goStore.self?.turn?.effects_availables.filter(e => !state.activatedClientside.includes(e));
// },
// },
// actions: {
// activateEffect(effect: Effect) {
// const amount =
// goStore.self?.turn?.effects_availables.filter((e) => e == effect)
// .length ?? 0;
// for (let i = 0; i < amount; i++) {
// this.activatedClientside.push(effect);
// }
// },
// useEffect(effect: Effect, target: Player | Card) {
// const index = this.activatedClientside.indexOf(effect);
// if (index > -1) {
// this.activatedClientside.splice(index, 1);
// useEffect(effect, target);
// }
// },
// findUseEffect(cardEffect: CardEffects, target: Player | Card) {
// const effect = this.activatedClientside.find(e => (e.effect == cardEffect) && e.usable)
// if (!effect) { return }
// this.useEffect(effect, target)
// }
// },
});