Compare commits

...
Author SHA1 Message Date
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 443bfc128a Merge branch 'main' of git.legonzaur.fr:heronfief/front
release-tag / release-image (push) Successful in 24s
2024-05-24 20:56:38 +02:00
legonzaur a3630abf92 Fix : canvas pixelated not working 2024-05-24 20:56:36 +02:00
Erzangel 74d99aff6e feat(bourred%): card non broken link, hide tooltip (sorta????)
release-tag / release-image (push) Has been cancelled
2024-05-24 20:56:29 +02:00
legonzaur 76814efb18 fix : background of market and playlist
release-tag / release-image (push) Successful in 23s
2024-05-24 20:13:16 +02:00
legonzaur 571a35633b Fix shader 2024-05-24 20:13:05 +02:00
legonzaur 5fa34eda14 Merge pull request 'feat-shader-background' (#95) from feat-shader-background into main
release-tag / release-image (push) Successful in 33s
Reviewed-on: #95
2024-05-24 17:58:19 +00:00
legonzaur 37ad826a80 Merge pull request 'feat/panels-visuals' (#94) from feat/panels-visuals into main
release-tag / release-image (push) Has been cancelled
Reviewed-on: #94
2024-05-24 17:58:06 +00:00
legonzaur 3f8a580984 Better panel background 2024-05-24 19:57:44 +02:00
legonzaur b5f4d91ff8 Animate shader background 2024-05-24 19:15:47 +02:00
legonzaur 351cc06495 Allow using vsync 2024-05-24 17:49:08 +02:00
legonzaur 4ea5c25a47 Feat : handle settings gracefully 2024-05-24 17:19:31 +02:00
legonzaur 766dc92931 Merge branch 'feat/panels-visuals' into feat/shader-background 2024-05-24 14:19:13 +02:00
legonzaur f570b456ca Feat : center current player stats 2024-05-24 14:14:06 +02:00
legonzaur 8c7cc19991 Fix : remove rotation on locked cards too 2024-05-24 13:15:55 +02:00
legonzaur df6d77b870 Feat : more animations and transitions easing 2024-05-24 13:14:19 +02:00
legonzaur b2a3d4f701 Feat : add cards floating animation 2024-05-24 13:04:39 +02:00
legonzaur 6291989dd4 Feat : add background to player banner 2024-05-24 12:40:14 +02:00
legonzaur a6fcc28ced Feat : improve shadow effect 2024-05-24 12:28:36 +02:00
legonzaur 66b6cc61e9 wrap market and player list in containers 2024-05-24 12:14:37 +02:00
Erzangel 90ed8a6211 fix: forgot changes
release-tag / release-image (push) Successful in 23s
2024-05-22 20:41:00 +02:00
Erzangel ceab43e861 feat(rules): add mention of right click zoom 2024-05-22 20:40:41 +02:00
Erzangel ffd9f0187d feat: new favicon attempt
release-tag / release-image (push) Successful in 22s
2024-05-22 20:05:52 +02:00
Erzangel 0305dbe9b7 feat: we are so back, move settings img loc
release-tag / release-image (push) Successful in 25s
2024-05-22 19:47:44 +02:00
Erzangel db4aac845c feat(card): flushed on 002 2024-05-22 19:42:22 +02:00
legonzaur bd6978d392 Fix : prevent render retrigger when color changes 2024-05-22 12:45:34 +02:00
legonzaur 310b3e7b17 Merge remote-tracking branch 'origin/main' into feat-shader-background 2024-05-22 10:33:06 +02:00
legonzaur ca87817b33 WIP : shader Worker 2024-05-22 10:32:42 +02:00
legonzaur 6f7b1bdf03 Merge branch 'main' into feat-shader-background 2024-05-22 10:31:34 +02:00
legonzaur 17a1d23768 Update .gitea/workflows/build.yml
release-tag / release-image (push) Successful in 23s
2024-05-21 20:56:48 +00:00
Erzangel 0915c0a8d8 Merge pull request 'feat: new red cards non-bourred' (#88) from feat/new-red-cards into main
release-tag / release-image (push) Failing after 9s
Reviewed-on: #88
2024-05-21 20:36:21 +00:00
Erzangel 7ae052ee8b feat: new red cards non-bourred 2024-05-21 22:33:29 +02:00
legonzaur d9d8a7a8f0 Merge pull request 'feat/switch-assets' (#86) from feat/switch-assets into main
release-tag / release-image (push) Successful in 1m3s
Reviewed-on: legonzaur/heron-realms-front#86
2024-05-21 19:09:33 +00:00
51 changed files with 1498 additions and 1052 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
env:
DOCKER_ORG: legonzaur
DOCKER_ORG: heronfief
DOCKER_LATEST: nightly
ENDPOINT: git.legonzaur.fr
+1 -24
View File
@@ -2,7 +2,7 @@
import goStore, { oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
await setup_websocket()
const router = useRouter();
useHead
({
@@ -10,27 +10,6 @@ useHead
: '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 }
@@ -85,8 +64,6 @@ onUnmounted(() => {
window.removeEventListener("mouseup", mouseUpHandler)
document.removeEventListener("mousemove", follow_mouse)
})
onDeactivated
</script>
<template>
<div class="root_dom_div">
+5 -3
View File
@@ -16,19 +16,21 @@ uniform float u_time;
void main(){
vec2 st=(gl_FragCoord.xy+u_offset)/u_resolution;
vec2 q=vec2(0.);
q.x=fbm(st);
q.y=fbm(st+vec2(1.));
q=q*.5+.5;
vec2 r=vec2(0.);
r.x=fbm(st+1.*q+vec2(1.7,9.2)+.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.);
}
+214
View File
@@ -0,0 +1,214 @@
import noise from "~/assets/shaders/noise.frag?raw"
import resolveLygia from "~/assets/workers/resolve-lygia.esm"
let interval: number
let timeout: number
const shaderOptions = { quality: 1, framerate: 1000 / 60 }
const screen = { width: 0, height: 0, devicePixelRatio: 0 }
const _colorStart = [0, 0, 0]
const _colorEnd = [0, 0, 0]
let fragmentShader = resolveLygia(noise);
let canvas: OffscreenCanvas
let render: (time: number) => void
let must_resize = false
async function main(canvas: OffscreenCanvas) {
const gl = canvas.getContext("webgl");
if (!gl) {
return;
}
// setup GLSL program
const program = gl.createProgram()
if (!program) { return }
const fragment = gl.createShader(gl.FRAGMENT_SHADER);
if (!fragment) { return }
gl.shaderSource(fragment, fragmentShader)
gl.compileShader(fragment);
if (!gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
const lastError = gl.getShaderInfoLog(fragment);
console.error(lastError)
gl.deleteShader(fragment);
return
}
const vertex = gl.createShader(gl.VERTEX_SHADER);
if (!vertex) { return }
gl.shaderSource(vertex, `attribute vec4 a_position;void main() {gl_Position = a_position;}`)
gl.compileShader(vertex);
if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS)) {
const lastError = gl.getShaderInfoLog(vertex);
console.error(lastError)
gl.deleteShader(vertex);
return
}
gl.attachShader(program, fragment)
gl.attachShader(program, vertex)
gl.linkProgram(program)
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) { return }
// const program = webglUtils.createProgramFromSources(gl, [vs, fragmentShader2]);
// look up where the vertex data needs to go.
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
// look up uniform locations
const offset = gl.getUniformLocation(program, "u_offset");
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const colorStart = gl.getUniformLocation(program, "u_color_start");
const quality = gl.getUniformLocation(program, "u_quality");
const colorEnd = gl.getUniformLocation(program, "u_color_end");
const timeLocation = gl.getUniformLocation(program, "u_time");
// Create a buffer to put three 2d clip space points in
const positionBuffer = gl.createBuffer();
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// fill it with a 2 triangles that cover clipspace
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, // first triangle
1, -1,
-1, 1,
-1, 1, // second triangle
1, -1,
1, 1,
]), gl.STATIC_DRAW);
const gl2 = gl
function render(time: number) {
if (must_resize) {
canvas.width = screen.width * shaderOptions.quality / 10
canvas.height = screen.height * shaderOptions.quality / 10
must_resize = false;
}
time *= 0.001; // convert to seconds
// Tell WebGL how to convert from clip space to pixels
gl2.viewport(0, 0, gl2.canvas.width, gl2.canvas.height);
// Tell it to use our program (pair of shaders)
gl2.useProgram(program);
// Turn on the attribute
gl2.enableVertexAttribArray(positionAttributeLocation);
// Bind the position buffer.
gl2.bindBuffer(gl2.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
gl2.vertexAttribPointer(
positionAttributeLocation,
2, // 2 components per iteration
gl2.FLOAT, // the data is 32bit floats
false, // don't normalize the data
0, // 0 = move forward size * sizeof(type) each iteration to get the next position
0, // start at the beginning of the buffer
);
const secondRatio = screen.width / screen.height
if (secondRatio < 1) {
const half = canvas.height / 2
gl2.uniform2f(resolutionLocation, canvas.width, canvas.height * secondRatio);
gl2.uniform2f(offset, 0, -half + ((half) / (1 / secondRatio)));
} else {
const half = canvas.width / 2
gl2.uniform2f(resolutionLocation, canvas.width / secondRatio, canvas.height);
gl2.uniform2f(offset, -half + ((half) / secondRatio), 0);
}
gl2.uniform1i(quality, shaderOptions.quality)
gl2.uniform3f(colorStart, _colorStart[0], _colorStart[1], _colorStart[2]);
gl2.uniform3f(colorEnd, _colorEnd[0], _colorEnd[1], _colorEnd[2]);
gl2.uniform1f(timeLocation, time);
gl2.drawArrays(
gl2.TRIANGLES,
0, // offset
6, // num vertices to process
);
if (timeout) {
self.clearTimeout(timeout)
timeout = self.setTimeout(() => {
requestAnimationFrame(render)
}, 1000 / shaderOptions.framerate)
}
}
requestAnimationFrame(render)
// self.clearInterval(interval)
// interval = self.setInterval(() => requestAnimationFrame(render), 1000 / shaderOptions.framerate)
self.clearTimeout(timeout)
if (shaderOptions.framerate == 0) {
return render
}
timeout = self.setTimeout(() => {
requestAnimationFrame(render)
}, 1000 / shaderOptions.framerate)
return render
}
self.addEventListener("message", (e) => {
const data = e.data
if (data.command == "initShader") {
canvas = data.canvas
shaderOptions.quality = data.shaderOptions.quality
shaderOptions.framerate = data.shaderOptions.framerate
if (shaderOptions.framerate == 65) {
shaderOptions.framerate == Infinity
}
screen.devicePixelRatio = data.screen.devicePixelRatio
screen.width = data.screen.width
screen.height = data.screen.height
loadShader()
} else if (data.command == "killShader") {
console.log("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
}
}
+52
View File
@@ -0,0 +1,52 @@
function getFile(url) {
let httpRequest = new XMLHttpRequest();
httpRequest.open("GET", url, false);
httpRequest.send();
if (httpRequest.status == 200)
return httpRequest.responseText;
else
return "";
}
function resolveLygia(lines) {
if ( !Array.isArray(lines) ) {
lines = lines.split(/\r?\n/);
}
let src = "";
lines.forEach( (line, i) => {
const line_trim = line.trim();
if (line_trim.startsWith('#include \"lygia') ) {
let include_url = line_trim.substring(15);
include_url = "https://lygia.xyz" + include_url.replace(/\"|\;|\s/g,'');
src += getFile(include_url) + '\n';
}
else {
src += line + '\n';
}
});
return src;
}
async function resolveLygiaAsync(lines) {
if (!Array.isArray(lines))
lines = lines.split(/\r?\n/);
let src = "";
const response = await Promise.all(
lines.map(async (line, i) => {
const line_trim = line.trim();
if (line_trim.startsWith('#include "lygia')) {
let include_url = line_trim.substring(15);
include_url = "https://lygia.xyz" + include_url.replace(/\"|\;|\s/g, "");
return fetch(include_url).then((res) => res.text());
}
else
return line;
})
);
return response.join("\n");
}
export { resolveLygia as default };
+91 -116
View File
@@ -1,151 +1,121 @@
<script setup lang="ts">
import noise from "~/assets/shaders/noise.frag?raw"
const canvas = ref<HTMLCanvasElement | null>(null)
const settingsStore = useSettingsStore()
declare const webglUtils: {
createProgramFromSources: (gl: WebGLRenderingContext, array: [string, string]) => WebGLProgram,
resizeCanvasToDisplaySize: (e: HTMLCanvasElement | OffscreenCanvas) => any
}
declare const resolveLygiaAsync: (source: string) => Promise<string>
import gsap from 'gsap';
import ShaderWorker from '~/assets/workers/ShaderWorker?worker'
import goStore from '~/netcode';
const worker = new ShaderWorker()
const canvas = ref<HTMLCanvasElement | null>(null)
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null)
export interface Props {
colorStart?: [number, number, number]
colorEnd?: [number, number, number]
}
const props = withDefaults(defineProps<Props>(), {
colorStart: () => [0, .3, 0],
colorEnd: () => [0, 1, 0]
})
const targetFramerate = 1000 / settingsStore.shaderFramerate
const interval = ref<ReturnType<typeof setInterval>>()
let fragmentShader = await resolveLygiaAsync(noise);
async function main(canvas: HTMLCanvasElement, fragmentShader2: string) {
// Get A WebGL context
/** @type {HTMLCanvasElement} */
const gl = canvas.getContext("webgl");
if (!gl) {
return;
}
const vs = `attribute vec4 a_position;void main() {gl_Position = a_position;}`;
// setup GLSL program
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 _colorStart = reactive(props.colorStart)
const _colorEnd = reactive(props.colorEnd)
const gl2 = gl
function render(time: number) {
time *= 0.001; // convert to seconds
webglUtils.resizeCanvasToDisplaySize(gl2.canvas);
// 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 bounds = canvas.getBoundingClientRect()
const aspectRatio = Math.max(window.screen.width, window.screen.height) * devicePixelRatio
gl2.uniform2f(offset, bounds.left, -bounds.bottom);
// console.log(canvas.getBoundingClientRect().left * window.devicePixelRatio)
gl2.uniform1i(quality, settingsStore.shaderQuality)
gl2.uniform2f(resolutionLocation, aspectRatio, aspectRatio);
gl2.uniform3f(colorStart, props.colorStart[0], props.colorStart[1], props.colorStart[2]);
gl2.uniform3f(colorEnd, props.colorEnd[0], props.colorEnd[1], props.colorEnd[2]);
gl2.uniform1f(timeLocation, time);
gl2.drawArrays(
gl2.TRIANGLES,
0, // offset
6, // num vertices to process
);
}
clearInterval(interval.value)
interval.value = setInterval(() => requestAnimationFrame(render), targetFramerate)
}
async function reloadShader() {
nextTick(() => {
function initShader() {
if (!canvas.value) { return }
let fragmentShader2 = fragmentShader.replace("#define FBM_OCTAVES 6", "#define FBM_OCTAVES " + settingsStore.shaderQuality);
clearInterval(interval.value)
main(canvas.value, fragmentShader2);
})
const offscreen = canvas.value?.transferControlToOffscreen()
worker.postMessage({
command: "initShader",
canvas: offscreen,
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
}, [offscreen])
if (settingsStore.shaderFramerate == 0) {
killShader()
}
watch(() => settingsStore.shaderQuality, (newValue, oldValue) => {
reloadShader()
resizeShader()
}
function recolorShader() {
worker.postMessage({
command: 'recolorShader',
colorStart: [..._colorStart],
colorEnd: [..._colorEnd]
})
}
function resizeShader() {
worker.postMessage({
command: 'resizeShader',
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
}
function killShader() {
worker.postMessage({
command: 'killShader'
})
}
function resumeShader() {
worker.postMessage({
command: 'resumeShader',
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
}
watch(() => [settingsStore.shaderFramerate], () => {
if (settingsStore.shaderFramerate == 0) {
killShader()
return
}
resumeShader()
})
watch(() => settingsStore.shaderFramerate, (newValue, oldValue) => {
reloadShader()
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 })
} 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 })
}
}, { immediate: true })
watch([_colorEnd, _colorStart], () => {
recolorShader()
})
onMounted(async () => {
reloadShader()
nextTick(() => {
recolorShader()
initShader()
window.addEventListener("resize", resizeShader, true)
})
})
onUnmounted(() => {
clearInterval(interval.value)
window.removeEventListener("resize", resizeShader)
killShader()
})
</script>
<template>
<canvas v-if="settingsStore.shaderQuality != 0" ref="canvas" id="background">test</canvas>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
height="270">test</canvas>
</template>
<style scoped>
@@ -154,5 +124,10 @@ onUnmounted(() => {
position: absolute;
height: 100%;
width: 100%;
image-rendering: smooth;
}
#background.pixelated {
image-rendering: pixelated;
}
</style>
+56 -14
View File
@@ -11,7 +11,8 @@ export interface Props {
const props = withDefaults(defineProps<Props>(), {
wrapped: () => false,
tilted: () => true
tilted: () => true,
locked: () => true
})
@@ -21,6 +22,8 @@ const settingsStore = useSettingsStore()
const childCount = ref<number>(0)
const card = ref<HTMLElement>()
function cardClick() {
if (!props.card_id) return
emit("click")
@@ -41,10 +44,15 @@ watch(
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 :class="`card ${childCount > 0 ? 'active' : ''} ${wrapped ? 'wrapped' : ''} ${props.locked ? 'locked' : ''}`"
<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) }">
@@ -66,15 +74,18 @@ watch(
<style scoped>
.card {
transition: 0.025s rotate linear;
--max-height: 250px;
--margin-bottom: calc(var(--max-height) * 0.05);
max-width: 256px;
position: relative;
display: flex;
justify-content: center;
margin-bottom: 0px;
margin-top: 15px;
transition: all .125s cubic-bezier(0.075, 0.82, 0.165, 1);
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 {
@@ -82,9 +93,9 @@ watch(
}
.card_image {
max-height: 250px;
transition: all .125s cubic-bezier(0.075, 0.82, 0.165, 1);
max-height: var(--max-height);
z-index: 1;
height: var(--max-height);
}
.card_image.outlined {
@@ -92,18 +103,49 @@ watch(
outline-offset: -2px;
}
.card.locked {
@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: 15px;
filter: drop-shadow(5px 15px 2px rgba(0, 0, 0, .60));
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;
max-height: 6px;
margin: 0;
}
.card_image {
max-height: 250px;
.card.wrapped .card_image {
filter: drop-shadow(.5px .5px .5px gray) drop-shadow(-.5px -.5px .5px gray);
}
#contextMenuButtons {
@@ -115,7 +157,7 @@ watch(
bottom: 0;
width: 99%;
transform: translateY(-0px);
transition: 0.1s transform cubic-bezier(0.075, 0.82, 0.165, 1);
transition: 0.1s transform cubic-bezier(1, 1.81, .59, 1.42);
}
.card.active:hover #contextMenuButtons {
+5 -8
View File
@@ -12,7 +12,6 @@ const props = withDefaults(defineProps<Props>(), {
</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">
@@ -26,27 +25,25 @@ const props = withDefaults(defineProps<Props>(), {
</template>
</div>
</div>
</template>
<style scoped>
.cards_container::before {
width: calc(185px - 10px);
height: calc(250px - 10px);
display: block;
content: "";
height: 0;
width: 0;
}
.cards_container {
display: flex;
flex-direction: row-reverse;
flex-direction: column-reverse;
justify-content: center;
min-width: 200px;
}
.empty {
max-width: 10px;
max-height: 10px;
}
.empty>.card_image {
-128
View File
@@ -1,128 +0,0 @@
<script setup lang="ts">
import goStore from '~/netcode';
import type { Player } from '~/netcode/Player';
import { playSound } from '~/helpers/audioHelpers';
const current_players = computed<Array<Player>>(() => Array.from(goStore.current_game?.players ?? []).filter(p => p.team === goStore.current_game?.current_turn))
watch(
() => goStore.current_game?.current_turn?.uuid,
() => {
if (goStore.current_game?.current_turn?.uuid == goStore.self?.team?.uuid) {
playSound('/sounds/ding.mp3')
}
})
watch(
() => goStore.current_game?.losers,
() => {
if (goStore.self?.uuid && goStore.current_game?.losers && goStore.current_game.losers.includes(goStore.self.uuid)) {
showLoserScreen()
}
})
watch(
() => goStore.current_game?.ended,
() => {
if (goStore.current_game?.ended && goStore.self?.uuid && goStore.current_game?.losers && !goStore.current_game.losers.includes(goStore.self.uuid)) {
showWinnerScreen()
}
})
watch(
() => goStore.current_game?.players,
(newList, oldList) => {
if (!oldList || !newList) { return }
if (goStore.current_game?.started) { return }
if (newList?.length > oldList.length) {
playSound('/sounds/teleport.mp3')
}
})
const showResults = ref(false)
const 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">
<div id="market">
<MarketCards></MarketCards>
</div>
<div id="player_list">
<AnimatedBackground />
<PlayerListElement v-for="p in goStore.current_game?.players" :player="p">
</PlayerListElement>
</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 400px;
grid-template-rows: min-content 1fr;
grid-template-areas:
"market player_list"
"current_player player_list";
height: 100%;
}
#player_list,
#current_player {
overflow: auto;
position: relative;
isolation: isolate;
background: url('/images/bg-darkgrass.png');
}
#market {
grid-area: market;
background-image: url("/images/bg-sand.png");
border-bottom: 3px solid black;
;
}
#current_player {
grid-area: current_player;
}
#player_list {
grid-area: player_list;
border-left: 5px solid black;
height: 100%;
background-image: url("/images/bg-darkgrass.png");
}
</style>
<style>
#player_list .card_image {
max-width: 64px;
}
</style>
+1 -1
View File
@@ -7,7 +7,7 @@ const displayOptions = ref(false)
<template>
<div :class="`card_overlay`" ref="card">
<img class="settings_button" src="/settings.png" alt="settings" @click="displayOptions = true"/>
<img class="settings_button" src="/images/settings.png" alt="settings" @click="displayOptions = true"/>
</div>
<OptionsDialog :isVisible="displayOptions" @close="displayOptions = false" />
</template>
+52 -4
View File
@@ -2,21 +2,43 @@
import goStore, { buy, useEffect } from '@/netcode';
import { CardEffects, type Effect } from '@/netcode/Effect'
import { CardType } from '@/netcode/Card'
import gsap from 'gsap'
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 onBeforeEnter(el: Element) {
(el as HTMLElement).style.visibility = "hidden"
const element = el as HTMLElement
element.style.top = element.offsetTop + "px"
element.style.left = element.offsetLeft + "px"
element.style.zIndex = "999"
}
function onEnter(el: any, done: () => unknown) {
console.log("enter")
// setTimeout(() => {
// el.style.visibility = null
// done()
// }, 1000)
done()
}
</script>
<template>
<div id="market">
<AnimatedBackground :color-end="[1, 1, 0]" :color-start="[.3, .3, 0]"></AnimatedBackground>
<CardPreview :tilted="false" :card_id="undefined"></CardPreview>
<div class="separator"></div>
<TransitionGroup name="market_card">
<CardPreview :tilted="false" :card_id="c?.card_id" v-for="c in goStore.current_game?.market"
@click="c && buy(c)">
:key="c?.card_id" @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>
@@ -32,6 +54,7 @@ const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
BUY IN STACK (actions)</button>
</template>
</CardPreview>
</TransitionGroup>
<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)!)">
@@ -53,8 +76,26 @@ const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
</template>
<style scoped>
/* .market_card-leave-active,
.market_card-move {
display: none;
}
.market_card-enter-from {
transform: translate(-500px);
}
.market_card-enter-active,
.market_card-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
} */
</style>
<style scoped>
#market {
width: min-content;
display: flex;
flex-direction: row;
justify-content: center;
@@ -62,8 +103,15 @@ const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
z-index: 2;
position: relative;
gap: 2px;
padding: 10px;
;
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 {
+17 -16
View File
@@ -6,6 +6,9 @@ export interface Props {
isVisible: boolean;
}
const _devicePixelRatio = computed(() => devicePixelRatio)
const _window = computed(() => window)
const props = withDefaults(defineProps<Props>(), {
})
@@ -15,10 +18,6 @@ const close = () => {
emit('close')
}
watch(() => settingsStore.shaderQuality, async () => {
console.log(settingsStore.shaderQuality)
})
</script>
<template>
@@ -40,27 +39,29 @@ watch(() => settingsStore.shaderQuality, async () => {
<span>Shader Quality</span>
</div>
<div class="dialog-body">
<input type="range" min="0" max="10" v-model="settingsStore.shaderQuality">
<input type="range" min="1" max="10" v-model="settingsStore.shaderQuality"
:disabled="settingsStore.shaderFramerate == 0">
</div>
<div class="dialog-footer">
<span>
<template v-if="settingsStore.shaderQuality == 0">
Disabled
</template>
<template v-if="settingsStore.shaderQuality > 0">
{{ settingsStore.shaderQuality }}
</template>
</span>
<span>{{ Math.floor(_window.innerWidth * _devicePixelRatio * settingsStore.shaderQuality / 10) }}x{{
Math.floor(_window.innerHeight *
_devicePixelRatio * settingsStore.shaderQuality / 10) }}</span>
</div>
<div class="dialog-header">
<span>Shader Framerate</span>
</div>
<div class="dialog-body">
<input type="range" min="10" max="60" step="5" v-model="settingsStore.shaderFramerate"
:disabled="settingsStore.shaderQuality == 0">
<input type="range" min="0" max="65" step="5" v-model="settingsStore.shaderFramerate">
</div>
<div class="dialog-footer">
<span>{{ settingsStore.shaderFramerate }} fps</span>
<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>
+1 -1
View File
@@ -12,7 +12,7 @@ const props = defineProps<Props>()
<template>
<div class="player-icon-container">
<img class="avatar" v-if="props.player.image" :src="props.player.image">
<img class="avatar" v-if="props.player.image" :src="props.player.image" draggable="false">
<b class="player-name">{{ props.player.username }}</b>
</div>
</template>
View File
+89 -116
View File
@@ -2,152 +2,125 @@
import goStore from '@/netcode';
import type { Player } from "~/netcode/Player";
const emit = defineEmits(['click'])
const view = ref<"main" | "discard">("main")
function show(data: "main" | "discard") {
view.value = data
}
export interface Props {
player: Player;
focused?: 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/${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 class="player_list_element" @click="$emit('click')"
:class="{ self: props.player == goStore.self, focused: props.focused == props.player, isTurn: props.player.team == goStore.current_game?.current_turn }">
<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="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 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 class="discard_button" @click="show('discard')">
<div class="tooltip">Discard</div>
<img :src="`/cards/${player.discard_pile.at(-1)?.card_id.toString().padStart(3, '0') ?? 'background'}.png`"
draggable="false" :class="view === 'discard' && 'selected'">
<span class="button_card_number">{{ player.discard_pile.length }}</span>
</div>
</div>
<div class="view">
<PlayerSlot :player="player" :hide_stack_and_discard="true" v-if="view == 'main'" />
<template v-if="view == 'discard'">
<CardPreview :tilted="false" v-for=" card in player.discard_pile " :card_id="card.card_id">
</CardPreview>
<div id="player_stats">
<template v-if="goStore.current_game?.started">
<PlayerStats :player="props.player">
</PlayerStats>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.button>div>img:not(.selected) {
filter: brightness(.5)
}
.tooltip {
visibility: visible;
position: absolute;
transition: .1s transform;
background: white;
right: 0;
border-top-right-radius: 1em;
border-bottom-right-radius: 1em;
padding-right: 1em;
}
.button {
display: flex;
flex-direction: column;
align-items: stretch;
z-index: 1;
}
.button>div {
flex: 1 1 0;
border-right: 5px solid black;
width: 45px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.button>div:not(.stack_button) {
cursor: pointer;
}
.button div:hover .tooltip {
visibility: visible;
transform: translateX(100%);
}
.button_card_number {
font-size: 2em;
position: absolute;
text-shadow: white 0 0 1px;
}
.button>div:not(:last-child) {
border-bottom: 5px solid black;
}
.player_list_element {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-areas:
"button player";
border-bottom: 5px solid black;
min-height: 200px;
position: relative;
}
.player_list_element.self {
background-image: url('/images/bg-grass.png');
}
.view {
border-radius: 10px;
margin: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, .60));
transform: translate(-2.5px, -7.5px);
backdrop-filter: blur(5px);
display: flex;
justify-content: center;
box-sizing: border-box;
transition: all .1s;
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: .5;
}
.player_list_element.focused {
filter: unset;
transform: unset;
}
.player_list_element.isTurn::before {
opacity: .1;
}
.player_list_element.self::before {
background: rgb(33, 177, 76);
}
#player_banner {
display: flex;
grid-area: player_stats;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
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;
}
</style>
<style>
#player_list_buttons img {
max-width: 100%;
object-fit: contain;
position: absolute;
display: block;
top: 0;
bottom: 0;
margin: auto 0;
.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);
}
.view #contextMenuButtons button:not(.enemy_action) {
display: none
.ready {
background-color: green;
}
.not-ready {
background-color: maroon;
}
</style>
+42 -167
View File
@@ -26,7 +26,7 @@ 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)
@@ -38,8 +38,8 @@ 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 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)
@@ -60,7 +60,6 @@ function can_buy_fire_gem(): boolean {
function checkEndTurn() {
// TODO: Add a confirmation dialog if resources are left
console.log("Checking end turn")
if (
(!warning.value)
&& has_actions_remaining()) {
@@ -92,71 +91,19 @@ function check_for_guard(player: Player) {
</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" v-if="isSelfTurn" class="end_turn_button turn_icon"
@click="checkEndTurn">END
TURN</button>
<div class="player" :class="{ turn: isPlayerTurn, self: isSelf }">
<div id="player_banner" class="big">test</div>
<div id="turn_buttons">
<button id="end_turn" class="end_turn_button turn_icon" :class="{ hidden: !isSelfTurn }"
@click="checkEndTurn">END TURN</button>
<div class="warning"
:class="{ 'warning-fire-gem': warningText == 'P\'tite Fire gem?', 'visible': warning }">
/!\<br>{{ warningText }}
</div>
</template>
</div>
</template>
</div>
</div>
<div id="current_player_board">
<div class="other_player_buttons_container">
<button @click.stop="damage_team(props.player.team)"
@@ -180,6 +127,7 @@ function check_for_guard(player: Player) {
<div class="cards_container">
<!-- HAND -->
<template v-if="props.player != goStore.self">
<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"
@@ -187,6 +135,8 @@ function check_for_guard(player: Player) {
DISCARD
</button>
</CardPreview>
</template>
<!-- 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">
@@ -270,11 +220,22 @@ function check_for_guard(player: Player) {
background-color: rgba(255, 165, 0, 0.7);
}
#turn_buttons {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
#end_turn {
z-index: 10;
height: 100%;
}
#end_turn.hidden {
visibility: hidden
}
.stack,
.discard,
.cards_container {
@@ -302,74 +263,24 @@ function check_for_guard(player: Player) {
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;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
/* border-top: 5px solid black; */
grid-template-areas:
"player_stats player_stats"
"stack player"
"discard player";
"player_stats"
"player";
transition: all 1s;
margin-bottom: -75px;
padding-bottom: 75px;
}
.discard_overlay {
grid-template-areas:
"player_stats player_stats"
"stack player";
justify-items: center;
/* margin-bottom: -75px;
padding-bottom: 75px; */
}
#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;
@@ -451,44 +362,24 @@ function check_for_guard(player: Player) {
}
#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>#player_info {
margin-left: 500px;
#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;
@@ -534,22 +425,6 @@ function check_for_guard(player: Player) {
/* 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;
+5 -5
View File
@@ -17,20 +17,20 @@ const props = defineProps<Props>()
{{ player.team?.health }}
</span>
</div>
<div class="turn_icon">
<!-- <div class="turn_icon" v-if="showTurnData">
<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">
<div class="turn_icon" v-if="showTurnData">
<img src="/images/damage.png" class="icon_img" draggable="false" />
<span class="turn_icon_number">
{{ player.turn?.damage_reserve }}
</span>
</div>
</div> -->
<div class="turn_icon">
<span class="icon_img">🖕</span>
<img src="/images/fuck.svg" class="icon_img" draggable="false" />
<span class="turn_icon_number">{{ (player.turn?.discard_markers ?? 0) + (player.turn?.fuck_u_markers ?? 0)
}}</span>
</div>
@@ -47,11 +47,11 @@ const props = defineProps<Props>()
display: flex;
justify-content: center;
align-items: center;
height: 50px;
}
img.icon_img {
height: 50px;
width: 50px;
}
span.icon_img {
+2
View File
@@ -45,7 +45,9 @@ const close = () => {
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'" />
</p>
</div>
</div>
</div>
-317
View File
@@ -1,317 +0,0 @@
<script setup lang="ts">
import type { Card } from '~/netcode/Card';
const router = useRouter();
</script>
<template>
<div class="background">
<div class="overlay-2">
<div class="erika-zone-2">
<img @click="router.push({ path: '/' })" class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/images/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse" />
</div>
</div>
<div class="text-content">
<h1>Règles de Héron Realms</h1>
<section>
<p>Héron Realms, c'est un jeu de <span style="text-decoration:line-through;">héron building</span>
deck-building.</p>
<p>Tout le monde part d'un même deck de base.</p>
<p>Le but du jeu est de l'enrichir pour obtenir de quoi gagner.</p>
<p>Et comme la violence est une solution universelle, ça se traduit en battre ses adversaires.</p>
<p>&#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>
</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>
-4
View File
@@ -103,8 +103,4 @@ import goStore, { useEffect, playCard, playAllCards, discard, lockCard, heal, da
transform: translateY(-40px);
}
.cards_container {
min-width: 250px;
}
</style>
+66
View File
@@ -0,0 +1,66 @@
import goStore, { oauth2_register_discord, setup_websocket, heron_cookie_login, join_game } from "@/netcode";
export default defineNuxtRouteMiddleware
(async (to, from) => {
await setup_websocket()
if (to.path == '/login') {
return
}
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("/games")) {
if (Array.isArray(to.params.id)) { return navigateTo('/lobby') }
try {
await join_game(to.params.id)
return
} catch {
return navigateTo("/lobby")
}
}
if (to.path == "/lobby") {
return
}
if (to.path == "/rules") {
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('/')
}
})
+40
View File
@@ -15,6 +15,7 @@ import { type Lobby, create_lobby, type LobbyNetcode } from "./Lobby";
import { type Ref } from "vue";
const socketTimeout = 1000
let socket: WebSocket;
export class GameObjectRegister {
@@ -25,6 +26,8 @@ export class GameObjectRegister {
readonly turns: Map<string, Turn>;
readonly games: Map<string, Game>;
readonly gameRoutingCallbacks: Map<string, [() => void, () => void]>;
readonly loginCallbacks: Set<() => void>;
context: "lobby" | "login" | "pregame" | "game" | "rules" = "login";
private readonly objects: Map<string, GameObjectNetcode>;
current_game: Ref<Game | null>;
@@ -37,6 +40,9 @@ export class GameObjectRegister {
clientMouseY: Ref<number>
discordId: Ref<string>
constructor() {
this.gameRoutingCallbacks = new Map();
this.loginCallbacks = new Set();
this.effects = reactive(new Map());
this.cards = reactive(new Map());
this.teams = reactive(new Map());
@@ -117,6 +123,12 @@ export class GameObjectRegister {
}
setCurrentGame = (uuid: string) => {
this.current_game.value = this.games.get(uuid) ?? null;
this.gameRoutingCallbacks.get(uuid)?.[0]()
this.gameRoutingCallbacks.delete(uuid)
for (let [_key, value] of this.gameRoutingCallbacks) {
value[1]()
this.gameRoutingCallbacks.delete(_key)
}
};
}
@@ -179,6 +191,8 @@ export const setup_websocket = () =>
heron_session.value = data.data;
} else if (data.data_type == "self_discord_id") {
goStore.setDiscordId(data.data)
goStore.loginCallbacks.forEach(c => c())
goStore.loginCallbacks.clear()
}
} else {
// console.log(data.type, data.data_type, data.data);
@@ -188,6 +202,8 @@ export const setup_websocket = () =>
export async function heron_cookie_login(cookie: string) {
await setup_websocket();
return new Promise<void>((resolve, reject) => {
goStore.loginCallbacks.add(resolve)
socket.send(
JSON.stringify({
type: "cookie_login",
@@ -195,10 +211,20 @@ export async function heron_cookie_login(cookie: string) {
data: cookie,
})
);
setTimeout(() => {
goStore.loginCallbacks.delete(resolve);
reject()
}
, socketTimeout)
})
}
export async function oauth2_register_discord(code: string) {
await setup_websocket();
return new Promise<void>((resolve, reject) => {
goStore.loginCallbacks.add(resolve)
socket.send(
JSON.stringify({
type: "discord_login",
@@ -206,6 +232,12 @@ export async function oauth2_register_discord(code: string) {
data: code,
})
);
setTimeout(() => {
goStore.loginCallbacks.delete(resolve);
reject()
}
, socketTimeout)
})
}
export async function register(username: string) {
@@ -364,6 +396,13 @@ export async function netcode_create_game() {
export async function join_game(game_uuid: string) {
await setup_websocket();
return new Promise<void>(((resolve, reject) => {
goStore.gameRoutingCallbacks.set(game_uuid, [resolve, reject])
setTimeout(() => {
const cb = goStore.gameRoutingCallbacks.get(game_uuid)
if (!cb) { return }
cb[1]()
}, socketTimeout)
socket.send(
JSON.stringify({
type: "join",
@@ -371,5 +410,6 @@ export async function join_game(game_uuid: string) {
data: game_uuid,
})
);
}))
}
export default goStore;
Submodule netcode/interfaces added at 89c03fe650
-9
View File
@@ -12,13 +12,4 @@ export default defineNuxtConfig({
'@pinia/nuxt',
'@pinia-plugin-persistedstate/nuxt',
],
app: {
head: {
script: [{
src: "/webgl-utils.js"
}, {
src: "https://lygia.xyz/resolve.js"
}]
}
},
})
+7
View File
@@ -8,6 +8,7 @@
"hasInstallScript": true,
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"gsap": "^3.12.5",
"nuxt": "^3.11.2",
"pinia": "^2.1.7",
"vue": "^3.4.21",
@@ -5670,6 +5671,12 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/gsap": {
"version": "3.12.5",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
},
"node_modules/gzip-size": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
+1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"gsap": "^3.12.5",
"nuxt": "^3.11.2",
"pinia": "^2.1.7",
"vue": "^3.4.21",
+276
View File
@@ -0,0 +1,276 @@
<script setup lang="ts">
import goStore, { setReady } from '../../netcode';
import type { Player } from '../../netcode/Player';
import { playSound } from '../../helpers/audioHelpers';
import { CardType, type Card } from '../../netcode/Card';
import { CardEffects, type Effect } from '../../netcode/Effect';
import type { Team } from '~/netcode/Team';
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
}
const discard_unwrapped = ref(false)
const focusedPlayer = ref<Player>((goStore.current_game?.players.find(p => p.team == goStore.current_game?.current_turn) ?? goStore.current_game?.players[0])!)
const clientStore = useClientSideStore()
const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
function focusPlayer(p: Player) {
focusedPlayer.value = p
}
watch<Team>(() => goStore.current_game!.current_turn, (new_team, old_team) => {
if (focusedPlayer.value.team != old_team) { return }
const first_player = goStore.current_game?.players.find(p => p.team == goStore.current_game?.current_turn)
if (!first_player) {
return
}
focusPlayer(first_player)
})
</script>
<template>
<div @contextmenu.prevent="false" id="game_board">
<AnimatedBackground></AnimatedBackground>
<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>
<div id="stack_discard">
<div id="stack">
<template v-for="p in current_players">
<CardsGrouped :stack="p.stack_pile">
</CardsGrouped>
</template>
</div>
<div id="discard" :class="{ invisible: discard_unwrapped }" @click="() => discard_unwrapped = true">
<template v-for="p in current_players">
<CardsGrouped :stack="p.discard_pile" :visible="!discard_unwrapped">
</CardsGrouped>
</template>
<template v-for="p in current_players">
<div class="discard_overlay" v-if="discard_unwrapped">
<CardPreview :tilted="false" v-if="discard_unwrapped" :card_id="c.card_id"
v-for="c in p.discard_pile">
<template
v-if="goStore.self?.turn && c && p == goStore.self && goStore.current_game?.current_turn == goStore.self.team">
<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="{ unwrapped: discard_unwrapped }"
@click.stop="() => { discard_unwrapped = false; console.log(discard_unwrapped); }"></div>
</template>
</div>
</div>
<MarketCards></MarketCards>
<div id="player_list_container">
<div id="player_list">
<PlayerListElement v-for="p in goStore.current_game?.players" :player="p" @click="focusPlayer(p)"
:focused="focusedPlayer">
</PlayerListElement>
</div>
</div>
<div id="current_player">
<!-- <AnimatedBackground :color-start="goStore.self == p ? [.0, .3, .3] : undefined"
:color-end="goStore.self == p ? [.0, 1, 1] : undefined" /> -->
<PlayerSlot v-if="focusedPlayer" :player="focusedPlayer"></PlayerSlot>
</div>
<ResultDialog :isVisible="showResults" :won="isWinner" :players="goStore.current_game?.players"
@close="showResults = false">
</ResultDialog>
<GlobalOverlay></GlobalOverlay>
<SelfView></SelfView>
</div>
</template>
<style scoped>
#game_status {
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, .75);
}
#game_board {
display: grid;
grid-template-columns: 1fr min-content 1fr;
grid-template-rows: min-content min-content 1fr;
grid-template-areas:
"stack_discard market empty"
"player_list player_list player_list"
"current_player current_player current_player";
height: 100%;
}
#player_list,
#current_player {
overflow: auto;
position: relative;
isolation: isolate;
/* background: url('/images/bg-darkgrass.png'); */
}
#current_player {
grid-area: current_player;
}
#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("/images/bg-darkgrass.png"); */
}
#player_list {
/* border: 5px solid #000; */
display: flex;
}
</style>
<style>
#player_list .card {
--max-height: 90px;
}
</style>
+1 -23
View File
@@ -1,24 +1,11 @@
<script setup lang="ts">
import goStore, { setReady } from "@/netcode";
</script>
<template>
<div class="root_page_div">
<div id="game_status">
<template v-if="goStore.self && !goStore.current_game?.started">
<input @click.prevent="setReady(($event.target as HTMLInputElement).checked)" type="checkbox" id="ready"
:checked="goStore.self?.ready" />
<label for="ready">Ready</label>
</template>
</div>
<LoginView v-if="goStore.context === 'login'"></LoginView>
<GameBoard v-if="goStore.context != 'lobby' && goStore.context != 'login'"></GameBoard>
<LobbyView v-if="goStore.context == 'lobby'"></LobbyView>
<RulesDisplay v-if="goStore.context == 'rules'"></RulesDisplay>
tehe
</div>
</template>
@@ -26,13 +13,4 @@ import goStore, { setReady } from "@/netcode";
.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>
+11 -4
View File
@@ -10,6 +10,14 @@ function killErika() {
playSound('/sounds/splat1.mp3')
erika_alive.value = false;
}
// Autojoin broken
// watch(() => goStore.context, () => {
// console.log(goStore.context)
// if (goStore.context == "pregame") {
// router.push(`/games/${goStore.current_game?.uuid}`)
// }
// })
</script>
@@ -26,7 +34,8 @@ function killErika() {
<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' : ''">
<button v-if="g" @click="router.push({ path: `/games/${g.uuid}` })"
:class="goStore.discordId == g.owner ? 'own_game' : ''">
<template v-if="g.started && g.ended">
GAME OVER
</template>
@@ -57,7 +66,7 @@ function killErika() {
<div class="overlay" v-if="erika_alive">
<div class="erika-zone">
<img @click="router.push({ path: 'rules' })" class="overlay-image"
<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">
@@ -76,8 +85,6 @@ function killErika() {
</div>
</template>
<style scoped>
+3 -2
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import goStore, { netcode_create_game, join_game } from '~/netcode';
import { navigateTo, useRuntimeConfig } from 'nuxt/app';
import goStore, { netcode_create_game, join_game } from '../netcode';
const config = useRuntimeConfig()
@@ -103,4 +104,4 @@ footer {
font-size: 12px;
text-align: center;
}
</style>
</style>import { useRuntimeConfig, navigateTo } from 'nuxt/app';
+317 -1
View File
@@ -1,3 +1,319 @@
<script setup lang="ts">
import type { Card } from '~/netcode/Card';
const router = useRouter();
</script>
<template>
<RulesDisplay></RulesDisplay>
<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 scoped>
.background {
background-color: #f0f0f0;
background-image: url('/images/bg-darkgrass.png');
background-size: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100vw;
;
}
.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>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+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

Binary file not shown.
+3 -2
View File
@@ -8,8 +8,9 @@ enum CardStyles {
export const useSettingsStore = defineStore("settingstore", {
state: () => ({
cardStyle: CardStyles.HERON,
shaderQuality: 0,
shaderFramerate: 10,
shaderQuality: 3,
shaderFramerate: 0,
shaderPixelated: true
}),
actions: {
switchCardStyles() {