Compare commits

...
175 Commits
Author SHA1 Message Date
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 80f6d02fc7 Update .gitea/workflows/build.yml 2024-08-01 10:44:09 +00: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 da54495d1c Ci : add more tags 2024-08-01 08:12:50 +00:00
legonzaur f60ec850f0 Ci : fix repo name in tag 2024-08-01 08:12:33 +00: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 bce8edc150 Ci : run on any tag creation
release-tag / release-image (push) Failing after 10s
2024-08-01 07:51:05 +00:00
legonzaur fadc8cc451 Ci : run on tag creation 2024-08-01 09:48:45 +02:00
legonzaur 8cb8bdfc5e Ci : add tags
release-tag / release-image (release) Failing after 10s
2024-07-31 22:42:28 +02:00
legonzaur 0f1ab017ed Update .gitea/workflows/build.yml
release-tag / release-image (push) Successful in 23s
2024-07-31 20:36:09 +00:00
legonzaur 9384f8f23e Ci : build on tag published
release-tag / release-image (push) Successful in 23s
2024-07-31 20:23:55 +00: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 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 dd09ab3c8c WIP 2024-05-22 09:22:32 +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
legonzaur 32c4c9a4bf Merge branch 'main' into feat/switch-assets 2024-05-21 19:09:26 +00:00
Erzangel 06ac020723 feat: more original cards, fix heron 15, settings img 2024-05-21 21:08:13 +02:00
legonzaur 125bb0e894 Chore: cleanup settings view
release-tag / release-image (push) Successful in 35s
2024-05-19 22:25:17 +02:00
legonzaur 221935faae Feat: fix some shaders issue, add framerate slider
release-tag / release-image (push) Successful in 30s
2024-05-19 22:04:09 +02:00
legonzaur ab975d499e Fix : normalize borders
release-tag / release-image (push) Successful in 35s
2024-05-19 21:49:58 +02:00
legonzaur dcac9ecd56 Fix : slow down shader animations
release-tag / release-image (push) Successful in 39s
2024-05-19 21:47:53 +02:00
legonzaur 86ae4c57f8 Fix : switch to setTimeout for animations 2024-05-19 21:47:34 +02:00
legonzaur b3ca9eafd8 Feat : add border to bottom of market
release-tag / release-image (push) Successful in 51s
2024-05-19 21:46:08 +02:00
legonzaur 2b678a51a7 Fix : add background back
release-tag / release-image (push) Successful in 1m14s
2024-05-19 21:11:28 +02:00
legonzaur e18a8dce49 Feat : Add shader quality slider 2024-05-19 18:55:13 +02:00
legonzaur 2c2342580b Merge remote-tracking branch 'origin/feat/switch-assets' into feat-shader-background 2024-05-19 18:49:34 +02:00
legonzaur cfc04b8451 WIP 2024-05-19 18:49:29 +02:00
legonzaur 02e6548eea Feat: add persistentSettingsStore using LocalStorage plugin 2024-05-19 18:44:57 +02:00
legonzaur 2aac51a018 Feat: add persistentSettingsStore using LocalStorage plugin 2024-05-19 18:44:37 +02:00
legonzaur 658add7888 Feat: add persistentSettingsStore using LocalStorage plugin 2024-05-19 18:44:07 +02:00
Erzangel 5cd57a7266 feat(wip): button to switch assets 2024-05-19 17:49:33 +02:00
legonzaur 58d0e4a416 Add shaders everywhere! 2024-05-19 13:22:05 +02:00
legonzaur 1fc3378083 Merge branch 'main' into feat-shader-background 2024-05-19 10:24:23 +02:00
legonzaur f16dad82ae Feat : darken background 2024-05-19 01:38:08 +02:00
legonzaur 51171fc2e1 Feat : temporarily remove self background 2024-05-19 01:32:59 +02:00
legonzaur d5c65cfb14 Feat : add game board to current_player 2024-05-19 01:23:11 +02:00
legonzaur caa54e6ece WIP canvas 2024-05-19 01:20:10 +02:00
239 changed files with 6198 additions and 3019 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
+23 -19
View File
@@ -1,19 +1,16 @@
name: release-tag
on:
release:
types: [released]
push:
branches:
- main
tags:
- "**"
jobs:
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: legonzaur
DOCKER_LATEST: nightly
REPO_NAME: ${{ gitea.repository }}
ENDPOINT: git.legonzaur.fr
steps:
@@ -23,12 +20,6 @@ jobs:
lfs: false
persist-credentials: false
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: setup private key
shell: bash
run: |
@@ -40,7 +31,7 @@ jobs:
shell: bash
run: |
ssh-keyscan ${{ env.ENDPOINT }} >> ~/.ssh/known_hosts
git remote set-url origin git@${{ env.ENDPOINT }}:${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}.git
git remote set-url origin git@${{ env.ENDPOINT }}:${{ env.REPO_NAME }}.git
git lfs pull
- name: Login to DockerHub
@@ -50,15 +41,28 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ENDPOINT }}/${{ env.REPO_NAME }}
tags: |
type=schedule
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
secrets: |
"NUXT_PUBLIC_WEBSOCKET_ENDPOINT=${{ secrets.NUXT_PUBLIC_WEBSOCKET_ENDPOINT }}"
"NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=${{ secrets.NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT }}"
build-args: |
NUXT_PUBLIC_ENDPOINT=${{ vars.NUXT_PUBLIC_ENDPOINT }}
NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=${{ vars.NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT }}
NUXT_PUBLIC_WELCOME_MESSAGE=${{ vars.NUXT_PUBLIC_WELCOME_MESSAGE }}
push: true
tags: | # replace it with your local IP and tags
${{ env.ENDPOINT }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.ENDPOINT }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+3
View File
@@ -22,3 +22,6 @@ logs
.env
.env.*
!.env.example
assets/images/*
public/img/skins
+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"
}
+17 -9
View File
@@ -1,16 +1,24 @@
FROM node:lts-alpine as build-stage
FROM node:lts-alpine AS install-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN --mount=type=secret,id=NUXT_PUBLIC_WEBSOCKET_ENDPOINT \
--mount=type=secret,id=NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT \
NUXT_PUBLIC_WEBSOCKET_ENDPOINT=$(cat /run/secrets/NUXT_PUBLIC_WEBSOCKET_ENDPOINT ) \
NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT=$(cat /run/secrets/NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT ) \
npx nuxi generate
FROM git.legonzaur.fr/heronfief/cards-gen:main AS svg-stage
RUN python3 -u ./src/generate_cards_mvp.py
FROM install-stage AS build-stage
COPY --from=svg-stage /app/output/heron ./assets/images/cards/heron-svg
COPY --from=svg-stage /app/img/skins ./public/img
ARG NUXT_PUBLIC_ENDPOINT
ARG NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT
ARG NUXT_PUBLIC_WELCOME_MESSAGE
RUN npx nuxi generate
# étape de production
FROM nginx:stable-alpine as production-stage
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/.output/public /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
COPY --from=svg-stage /app/img/skins /usr/share/nginx/html/public/img/skins
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
+45 -86
View File
@@ -1,103 +1,39 @@
<script setup lang="ts">
import { cards, compileCards } from "./services/loadCards";
import { getCurrentInstance } from "vue";
// import goStore, { oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
import goStore, { setReady, oauth2_register_discord, setup_websocket, heron_cookie_login } from "@/netcode";
const nuxtApp = useNuxtApp();
await setup_websocket()
const authStore = useAuthStore();
useHead
({
title
: 'Héron Realms',
})
const heron_session = useCookie('heron_session')
const route = useRoute()
onMounted(async () => {
const code = route.query.code
if (!code) { return }
if (Array.isArray(code)) { return }
await setup_websocket()
oauth2_register_discord(code)
history.pushState(
{},
'',
'/'
)
})
onMounted(async () => {
if (!heron_session.value) { return }
heron_cookie_login(heron_session.value)
})
function keyDownHandler(e: KeyboardEvent) {
if (e.repeat) { return }
console.log(e.code)
if (e.code != "AltLeft") { return }
goStore.zoomKeyPressed = true
e.preventDefault()
return false
console.log("compiling cards...");
const app = getCurrentInstance()?.appContext.app;
if (!app) {
throw new Error("Couldn't bake cards: instance is null");
}
await compileCards(app);
console.log("compiling done");
function keyUpHandler(e: KeyboardEvent) {
if (e.repeat) { return }
if (e.code != "AltLeft") { return }
goStore.zoomKeyPressed = false
e.preventDefault()
return false
}
useHead({
title: "Héron Realms",
});
function contextMenuHandler(e: MouseEvent) {
e.preventDefault()
}
function mouseDownHandler(e: MouseEvent) {
if (e.button !== 2) { return }
goStore.zoomKeyPressed = true
e.preventDefault()
return false
}
function mouseUpHandler(e: MouseEvent) {
if (e.button !== 2) { return }
goStore.zoomKeyPressed = false
e.preventDefault()
return false
}
function follow_mouse(e: MouseEvent) {
goStore.clientMouseX = e.clientX
goStore.clientMouseY = e.clientY
}
onMounted(() => {
window.addEventListener("contextmenu", contextMenuHandler)
window.addEventListener("mousedown", mouseDownHandler)
window.addEventListener("mouseup", mouseUpHandler)
document.addEventListener("mousemove", follow_mouse)
})
onUnmounted(() => {
window.removeEventListener("contextmenu", contextMenuHandler)
window.removeEventListener("mousedown", mouseDownHandler)
window.removeEventListener("mouseup", mouseUpHandler)
document.removeEventListener("mousemove", follow_mouse)
})
onDeactivated
await authStore.whoami();
</script>
<template>
<div class="root_dom_div">
<CardOverlay v-if="goStore.zoomKeyPressed" :card_id="goStore.hoveredCard" />
<NuxtPage />
<NuxtLoadingIndicator> Loading... </NuxtLoadingIndicator>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style>
:root {
--animation-curve: cubic-bezier(0.68, -0.275, 0.32, 1.275);
}
html {
text-align: center;
}
@@ -107,6 +43,29 @@ body {
height: 100vh;
}
button {
background-color: #4caf50;
border: none;
color: white;
text-align: center;
text-decoration: none;
font-family: "Roboto", sans-serif;
font-size: 16px;
width: max-content;
cursor: pointer;
border-radius: 12px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
padding: 10px 24px;
height: 50px;
}
button:disabled {
background-color: gray;
cursor: not-allowed;
}
#__nuxt {
height: 100%;
user-select: none;
+3
View File
@@ -0,0 +1,3 @@
<html>
I LOVE LOADING SCREENS
</html>
+36
View File
@@ -0,0 +1,36 @@
#ifdef GL_ES
precision mediump float;
#endif
#define FBM_OCTAVES 6
#include "lygia/generative/fbm.glsl"
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform vec3 u_color_start;
uniform vec3 u_color_end;
uniform vec2 u_offset;
uniform float u_time;
void main(){
vec2 st=(gl_FragCoord.xy+u_offset)/u_resolution;
vec2 q=vec2(0.);
q.x=fbm(st);
q.y=fbm(st+vec2(1.));
q=q*.5+.5;
vec2 r=vec2(0.);
r.x=fbm(st+1.*q+vec2(1.7,9.2)+.00215*u_time);
r.y=fbm(st+1.*q+vec2(8.3,2.8)+.00526*u_time);
r=r*.5+.5;
float f=fbm(st+r);
f = floor((f+.4)*4.)/4.;
vec3 color=mix(u_color_start,u_color_end,f*.3);
gl_FragColor=vec4(color,1.);
}
+243
View File
@@ -0,0 +1,243 @@
import noise from "~/assets/shaders/noise.frag?raw";
import resolveLygia from "~/assets/workers/resolve-lygia.esm";
let interval: number;
let timeout: number;
const shaderOptions = { quality: 1, framerate: 1000 / 60 };
const screen = { width: 0, height: 0, devicePixelRatio: 0 };
const _colorStart = [0, 0, 0];
const _colorEnd = [0, 0, 0];
let fragmentShader = resolveLygia(noise);
let canvas: OffscreenCanvas;
let render: (time: number) => void;
let must_resize = false;
async function main(canvas: OffscreenCanvas) {
const gl = canvas.getContext("webgl");
if (!gl) {
return;
}
// setup GLSL program
const program = gl.createProgram();
if (!program) {
return;
}
const fragment = gl.createShader(gl.FRAGMENT_SHADER);
if (!fragment) {
return;
}
gl.shaderSource(fragment, fragmentShader);
gl.compileShader(fragment);
if (!gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
const lastError = gl.getShaderInfoLog(fragment);
console.error(lastError);
gl.deleteShader(fragment);
return;
}
const vertex = gl.createShader(gl.VERTEX_SHADER);
if (!vertex) {
return;
}
gl.shaderSource(
vertex,
`attribute vec4 a_position;void main() {gl_Position = a_position;}`
);
gl.compileShader(vertex);
if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS)) {
const lastError = gl.getShaderInfoLog(vertex);
console.error(lastError);
gl.deleteShader(vertex);
return;
}
gl.attachShader(program, fragment);
gl.attachShader(program, vertex);
gl.linkProgram(program);
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
return;
}
// const program = webglUtils.createProgramFromSources(gl, [vs, fragmentShader2]);
// look up where the vertex data needs to go.
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
// look up uniform locations
const offset = gl.getUniformLocation(program, "u_offset");
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const colorStart = gl.getUniformLocation(program, "u_color_start");
const quality = gl.getUniformLocation(program, "u_quality");
const colorEnd = gl.getUniformLocation(program, "u_color_end");
const timeLocation = gl.getUniformLocation(program, "u_time");
// Create a buffer to put three 2d clip space points in
const positionBuffer = gl.createBuffer();
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// fill it with a 2 triangles that cover clipspace
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-1,
-1, // first triangle
1,
-1,
-1,
1,
-1,
1, // second triangle
1,
-1,
1,
1,
]),
gl.STATIC_DRAW
);
const gl2 = gl;
function render(time: number) {
if (must_resize) {
canvas.width = (screen.width * shaderOptions.quality) / 10;
canvas.height = (screen.height * shaderOptions.quality) / 10;
must_resize = false;
}
time *= 0.001; // convert to seconds
// Tell WebGL how to convert from clip space to pixels
gl2.viewport(0, 0, gl2.canvas.width, gl2.canvas.height);
// Tell it to use our program (pair of shaders)
gl2.useProgram(program);
// Turn on the attribute
gl2.enableVertexAttribArray(positionAttributeLocation);
// Bind the position buffer.
gl2.bindBuffer(gl2.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
gl2.vertexAttribPointer(
positionAttributeLocation,
2, // 2 components per iteration
gl2.FLOAT, // the data is 32bit floats
false, // don't normalize the data
0, // 0 = move forward size * sizeof(type) each iteration to get the next position
0 // start at the beginning of the buffer
);
const secondRatio = screen.width / screen.height;
if (secondRatio < 1) {
const half = canvas.height / 2;
gl2.uniform2f(
resolutionLocation,
canvas.width,
canvas.height * secondRatio
);
gl2.uniform2f(offset, 0, -half + half / (1 / secondRatio));
} else {
const half = canvas.width / 2;
gl2.uniform2f(
resolutionLocation,
canvas.width / secondRatio,
canvas.height
);
gl2.uniform2f(offset, -half + half / secondRatio, 0);
}
gl2.uniform1i(quality, shaderOptions.quality);
gl2.uniform3f(colorStart, _colorStart[0], _colorStart[1], _colorStart[2]);
gl2.uniform3f(colorEnd, _colorEnd[0], _colorEnd[1], _colorEnd[2]);
gl2.uniform1f(timeLocation, time);
gl2.drawArrays(
gl2.TRIANGLES,
0, // offset
6 // num vertices to process
);
if (timeout) {
self.clearTimeout(timeout);
timeout = self.setTimeout(() => {
requestAnimationFrame(render);
}, 1000 / shaderOptions.framerate);
}
}
requestAnimationFrame(render);
// self.clearInterval(interval)
// interval = self.setInterval(() => requestAnimationFrame(render), 1000 / shaderOptions.framerate)
self.clearTimeout(timeout);
if (shaderOptions.framerate == 0) {
return render;
}
timeout = self.setTimeout(() => {
requestAnimationFrame(render);
}, 1000 / shaderOptions.framerate);
return render;
}
self.addEventListener("message", (e) => {
const data = e.data;
if (data.command == "initShader") {
canvas = data.canvas;
shaderOptions.quality = data.shaderOptions.quality;
shaderOptions.framerate = data.shaderOptions.framerate;
if (shaderOptions.framerate == 65) {
shaderOptions.framerate == Infinity;
}
screen.devicePixelRatio = data.screen.devicePixelRatio;
screen.width = data.screen.width;
screen.height = data.screen.height;
loadShader();
} else if (data.command == "killShader") {
self.clearInterval(interval);
self.clearTimeout(timeout);
} else if (data.command == "resumeShader") {
self.clearInterval(interval);
self.clearTimeout(timeout);
shaderOptions.framerate = data.shaderOptions.framerate;
if (shaderOptions.framerate == 65) {
shaderOptions.framerate == Infinity;
}
timeout = self.setTimeout(() => {
requestAnimationFrame(render);
}, 1000 / shaderOptions.framerate);
// interval = self.setInterval(() => requestAnimationFrame(render), 1000 / shaderOptions.framerate)
} else if (data.command == "recolorShader") {
_colorStart[0] = data.colorStart[0];
_colorStart[1] = data.colorStart[1];
_colorStart[2] = data.colorStart[2];
_colorEnd[0] = data.colorEnd[0];
_colorEnd[1] = data.colorEnd[1];
_colorEnd[2] = data.colorEnd[2];
} else if (data.command == "resizeShader") {
shaderOptions.quality = data.shaderOptions.quality;
screen.devicePixelRatio = data.screen.devicePixelRatio;
screen.width = data.screen.width;
screen.height = data.screen.height;
must_resize = true;
}
});
async function loadShader() {
if (!canvas) {
return;
}
// let fragmentShader2 = fragmentShader.replace("#define FBM_OCTAVES 6", "#define FBM_OCTAVES " + shaderOptions.quality);
const temp = await main(canvas);
if (temp) {
render = temp;
}
postMessage("ready");
}
+56
View File
@@ -0,0 +1,56 @@
function getFile(url) {
if(url == "https://lygia.xyz/generative/fbm.glsl"){
url = "/fbm.glsl"
}
let httpRequest = new XMLHttpRequest();
httpRequest.open("GET", url, false);
httpRequest.send();
if (httpRequest.status == 200)
return httpRequest.responseText;
else
return "";
}
const lygiaURL = "https://lygia.xyz"
function resolveLygia(lines) {
if ( !Array.isArray(lines) ) {
lines = lines.split(/\r?\n/);
}
let src = "";
lines.forEach( (line, i) => {
const line_trim = line.trim();
if (line_trim.startsWith('#include \"lygia') ) {
let include_url = line_trim.substring(15);
include_url = lygiaURL + include_url.replace(/\"|\;|\s/g,'');
src += getFile(include_url) + '\n';
}
else {
src += line + '\n';
}
});
return src;
}
async function resolveLygiaAsync(lines) {
if (!Array.isArray(lines))
lines = lines.split(/\r?\n/);
let src = "";
const response = await Promise.all(
lines.map(async (line, i) => {
const line_trim = line.trim();
if (line_trim.startsWith('#include "lygia')) {
let include_url = line_trim.substring(15);
include_url = lygiaURL + include_url.replace(/\"|\;|\s/g, "");
return fetch(include_url).then((res) => res.text());
}
else
return line;
})
);
return response.join("\n");
}
export { resolveLygia as default };
+176
View File
@@ -0,0 +1,176 @@
<script setup lang="ts">
const settingsStore = useSettingsStore();
import gsap from "gsap";
import ShaderWorker from "~/assets/workers/ShaderWorker?worker";
const emit = defineEmits<{
ready: [];
}>();
let worker: Worker;
const canvas = ref<HTMLCanvasElement | null>(null);
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null);
export interface Props {
isTurn?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isTurn: () => false,
});
const _colorStart = reactive([0, 0.3, 0]);
const _colorEnd = reactive([0, 1, 0]);
if (props.isTurn) {
Object.assign(_colorStart, [0, 0.3, 0]);
Object.assign(_colorEnd, [0, 1, 0]);
} else {
Object.assign(_colorStart, [0, 0.2, 0.3]);
Object.assign(_colorEnd, [0, 1, 1]);
}
function initShader() {
return new Promise<void>((resolve, reject) => {
if (!canvas.value) {
reject("canvas is null");
return;
}
const offscreen = canvas.value?.transferControlToOffscreen();
worker.postMessage(
{
command: "initShader",
canvas: offscreen,
screen: {
height: window.innerHeight * devicePixelRatio,
width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
},
[offscreen]
);
if (settingsStore.shaderFramerate == 0) {
killShader();
}
resizeShader();
worker.onmessage = (e) => {
if (e.data == "ready") {
resolve();
}
};
});
}
function recolorShader() {
worker.postMessage({
command: "recolorShader",
colorStart: [..._colorStart],
colorEnd: [..._colorEnd],
});
}
function resizeShader() {
worker.postMessage({
command: "resizeShader",
screen: {
height: window.innerHeight * devicePixelRatio,
width: window.innerWidth * devicePixelRatio,
devicePixelRatio,
},
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
}
function killShader() {
worker.postMessage({
command: "killShader",
});
}
function resumeShader() {
worker.postMessage({
command: "resumeShader",
shaderOptions: {
quality: settingsStore.shaderQuality,
framerate: settingsStore.shaderFramerate,
},
});
}
watch(
() => [settingsStore.shaderFramerate],
() => {
if (settingsStore.shaderFramerate == 0) {
killShader();
return;
}
resumeShader();
}
);
watch(
() => [settingsStore.shaderQuality],
() => {
resizeShader();
}
);
watch(
() => [props.isTurn],
() => {
if (props.isTurn) {
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: 0.3, 2: 0 });
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 0 });
} else {
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: 0.2, 2: 0.3 });
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 1 });
}
}
);
watch([_colorEnd, _colorStart], () => {
recolorShader();
});
onMounted(() => {
worker = new ShaderWorker();
recolorShader();
initShader().then(() => {
emit("ready");
});
window.addEventListener("resize", resizeShader, true);
});
onUnmounted(() => {
window.removeEventListener("resize", resizeShader);
killShader();
});
</script>
<template>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
height="270"></canvas>
</template>
<style scoped>
#background {
left: 0;
position: fixed;
height: 100%;
width: 100%;
z-index: -999999;
}
#background.pixelated {
image-rendering: pixelated;
}
</style>
-49
View File
@@ -1,49 +0,0 @@
<script setup lang="ts">
import goStore from '~/netcode';
export interface Props {
card_id: number | undefined
}
const props = defineProps<Props>()
const card: Ref<Element | null> = ref(null)
const x = computed(() => {
const bounds = card.value?.getBoundingClientRect()
let offset = 1
if (!bounds) { return 0 }
if ((bounds.width + goStore.clientMouseX) > (window.visualViewport?.width ?? Infinity)) {
offset = - (bounds.width + 1)
}
return goStore.clientMouseX + offset
})
const y = computed(() => {
const bounds = card.value?.getBoundingClientRect()
let offset = 1
if (!bounds) { return 0 }
if ((bounds.height + goStore.clientMouseY) > (window.visualViewport?.height ?? Infinity)) {
offset = - (bounds.height + 1)
}
return goStore.clientMouseY + offset
})
</script>
<template>
<div :class="`card_overlay`" :style="`top: ${y}px; left: ${x}px`" ref="card">
<img :class="`card_image`" v-if="props.card_id"
:src="'/cards/' + props.card_id.toString().padStart(3, '0') + '.png'" draggable="false">
<!-- <img :class="`card_image`" v-if="!props.card_id" :src="'/cards/background.png'" draggable="false"> -->
</div>
</template>
<style scoped>
.card_overlay {
/* display: none; */
position: fixed;
z-index: 999;
display: flex;
pointer-events: none;
}
</style>
-128
View File
@@ -1,128 +0,0 @@
<script setup lang="ts">
import goStore from '~/netcode';
export interface Props {
card_id: number | undefined
locked?: boolean
wrapped?: boolean
tilted?: boolean
}
const props = withDefaults(defineProps<Props>(), {
wrapped: () => false,
tilted: () => true
})
const emit = defineEmits(["click"])
const childCount = ref<number>(0)
function cardClick() {
if (!props.card_id) return
emit("click")
}
const rotation = ref(0)
function mouseenter(e: Event) {
goStore.hoveredCard = props.card_id
}
function mouseleave(e: Event) {
goStore.hoveredCard = undefined
}
watch(
() => props.card_id,
() => {
props.wrapped ? rotation.value = (Math.random() * 4) - 2 : rotation.value = Math.random() > .5 ? 2 : -2
},
{ immediate: true })
</script>
<template>
<div :class="`card ${childCount > 0 ? 'active' : ''} ${wrapped ? 'wrapped' : ''} ${props.locked ? 'locked' : ''}`"
@contextmenu.prevent="false" @click.left="cardClick" :style="tilted ? `rotate:${rotation}deg;` : ''"
@mouseenter="mouseenter" @mouseleave="mouseleave">
<div id="contextMenuButtons" :ref="(el) => { childCount = ((el as Element)?.childElementCount ?? 0) }">
<slot></slot>
</div>
<img :class="`card_image`" v-if="props.card_id"
:src="'/cards/' + props.card_id.toString().padStart(3, '0') + '.png'" draggable="false">
<img :class="`card_image`" v-if="!props.card_id" :src="'/cards/background.png'" draggable="false">
</div>
</template>
<style scoped>
.card {
transition: 0.025s rotate linear;
max-width: 256px;
position: relative;
display: flex;
margin-bottom: 0px;
margin-top: 15px;
transition: all .125s cubic-bezier(0.075, 0.82, 0.165, 1);
isolation: isolate;
pointer-events: all;
}
.card.active {
cursor: pointer
}
.card_image {
max-height: 250px;
transition: all .125s cubic-bezier(0.075, 0.82, 0.165, 1);
z-index: 1;
}
.card_image.outlined {
outline: 2px solid red;
outline-offset: -2px;
}
.card.locked {
margin-top: 0px;
margin-bottom: 15px;
filter: drop-shadow(5px 15px 2px rgba(0, 0, 0, .60));
}
.card.wrapped {
max-width: 10px;
}
.card_image {
max-height: 250px;
}
#contextMenuButtons {
position: absolute;
display: flex;
flex-direction: row;
flex-wrap: wrap;
word-break: break-word;
bottom: 0;
width: 99%;
transform: translateY(-0px);
transition: 0.1s transform cubic-bezier(0.075, 0.82, 0.165, 1);
}
.card.active:hover #contextMenuButtons {
transform: translateY(100%);
}
</style>
<style>
#contextMenuButtons>* {
flex: 1;
border-radius: 12px;
min-height: 50px;
}
</style>
-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>
-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 isWinner = ref(false)
function showLoserScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true
isWinner.value = false
}
function showWinnerScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true
isWinner.value = true
}
</script>
<template>
<div @contextmenu.prevent="false" id="game_board">
<div id="market">
<MarketCards></MarketCards>
</div>
<div id="player_list">
<PlayerListElement v-for="p in goStore.current_game?.players" :player="p">
</PlayerListElement>
</div>
<div id="current_player"
:class="(goStore.current_game?.current_turn && (goStore.current_game?.current_turn === goStore.self?.team)) ? 'self' : ''">
<PlayerSlot v-for="p in current_players" :player="p"></PlayerSlot>
</div>
</div>
<ResultDialog
:isVisible="showResults"
:won="isWinner"
:players="goStore.current_game?.players"
@close="showResults = false">
</ResultDialog>
<SelfView></SelfView>
</template>
<style scoped>
#game_board {
display: grid;
grid-template-columns: 1fr 400px;
grid-template-rows: min-content 1fr;
grid-template-areas:
"market player_list"
"current_player player_list";
height: 100%;
}
#player_list,
#current_player {
overflow: auto;
position: relative;
isolation: isolate;
}
#current_player.self {
background-image: url('../images/bg-grass.png');
}
#market {
grid-area: market;
background-image: url("/images/bg-sand.png");
padding: 10px;
}
#current_player {
grid-area: current_player;
background-image: url("/images/bg-darkgrass.png");
}
#player_list {
grid-area: player_list;
border-left: 5px solid black;
height: 100%;
background-image: url("/images/bg-darkgrass.png");
}
</style>
<style>
#player_list .card_image {
max-width: 64px;
}
</style>
-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>
-68
View File
@@ -1,68 +0,0 @@
<script setup lang="ts">
import goStore, { buy, useEffect } from '@/netcode';
import { CardEffects, type Effect } from '@/netcode/Effect'
import { CardType } from '@/netcode/Card'
const isTurn = computed(() => goStore.self && goStore.current_game?.started && (goStore.self.team == goStore.current_game.current_turn))
const clientStore = useClientSideStore()
const buyableFiregem = computed(() => goStore?.current_game?.gem_stack.at(-1))
</script>
<template>
<div id="market">
<CardPreview :tilted="false" :card_id="undefined"></CardPreview>
<div class="separator"></div>
<CardPreview :tilted="false" :card_id="c?.card_id" v-for="c in goStore.current_game?.market"
@click="c && buy(c)">
<template
v-if="isTurn && goStore.self?.turn && c && c.cost && (goStore.self?.turn.gold_reserve ?? 0) >= c.cost">
<button @click.stop="buy(c)">BUY</button>
<button @click.stop="clientStore.findUseEffect(CardEffects.PLAY_NEXT_CARD_BOUGHT, c);"
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PLAY_NEXT_CARD_BOUGHT) && e.usable)">
BUY IN HAND</button>
<button @click.stop="clientStore.findUseEffect(CardEffects.STACK_NEXT_CARD_BOUGHT, c)"
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STACK_NEXT_CARD_BOUGHT) && e.usable)">
BUY IN STACK (generic)</button>
<button @click.stop="clientStore.findUseEffect(CardEffects.STACK_NEXT_ACTION_BOUGHT, c)"
v-if="c.card_type == CardType.ACTION && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STACK_NEXT_ACTION_BOUGHT) && e.usable)">
BUY IN STACK (actions)</button>
</template>
</CardPreview>
<div class="separator"></div>
<CardPreview :tilted="false" :card_id="goStore.current_game?.gem_stack.at(-1)?.card_id"
@click="buy(goStore.current_game?.gem_stack.at(-1)!)">
<template
v-if="isTurn && goStore.self?.turn && goStore.current_game?.gem_stack.length && goStore.current_game?.gem_stack.length > 0 && (goStore.self?.turn.gold_reserve ?? 0) >= (goStore.current_game?.gem_stack.at(-1)?.cost ?? Infinity)">
<button @click.stop="buy(goStore.current_game.gem_stack.at(-1)!)">
BUY
</button>
<button @click.stop="clientStore.findUseEffect(CardEffects.PLAY_NEXT_CARD_BOUGHT, buyableFiregem)"
v-if="buyableFiregem && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PLAY_NEXT_CARD_BOUGHT) && e.usable)">
BUY IN HAND</button>
<button @click.stop="clientStore.findUseEffect(CardEffects.STACK_NEXT_CARD_BOUGHT, buyableFiregem)"
v-if="buyableFiregem && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STACK_NEXT_CARD_BOUGHT) && e.usable)">
BUY IN STACK(generic)
</button>
</template>
</CardPreview>
</div>
</template>
<style scoped>
#market {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 2;
position: relative;
gap: 2px;
}
.separator {
width: 50px;
}
</style>
-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>
-151
View File
@@ -1,151 +0,0 @@
<script setup lang="ts">
import goStore from '@/netcode';
import type { Player } from "~/netcode/Player";
const view = ref<"main" | "discard">("main")
function show(data: "main" | "discard") {
view.value = data
}
export interface Props {
player: Player;
}
const props = defineProps<Props>()
</script>
<template>
<div :class="`player_list_element ${goStore.self === props.player ? 'self' : ''}`">
<div class="button" id="player_list_buttons">
<div class="main_button" @click="show('main')">
<div class="tooltip">Board</div>
<img :src="`/cards/${player.board.at(-1)?.card_id.toString().padStart(3, '0') ?? 'background'}.png`"
draggable="false" :class="view === 'main' && 'selected'">
<span class="button_card_number">{{ player.board.length }}</span>
</div>
<div class="stack_button">
<div class="tooltip">Stack</div>
<img :src="'/cards/background.png'" draggable="false">
<span class="button_card_number">{{ player.stack_pile.length }}</span>
</div>
<div class="discard_button" @click="show('discard')">
<div class="tooltip">Discard</div>
<img :src="`/cards/${player.discard_pile.at(-1)?.card_id.toString().padStart(3, '0') ?? 'background'}.png`"
draggable="false" :class="view === 'discard' && 'selected'">
<span class="button_card_number">{{ player.discard_pile.length }}</span>
</div>
</div>
<div class="view">
<PlayerSlot :player="player" :hide_stack_and_discard="true" v-if="view == 'main'" />
<template v-if="view == 'discard'">
<CardPreview :tilted="false" v-for=" card in player.discard_pile " :card_id="card.card_id">
</CardPreview>
</template>
</div>
</div>
</template>
<style scoped>
.button>div>img:not(.selected) {
filter: brightness(.5)
}
.tooltip {
visibility: visible;
position: absolute;
transition: .1s transform;
background: white;
right: 0;
border-top-right-radius: 1em;
border-bottom-right-radius: 1em;
padding-right: 1em;
}
.button {
display: flex;
flex-direction: column;
align-items: stretch;
z-index: 1;
}
.button>div {
flex: 1 1 0;
border-right: 5px solid black;
width: 45px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.button>div:not(.stack_button) {
cursor: pointer;
}
.button div:hover .tooltip {
visibility: visible;
transform: translateX(100%);
}
.button_card_number {
font-size: 2em;
position: absolute;
text-shadow: white 0 0 1px;
}
.button>div:not(:last-child) {
border-bottom: 5px solid black;
}
.player_list_element {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-areas:
"button player";
border-bottom: 5px solid black;
min-height: 200px;
}
.player_list_element.self {
background-image: url('../images/bg-grass.png');
}
.view {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
justify-content: center;
}
</style>
<style>
#player_list_buttons img {
max-width: 100%;
object-fit: contain;
position: absolute;
display: block;
top: 0;
bottom: 0;
margin: auto 0;
}
.view #contextMenuButtons button:not(.enemy_action) {
display: none
}
</style>
-557
View File
@@ -1,557 +0,0 @@
<script setup lang="ts">
import goStore, {
useEffect,
playCard,
playAllCards,
discard,
lockCard,
heal,
damage_champion,
damage_team,
endTurn
} from "~/netcode";
import { CardType, type Card } from "~/netcode/Card";
import type { Player } from "~/netcode/Player";
import { CardEffects, type Effect, EffectType } from "@/netcode/Effect";
import type { Team } from "~/netcode/Team";
import { useClientSideStore } from "~/stores/turn";
export interface Props {
player: Player;
hide_stack_and_discard?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
hide_stack_and_discard: () => false,
})
const discard_unwrapped = ref(false)
const isPlayerTurn = computed(() => props.player.team === goStore.current_game?.current_turn);
const isSelf = computed(() => props.player.uuid === goStore.self?.uuid);
const isSelfTurn = computed(() => goStore.self?.team === goStore.current_game?.current_turn)
const isPlayerEnemy = computed(() => goStore.self?.team !== props.player.team)
const warningText = ref("Effets restants!")
const clientStore = useClientSideStore()
const make_discard = (target: Player) =>
clientStore.findUseEffect(CardEffects.MAKE_DISCARD, target);
const sacrifice = (target: Card) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
const stun = (target: Card) => clientStore.findUseEffect(CardEffects.STUN, target)
const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
const prepare_champion = (target: Card) => clientStore.findUseEffect(CardEffects.PREPARE, target)
let warning = ref(false)
function has_actions_remaining(): boolean {
return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false)
|| (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= (goStore.current_game?.market.reduce((min, card) => Math.min(min, card?.cost ?? Infinity), Infinity) ?? Infinity) : false)
|| (goStore.self?.turn?.fuck_u_markers ? goStore.self?.turn?.fuck_u_markers > 0 : false)
|| !goStore.self?.turn?.effects_availables.every(e => !e.usable || e.effect_type == EffectType.SUICIDE)
|| !goStore.self?.board.every(c => (c.uuid in (goStore.self?.turn?.cards_locked ?? {})))
|| false
}
function can_buy_fire_gem(): boolean {
return (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= 2 : false)
}
function checkEndTurn() {
// TODO: Add a confirmation dialog if resources are left
console.log("Checking end turn")
if (
(!warning.value)
&& has_actions_remaining()) {
// Set a warning flag to true
// Display an alert somehow using this warning flag
warning.value = true
}
else if (!warning.value && can_buy_fire_gem()) {
warningText.value = "P'tite Fire gem?"
warning.value = true
}
else {
warningText.value = "Effets restants!"
warning.value = false
endTurn()
}
}
function damage_team_all(team: Team) {
for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) {
damage_team(team)
}
}
function check_for_guard(player: Player) {
return player.board.some(c => c.guard === true)
}
</script>
<template>
<div :class="`player ${isPlayerTurn ? 'turn' : ''} ${discard_unwrapped ? 'discard_unwrapped' : ''}`">
<template v-if="!hide_stack_and_discard">
<div class="stack">
<CardsGrouped :stack="props.player.stack_pile">
<!-- <button v-if="isSelf && isTurn" @click="draw">DRAW</button> -->
</CardsGrouped>
</div>
<div class="discard" :class="{ invisible: discard_unwrapped }" @click="() => discard_unwrapped = true">
<CardsGrouped :stack="props.player.discard_pile" :visible="!discard_unwrapped"> </CardsGrouped>
</div>
<div class="discard_overlay" v-if="discard_unwrapped">
<CardPreview :tilted="false" v-if="discard_unwrapped" :card_id="c.card_id"
v-for="c in props.player.discard_pile">
<template v-if="goStore.self?.turn && c && isSelf && isPlayerTurn">
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
@click.stop="sacrifice(c)">
SACRIFICE
</button>
<button
v-if="c.card_type == CardType.CHAMPION && clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.RESTACK_DISCARDED_CHAMPION) && e.usable)"
@click.stop="restack_discarded_champion(c)">
RESTACK_DISCARDED_CHAMPION
</button>
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.RESTACK_DISCARDED_CARD) && e.usable)"
@click.stop="restack_discarded_card(c)">
RESTACK_DISCARDED_CARD
</button>
</template>
</CardPreview>
</div>
<div :id="`discard_unwrapped_bg`" :class="`${discard_unwrapped ? 'unwrapped' : ''}`"
@click="() => discard_unwrapped = false"></div>
</template>
<div id="player_banner" :class="hide_stack_and_discard ? 'small' : 'big'">
<div class="ready-bubble not-ready" v-if="!props.player.ready && !goStore.current_game?.started">Pas encore
prêt...
</div>
<div class="ready-bubble ready" v-if="props.player.ready && !goStore.current_game?.started">Prêt!</div>
<div id="player_info">
<PlayerIcon :player="props.player"></PlayerIcon>
</div>
<div id="player_stats">
<template v-if="goStore.current_game?.started">
<PlayerStats :player="props.player">
</PlayerStats>
<template v-if="!hide_stack_and_discard">
<button id="end_turn" v-if="isSelfTurn" class="end_turn_button turn_icon"
@click="checkEndTurn">END
TURN</button>
<div class="warning"
:class="{ 'warning-fire-gem': warningText == 'P\'tite Fire gem?', 'visible': warning }">
/!\<br>{{ warningText }}
</div>
</template>
</template>
</div>
</div>
<div id="current_player_board">
<div class="other_player_buttons_container">
<button @click.stop="damage_team(props.player.team)"
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team" class="damage_button"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
Damage
</button>
<button @click.stop="damage_team_all(props.player.team)"
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team"
class="damage_button all_damage_button"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
(All)
</button>
<button v-if="goStore.self && isSelfTurn && isPlayerEnemy && goStore.self?.turn"
@click.stop="make_discard(props.player)" class="make_discard_button"
:disabled="!clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.MAKE_DISCARD) && e.usable)">
make Discard
</button>
</div>
<br />
<div class="cards_container">
<!-- HAND -->
<CardPreview :card_id="c?.card_id" v-for="c in props.player.hand" @click="c && playCard(c)">
<button
v-if="props.player.turn && c && isSelf && (props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
@click.stop="discard(c)">
DISCARD
</button>
</CardPreview>
<!-- BOARD -->
<CardPreview :card_id="c.card_id" v-for="c in props.player.board" @click="lockCard(c)"
:locked="props.player.turn && c.uuid in props.player.turn.cards_locked">
<template v-if="c && isSelf && isPlayerTurn && props.player.turn">
<!-- Card Locked -->
<template v-if="c.uuid in props.player.turn.cards_locked">
<template v-for="e in clientStore.maskedServerEffects">
<template v-if="e.card == c">
<button v-if="!clientStore.effectsHandled.has(e.effect)" @click.stop="useEffect(e)"
:disabled="!e.usable"
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
EFFECT<br>
{{ e.effect + " " + (e.effect_type ?? '') }}
</button>
<button v-if="clientStore.effectsHandled.has(e.effect)"
@click.stop="clientStore.activateEffect(e)" :disabled="!e.usable"
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
EFFECT<br>
{{ e.effect + " " + (e.effect_type ?? '') }}
</button>
</template>
</template>
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PREPARE) && e.usable) && c.card_type == CardType.CHAMPION"
@click.stop="prepare_champion(c)">PREPARE</button>
</template>
<!-- Card Unlocked -->
<template v-if="!(c.uuid in props.player.turn.cards_locked)">
<button v-if="(props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
@click.stop="discard(c)">
DISCARD
</button>
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
@click.stop="sacrifice(c)">
SACRIFICE
</button>
<button @click.stop="lockCard(c)"
:disabled="((props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0 && c !== props.player.turn.discard_marker_card) || props.player.turn.fuck_u_markers > 0">
LOCK
</button>
</template>
</template>
<template
v-if="c && goStore.self?.turn && isPlayerEnemy && isSelfTurn && (c.health_as_champion ?? 0) > 0">
<button class="enemy_action"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) < (c.health_as_champion ?? Infinity) || (check_for_guard(props.player) && !c.guard)"
@click.stop="damage_champion(c)">
DAMAGE ({{ c.health_as_champion }})
</button>
<button class="enemy_action"
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STUN) && e.usable)"
@click.stop="stun(c)" :disabled="(check_for_guard(props.player) && !c.guard)">
STUN
</button>
</template>
</CardPreview>
</div>
</div>
</div>
</template>
<style scoped>
.warning {
background-color: rgba(255, 0, 0, 0.7);
color: white;
padding: 5px;
border-radius: 5px;
width: 100px;
font-size: 0.85em;
margin-left: 5px;
visibility: hidden
}
.warning.visible {
visibility: visible
}
.warning-fire-gem {
background-color: rgba(255, 165, 0, 0.7);
}
#end_turn {
z-index: 10;
height: 100%;
}
.stack,
.discard,
.cards_container {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap-reverse;
}
.stack {
grid-area: stack;
margin-left: 75px;
}
.discard {
margin-top: 40px;
grid-area: discard;
justify-content: start;
cursor: pointer;
pointer-events: none;
margin-left: 75px;
}
.discard.invisible {
visibility: hidden;
}
.discard_overlay {
z-index: 9;
display: flex;
/* background: blue; */
pointer-events: none;
gap: 2px;
flex-wrap: wrap-reverse;
align-content: flex-end;
cursor: default;
position: absolute;
padding-bottom: inherit;
padding-left: 40px;
height: calc(100% - 64px);
padding-top: 64px;
width: calc(100% -40px);
margin-left: 0;
}
#discard_unwrapped_bg {
display: none;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 8;
background: rgba(0, 0, 0, 0.5);
pointer-events: all;
padding: inherit;
}
#discard_unwrapped_bg.unwrapped {
display: block;
}
.player {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-rows: min-content min-content 1fr;
/* border-top: 5px solid black; */
grid-template-areas:
"player_stats player_stats"
"stack player"
"discard player";
transition: all 1s;
margin-bottom: -75px;
padding-bottom: 75px;
}
.discard_overlay {
grid-template-areas:
"player_stats player_stats"
"stack player";
}
#current_player_board {
grid-area: player;
}
.other_player_buttons_container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 0 5px;
}
.damage_button {
background-color: #ff4d4d;
color: white;
border: none;
border-radius: 8px;
padding: 5px 25px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.damage_button:disabled {
background-color: rgba(255, 77, 77, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
}
.damage_button:hover {
background-color: #e60000;
}
.damage_button:disabled:hover {
background-color: rgba(230, 0, 0, 0.5);
}
.damage_button:active {
transform: translateY(2px);
box-shadow: none;
}
.all_damage_button {
padding: 5px 5px;
font-size: 14px;
}
.make_discard_button {
background-color: #55b42f;
color: white;
border: none;
border-radius: 8px;
padding: 5px 15px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.make_discard_button:disabled {
background-color: rgba(85, 180, 47, 0.5);
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
cursor: not-allowed;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.make_discard_button:hover {
background-color: #03883a;
}
.make_discard_button:disabled:hover {
background-color: rgba(3, 136, 58, 0.5);
}
.make_discard_button:active {
transform: translateY(2px);
box-shadow: none;
}
.turn_icon {
position: relative;
}
#player_banner {
display: flex;
grid-area: player_stats;
flex-direction: row;
align-items: center;
justify-content: center;
position: sticky;
top: 0;
padding-top: 1em;
z-index: 1;
pointer-events: none;
}
#player_banner.small {
flex-direction: column
}
#player_banner>* {
pointer-events: all;
}
#player_info {
display: flex;
align-items: center;
}
#player_banner.big>#player_info {
margin-left: 500px;
}
#player_stats {
display: flex;
grid-area: player_stats;
flex-direction: row;
align-items: center;
justify-content: center;
}
#player_banner img {
height: 50px;
border-radius: 25px;
}
.turn_icon img {
height: 50px;
}
.turn_icon_number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.end_turn_button {
background-color: #4CAF50;
/* Green background color */
color: white;
border: none;
border-radius: 8px;
padding: 13px 10px;
margin: 0 0 0 5px;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.end_turn_button:hover {
background-color: #45a049;
/* Darker green on hover */
}
.end_turn_button:active {
transform: translateY(2px);
/* Add a slight press effect on click */
box-shadow: none;
/* Remove shadow on click */
}
.ready-bubble {
color: white;
padding: 0.5em;
border-radius: 1em;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
margin-bottom: 3%;
}
.ready {
background-color: green;
}
.not-ready {
background-color: maroon;
}
.suicide_effect {
background: linear-gradient(to bottom right, #666666, #ccc);
color: red;
}
</style>
-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>
-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>
-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>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { Effect } from "~/netcode/interfaces";
type Props = {
token: Effect;
};
const props = defineProps<Props>();
const emit = defineEmits(["useToken"]);
function onClick(e: MouseEvent) {
emit("useToken", props.token);
}
</script>
<template>
<div @click.stop="onClick" @mousedown.left.stop="" id="token">
{{ props.token.effect }}
</div>
</template>
<style scoped>
#token {
width: 50px;
height: 50px;
background: white;
border: 1px solid white;
cursor: pointer;
box-sizing: border-box;
transition: 0.2s all;
border-radius: 15px;
}
#token:hover {
border: 1px solid red;
}
</style>
+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>
+279
View File
@@ -0,0 +1,279 @@
<script setup lang="ts">
import { delay } from "~/netcode/events";
import {
CardEffects,
CardType,
type Card,
type Container,
type Effect,
type PlayingTurn,
} from "~/netcode/interfaces";
export interface Props {
container: Container;
playingTurn?: PlayingTurn;
wrapped?: boolean;
hidden?: boolean;
noEffects?: boolean;
tokens?: Effect[];
championsKillable?: boolean;
discardable?: boolean;
locked?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
wrapped: () => false,
hidden: () => false,
noEffects: () => false,
tokens: () => [],
championsKillable: () => false,
discardable: () => false,
});
const emit = defineEmits([
"cardClick",
"effectClick",
"tokenClick",
"damageChampionClick",
"discardCardClick",
]);
const settingsStore = useSettingsStore();
const cards = computed(() => {
if (props.hidden) {
return props.container.cards.map((_v, i) => ({
cardId: undefined,
})) as unknown as Card[];
}
return props.container.cards;
});
const animationSpeed = computed(() => settingsStore.animationSpeed + "ms");
const shuffleAnimationSpeed = computed(
() => settingsStore.animationSpeed * 2 + "ms"
);
function populateContainer() {}
// const rotate = ref(false);
// async function shuffleContainer() {
// rotate.value = true;
// await delay(settingsStore.animationSpeed * 2);
// rotate.value = false;
// await delay(settingsStore.animationSpeed * 2);
// }
function getCardsPosition(cards: string) {}
const animationCallbacks = ref<Array<() => void>>([]);
function startAnimation() {
animationCallbacks.value.forEach((p) => p());
animationCallbacks.value = [];
}
function endAnimation() {
animationCallbacks.value.forEach((p) => p());
}
// watch(
// props.container,
// (newValue, oldValue) => {
// if (oldValue) {
// containerEvents.delete(oldValue._id);
// }
// if (newValue) {
// containerEvents.set(newValue._id, {
// populateContainer,
// shuffleContainer,
// getCardsPosition,
// startAnimation,
// endAnimation,
// });
// }
// },
// { immediate: true }
// );
// async function onEnter(_el: any, done: () => void) {
// animationCallbacks.value.push(done);
// }
// async function onLeave(_el: any, done: () => void) {
// animationCallbacks.value.push(done);
// }
onUnmounted(() => {
// containerEvents.delete(props.container._id);
});
const interval = ref<ReturnType<typeof setInterval>>();
// function startMove() {
// interval.value = setInterval(() => {
// props.container.cards.sort((a, b) => Math.random() - 0.5);
// }, 1000);
// }
// function stopMove() {
// clearInterval(interval.value);
// }
function tokenClick(token: Effect, card: Card) {
emit("tokenClick", { token, card });
}
function damageChampionClick(card: Card) {
emit("damageChampionClick", card);
}
function discardCardClick(card: Card) {
emit("discardCardClick", card);
}
</script>
<template>
<div
class="cards_container"
:class="{ wrapped, rotate: container.shuffling }"
>
<TransitionGroup name="list">
<GameCard
@click="$emit('cardClick', c._id)"
@effectClick="$emit('effectClick', c._id, $event)"
v-for="(c, i) in cards"
:cardId="c?.cardId"
:uuid="c._id"
:wrapped="wrapped"
:key="c?._id || i"
:locked="
locked ||
!playingTurn ||
playingTurn?.cardsLocked.includes(c._id.toString())
"
:used_effects_indexes="c?.effects?.reduce( (prev,e, i)=>{
if(playingTurn?.lockedEffects.includes(e._id)){
prev.push(i)
}
return prev
},[] as number[])"
:noEffects="noEffects"
:discardable="discardable"
>
<template v-for="t in tokens">
<GameToken
:token="t"
@useToken="tokenClick(t, c)"
v-if="
(t.effect == CardEffects.SACRIFICE &&
playingTurn &&
!playingTurn?.cardsLocked.includes(c._id.toString())) ||
(t.effect == CardEffects.PREPARE &&
c.cardType == CardType.CHAMPION) ||
(t.effect == CardEffects.STACK_NEXT_ACTION_BOUGHT &&
c.cardType == CardType.ACTION) ||
(t.effect == CardEffects.STUN &&
c.cardType == CardType.CHAMPION &&
championsKillable) ||
t.effect == CardEffects.PLAY_NEXT_CARD_BOUGHT ||
t.effect == CardEffects.STACK_NEXT_CARD_BOUGHT ||
t.effect == CardEffects.RESTACK_DISCARDED_CARD ||
(t.effect == CardEffects.RESTACK_DISCARDED_ACTION &&
c.cardType == CardType.ACTION) ||
(t.effect == CardEffects.RESTACK_DISCARDED_CHAMPION &&
c.cardType == CardType.CHAMPION)
"
></GameToken>
</template>
<button
v-if="championsKillable && c.cardType == CardType.CHAMPION"
@click.stop="damageChampionClick(c)"
@mousedown.left.stop=""
>
Damage
</button>
<button
v-if="
discardable && !playingTurn?.cardsLocked.includes(c._id.toString())
"
@mousedown.left.stop=""
@click.stop="discardCardClick(c)"
>
Discard
</button>
</GameCard>
</TransitionGroup>
<!-- <img :class="`card_image`" :src="'/cards/background.png'" draggable="false"> -->
</div>
</template>
<style scoped>
.list-move,
.list-enter-active,
.list-leave-active {
transition: all v-bind("animationSpeed") ease;
}
/* .list-leave-to, */
.list-enter-from {
opacity: 0;
/* transform: translateX(30px); */
}
.list-leave-active {
position: absolute;
height: unset;
}
.rotate {
rotate: 360deg;
}
.cards_container {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 30px;
min-width: 200px;
flex-wrap: wrap;
transition: rotate v-bind("shuffleAnimationSpeed") ease;
}
.cards_container.wrapped {
flex-direction: column-reverse;
justify-content: center;
gap: 0;
}
.cards_container.wrapped:before {
height: calc(250px - 10px);
display: block;
content: "";
width: 0;
}
.empty {
max-height: 10px;
}
.empty > .card_image {
filter: brightness(0.5);
max-height: 250px;
}
</style>
<style>
use * use[data-effect-id="0"] {
opacity: 0;
}
.cards_container .card #background rect:first-child {
stroke-width: 0;
}
.cards_container .card.wrapped #background rect:first-child {
stroke-width: 1px;
}
.cards_container .card.card.wrapped:nth-child(5n) #background rect:first-child {
stroke-width: 2px;
}
</style>
+160
View File
@@ -0,0 +1,160 @@
<script setup lang="ts">
import { initCustomFormatter } from "vue";
import { buyCard, buyGem, useToken } from "~/netcode";
import {
CardEffects,
type Card,
type Container,
type Effect,
type Game,
type Player,
} from "~/netcode/interfaces";
export interface Props {
game: Game;
player: Player;
}
const settingsStore = useSettingsStore();
const props = defineProps<Props>();
const isVisible = ref(false);
const discardTokens = computed(() =>
props.game.currentTurn.tokens.filter((t) =>
[
CardEffects.RESTACK_DISCARDED_ACTION,
CardEffects.RESTACK_DISCARDED_CARD,
CardEffects.RESTACK_DISCARDED_CHAMPION,
CardEffects.SACRIFICE,
].includes(t.effect)
)
);
function tokenClick({ card, token }: { card: Card; token: Effect }) {
useToken(props.game._id, token._id, card._id);
}
function openDiscard() {
isVisible.value = true;
if (!settingsStore.saveDiscardSize) {
settingsStore.discardFullscreen = false;
}
}
</script>
<template>
<div id="discard">
<GameCardGrouped
@cardClick="openDiscard"
:container="player.discard"
:wrapped="true"
:no-effects="true"
>
</GameCardGrouped>
</div>
<div
class="discard-animations discard-background"
:class="{ visible: isVisible, fullscreen: settingsStore.discardFullscreen }"
@click.stop="isVisible = false"
></div>
<div
class="discard-animations discard-menu"
:class="{ visible: isVisible, fullscreen: settingsStore.discardFullscreen }"
@click.stop="isVisible = false"
>
<span>Discard of {{ player.user.username }}</span>
<button
class="discard-fullscreen-btn"
@click.stop="
settingsStore.discardFullscreen = !settingsStore.discardFullscreen
"
>
TOGGLE FULLSCREEN
</button>
<GameCardGrouped
:container="player.discard"
:wrapped="false"
:hidden="false"
:tokens="discardTokens"
:noEffects="true"
:playingTurn="game.currentTurn"
:locked="true"
@tokenClick="tokenClick"
></GameCardGrouped>
</div>
</template>
<style scoped>
span {
background: white;
}
.discard-fullscreen-btn {
pointer-events: all;
}
.discard-animations {
transition: 0.3s all var(--animation-curve);
}
.discard-background {
transition: 0.3s all linear;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
pointer-events: none;
z-index: 200;
}
.discard-background.visible {
background: rgba(0, 0, 0, 0.6);
pointer-events: all;
backdrop-filter: blur(2px);
}
.discard-menu {
left: -75%;
top: 0;
width: 25%;
height: 100%;
padding-left: 50%;
background: rgb(100, 53, 44);
position: fixed;
z-index: 200;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.discard-menu.visible {
left: -50%;
}
.discard-menu.fullscreen {
left: -100%;
width: 100%;
padding: 0;
background: rgba(0, 0, 0, 0);
pointer-events: none;
}
.discard-menu.visible.fullscreen {
left: 0%;
}
.discard-menu > .cards_container {
pointer-events: all;
overflow: auto;
padding: 30px;
max-width: 100%;
transition: 0.3s all var(--animation-curve);
justify-content: center;
}
.discard-menu.fullscreen > .cards_container {
max-width: 50%;
}
</style>
+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>
@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { Player } from '~/netcode/Player';
import type { Player } from "~/netcode/interfaces";
export interface Props {
isVisible: Boolean;
@@ -10,15 +9,14 @@ export interface Props {
const props = withDefaults(defineProps<Props>(), {
won: () => false,
players: () => []
})
players: () => [],
});
const emit = defineEmits(['close'])
const emit = defineEmits(["close"]);
const close = () => {
reloadNuxtApp()
}
reloadNuxtApp();
};
</script>
<template>
@@ -29,22 +27,24 @@ const close = () => {
<h3 v-else>Défaite!</h3>
</div>
<div class="dialog-body">
<PlayerIcon :player="players[0]"></PlayerIcon>
<p v-if="won">
Vous avez gagné!
</p>
<GamePlayerIcon :player="players[0]"></GamePlayerIcon>
<p v-if="won">Vous avez gagné!</p>
<p v-else>
Vous avez perdu face à ce magnifique individu qui a très clairement beaucoup plus de bitches que vous! Cela
dit, je pense qu'il n'y a pas de bonnes
ou de mauvaises situations, juste des situations différentes. Vous avez perdu, mais vous avez gagné en
expérience! C'est ça le plus important
dans la vie, n'est-ce pas? Apprendre de ses erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez
heureux d'avoir eu l'opportunité
d'apprendre quelque chose aujourd'hui! Et puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend
plus fort"! Alors, allez-y, soyez fort!
Et n'oubliez pas, la prochaine fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un champion!
Vous êtes le meilleur! Vous êtes
le roi du monde!
Vous avez perdu face à ce magnifique individu qui a très clairement
beaucoup plus de bitches que vous! Cela dit, je pense qu'il n'y a pas
de bonnes ou de mauvaises situations, juste des situations
différentes. Vous avez perdu, mais vous avez gagné en expérience!
C'est ça le plus important dans la vie, n'est-ce pas? Apprendre de ses
erreurs et devenir meilleur! Alors, ne soyez pas triste, soyez heureux
d'avoir eu l'opportunité d'apprendre quelque chose aujourd'hui! Et
puis, vous savez ce qu'on dit, "ce qui ne nous tue pas nous rend plus
fort"! Alors, allez-y, soyez fort! Et n'oubliez pas, la prochaine
fois, vous gagnerez! Vous verrez! Vous êtes un gagnant, vous êtes un
champion! Vous êtes le meilleur! Vous êtes le roi du monde!
<img
src="/img/so_back.png"
alt="Une image décrivant le meme 'We are so back'"
/>
</p>
</div>
</div>
@@ -66,7 +66,7 @@ const close = () => {
}
.dialog-box {
background-image: url('../images/bg-grass.png');
background-image: url("/img/bg-grass.png");
padding: 0 20px;
border-radius: 5px;
width: 800px;
@@ -82,7 +82,7 @@ const close = () => {
.dialog-box h3 {
font-size: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
width: 100%;
padding: 10px 0;
@@ -0,0 +1,211 @@
<script setup lang="ts">
const settingsStore = useSettingsStore();
const isVisible = ref(false);
const _devicePixelRatio = computed(() => devicePixelRatio);
const _window = computed(() => window);
</script>
<template>
<button @click="isVisible = true" class="dialog-open-button">
Open Settings
</button>
<div
:class="{ visible: isVisible }"
class="dialog dialog-background"
@click="isVisible = false"
></div>
<div class="dialog dialog-box" :class="{ visible: isVisible }" @click.stop>
<div class="dialog-header">
<h3>Game</h3>
</div>
<div class="dialog-header">
<span>Save discard size</span>
</div>
<div class="dialog-body">
<input type="checkbox" v-model="settingsStore.saveDiscardSize" />
</div>
<div class="dialog-footer">
<template v-if="!settingsStore.saveDiscardSize"
>Discard will always open small
</template>
<template v-else="!settingsStore.saveDiscardSize"
>Discard will remember its size
</template>
</div>
<div class="dialog-header">
<h3>Visuals</h3>
</div>
<div class="dialog-header">
<span>Toggle Card Style</span>
</div>
<div class="dialog-body">
<button @click="settingsStore.switchCardStyles">Toggle Card style</button>
</div>
<div class="dialog-footer">
<span>{{ settingsStore.cardStyle }}</span>
</div>
<div class="dialog-header">
<span>Animation Speed</span>
</div>
<div class="dialog-body">
<input
type="range"
min="0"
max="250"
step="25"
v-model="settingsStore.animationSpeed"
/>
</div>
<div class="dialog-footer">
<span>{{ settingsStore.animationSpeed }}ms</span>
</div>
<div class="dialog-header">
<span>Fast mode</span>
</div>
<div class="dialog-body">
<input type="checkbox" v-model="settingsStore.fastAnimations" />
</div>
<div class="dialog-header">
<h3>Graphics</h3>
</div>
<div class="dialog-header">
<span>Shader Quality</span>
</div>
<div class="dialog-body">
<input
type="range"
min="1"
max="10"
v-model="settingsStore.shaderQuality"
:disabled="settingsStore.shaderFramerate == 0"
/>
</div>
<div class="dialog-footer">
<span v-if="_window"
>{{
Math.floor(
(_window.innerWidth *
_devicePixelRatio *
settingsStore.shaderQuality) /
10
)
}}x{{
Math.floor(
(_window.innerHeight *
_devicePixelRatio *
settingsStore.shaderQuality) /
10
)
}}</span
>
</div>
<div class="dialog-header">
<span>Shader Framerate</span>
</div>
<div class="dialog-body">
<input
type="range"
min="0"
max="65"
step="5"
v-model="settingsStore.shaderFramerate"
/>
</div>
<div class="dialog-footer">
<span v-if="settingsStore.shaderFramerate == 65">vsync</span>
<span v-else>{{ settingsStore.shaderFramerate }} fps</span>
</div>
<div class="dialog-header">
<span>Pixelated</span>
</div>
<div class="dialog-body">
<input type="checkbox" v-model="settingsStore.shaderPixelated" />
</div>
</div>
</template>
<style scoped>
.dialog {
position: fixed;
bottom: 0;
left: 0;
z-index: 1000;
transition: 0.3s all var(--animation-curve);
}
.dialog-open-button {
cursor: pointer;
}
.dialog-open-button:active {
filter: drop-shadow(0px 0px 1px);
}
.dialog-background {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
pointer-events: none;
z-index: 1000;
transition: 0.3s all linear;
}
.dialog-background.visible {
background: rgba(0, 0, 0, 0.5);
pointer-events: all;
}
.dialog-box {
bottom: 0;
left: 50%;
transform: translate(-50%, 100%);
padding: 0 20px;
border-radius: 5px;
width: 800px;
border-radius: 30px;
max-width: 100%;
color: white;
text-align: center;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
display: grid;
grid-template-columns: 33% 33% 33%;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
background: rgb(38, 62, 15);
}
.dialog-box.visible {
bottom: 50%;
transform: translate(-50%, 50%);
}
.dialog-box h3 {
font-size: 30px;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
width: 100%;
padding: 10px 0;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.dialog-header,
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
}
.dialog-header {
grid-column-start: 1;
}
.dialog-body {
padding: 10px 0;
grid-column-start: 2;
}
.dialog-footer {
grid-column-start: 3;
}
</style>
+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>
+468
View File
@@ -0,0 +1,468 @@
<script setup lang="ts">
import {
damageChampion,
discardCard,
endTurn,
joinGame,
lockCard,
lockEffect,
startGame,
useToken,
} from "~/netcode";
import {
CardEffects,
type Card,
type Effect,
type Game,
type Player,
} from "~/netcode/interfaces";
import { useTurnStore } from "~/stores/turnStore";
export interface Props {
player: Player;
game: Game;
hide_stack_and_discard?: boolean;
}
const authStore = useAuthStore();
const props = withDefaults(defineProps<Props>(), {
hide_stack_and_discard: () => false,
});
const playerList = computed(() =>
props.game.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
);
const selfPlayer = computed(() =>
authStore.logged
? playerList.value?.find((p) => p.user.userId == authStore.selfUser.userId)
: undefined
);
const currentTeam = computed(() =>
props.game.teams.find((t) => t._id == props.game.currentTurn.currentTeam)
);
const isPlayerTurn = computed(() =>
currentTeam.value?.players.some(
(p) => p.user.userId == authStore.selfUser.userId
)
);
const isSelf = computed(
() => props.player.user.userId === authStore.selfUser.userId
);
const isSelfTurn = computed(() => isSelf && isPlayerTurn);
const cardTokens = computed(() =>
props.game.currentTurn.tokens.filter(
(t) =>
[CardEffects.PREPARE, CardEffects.STUN].includes(t.effect) ||
(CardEffects.SACRIFICE == t.effect && isSelf)
)
);
const warningText = ref("Effets restants!");
// const make_discard = (target: Player) =>
// clientStore.findUseEffect(CardEffects.MAKE_DISCARD, target);
// const sacrifice = (target: CardParser) => clientStore.findUseEffect(CardEffects.SACRIFICE, target);
// const stun = (target: CardParser) => clientStore.findUseEffect(CardEffects.STUN, target)
// const restack_discarded_champion = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CHAMPION, target)
// const restack_discarded_card = (target: Card) => clientStore.findUseEffect(CardEffects.RESTACK_DISCARDED_CARD, target)
// const prepare_champion = (target: CardParser) => clientStore.findUseEffect(CardEffects.PREPARE, target)
let warning = ref(false);
// function has_actions_remaining(): boolean {
// return (goStore.self?.turn?.damage_reserve ? goStore.self?.turn?.damage_reserve > 0 : false)
// || (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= (goStore.current_game?.market.reduce((min, card) => Math.min(min, card?.cost ?? Infinity), Infinity) ?? Infinity) : false)
// || (goStore.self?.turn?.fuck_u_markers ? goStore.self?.turn?.fuck_u_markers > 0 : false)
// || !goStore.self?.turn?.effects_availables.every(e => !e.usable || e.effect_type == EffectType.SUICIDE)
// || !goStore.self?.board.every(c => (c.uuid in (goStore.self?.turn?.cards_locked ?? {})))
// || false
// }
// function can_buy_fire_gem(): boolean {
// return (goStore.self?.turn?.gold_reserve ? goStore.self?.turn?.gold_reserve >= 2 : false)
// }
function checkEndTurn() {
// TODO: Add a confirmation dialog if resources are left
// if (
// (!warning.value)
// && has_actions_remaining()) {
// // Set a warning flag to true
// // Display an alert somehow using this warning flag
// warning.value = true
// }
// else if (!warning.value && can_buy_fire_gem()) {
// warningText.value = "P'tite Fire gem?"
// warning.value = true
// }
// else {
// warningText.value = "Effets restants!"
// warning.value = false
// endTurn()
// }
endTurn(props.game._id);
}
function boardCardClick(cardUUID: string) {
if (props.game.currentTurn.cardsLocked.includes(cardUUID)) {
//Card already locked
return;
}
lockCard(props.game._id, cardUUID);
}
function effectClick(cardUUID: string, effectIndex: number) {
const card = props.player.board.cards.find((c) => c._id == cardUUID);
if (!card) {
console.error("card not found");
return;
}
if (!props.game.currentTurn.cardsLocked.includes(cardUUID)) {
return;
}
const effectUUID = card.effects[effectIndex]._id;
lockEffect(props.game._id, effectUUID);
}
// function damage_team_all(team: Team) {
// for (let i = 0; i < (goStore.self?.turn?.damage_reserve ?? 0); i++) {
// damage_team(team)
// }
// }
// function check_for_guard(player: Player) {
// return player.board.some(c => c.guard === true)
// }
function tokenClick({ card, token }: { card: Card; token: Effect }) {
useToken(props.game._id, token._id, card._id, props.player._id);
}
function damageChampionClick(card: Card) {
damageChampion(props.game._id, props.player._id, card._id);
}
function discardCardClick(card: Card) {
discardCard(props.game._id, card._id);
}
</script>
<template>
<div class="player" :class="{ turn: isPlayerTurn, self: isSelf }">
<div id="current_player_board">
<!-- <div class="other_player_buttons_container">
<button @click.stop="damage_team(props.player.team)"
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team" class="damage_button"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
Damage
</button>
<button @click.stop="damage_team_all(props.player.team)"
v-if="isSelfTurn && isPlayerEnemy && goStore.self?.turn && props.player.team"
class="damage_button all_damage_button"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) <= 0 || check_for_guard(props.player)">
(All)
</button>
<button v-if="goStore.self && isSelfTurn && isPlayerEnemy && goStore.self?.turn"
@click.stop="make_discard(props.player)" class="make_discard_button"
:disabled="!clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.MAKE_DISCARD) && e.usable)">
make Discard
</button>
</div> -->
<!-- <br /> -->
<GameCardGrouped
:container="player.board"
:wrapped="false"
:playingTurn="props.game.currentTurn"
@cardClick="boardCardClick"
@effectClick="effectClick"
:tokens="cardTokens"
@tokenClick="tokenClick"
:championsKillable="!isSelf && isSelfTurn.value"
:discardable="
(player.fuckUMarkers > 0 || player.discardMarkers > 0) && isSelf
"
@damageChampionClick="damageChampionClick"
@discardCardClick="discardCardClick"
>
</GameCardGrouped>
<br />
<!-- <div class="cards_container"> -->
<!-- HAND -->
<!-- <template v-if="isSelf">
<CardPreview :card_id="c?.card_id" v-for="c in props.player.hand" @click="c && playCard(c)">
<button
v-if="props.player.turn && c && isSelf && (props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
@click.stop="discard(c)">
DISCARD
</button>
</CardPreview>
</template> -->
<!-- BOARD -->
<!-- <CardPreview :card_id="c.cardId" v-for="c in props.player.board.cards"
@click="console.log/*lockCard(c)*/"
:locked="/*props.player.turn && c.uuid in props.player.turn.cards_locked*/ false"> -->
<!-- <template v-if="c && isSelf && isPlayerTurn && props.player.turn">
// Card Locked
<template v-if="c.uuid in props.player.turn.cards_locked">
<template v-for="e in clientStore.maskedServerEffects">
<template v-if="e.card == c">
<button v-if="!clientStore.effectsHandled.has(e.effect)" @click.stop="useEffect(e)"
:disabled="!e.usable"
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
EFFECT<br>
{{ e.effect + " " + (e.effect_type ?? '') }}
</button>
<button v-if="clientStore.effectsHandled.has(e.effect)"
@click.stop="clientStore.activateEffect(e)" :disabled="!e.usable"
:class="{ 'suicide_effect': e.effect_type == EffectType.SUICIDE }">
EFFECT<br>
{{ e.effect + " " + (e.effect_type ?? '') }}
</button>
</template>
</template>
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.PREPARE) && e.usable) && c.card_type == CardType.CHAMPION"
@click.stop="prepare_champion(c)">PREPARE</button>
</template>
// Card Unlocked
<template v-if="!(c.uuid in props.player.turn.cards_locked)">
<button v-if="(props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0"
@click.stop="discard(c)">
DISCARD
</button>
<button
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.SACRIFICE) && e.usable)"
@click.stop="sacrifice(c)">
SACRIFICE
</button>
<button @click.stop="lockCard(c)"
:disabled="((props.player.turn.discard_markers + props.player.turn.fuck_u_markers) > 0 && c !== props.player.turn.discard_marker_card) || props.player.turn.fuck_u_markers > 0">
LOCK
</button>
</template>
</template>
<template
v-if="c && goStore.self?.turn && isPlayerEnemy && isSelfTurn && (c.health_as_champion ?? 0) > 0">
<button class="enemy_action"
:disabled="(goStore.self?.turn.damage_reserve ?? 0) < (c.health_as_champion ?? Infinity) || (check_for_guard(props.player) && !c.guard)"
@click.stop="damage_champion(c)">
DAMAGE ({{ c.health_as_champion }})
</button>
<button class="enemy_action"
v-if="clientStore.activatedClientside.find((e: Effect) => (e.effect == CardEffects.STUN) && e.usable)"
@click.stop="stun(c)" :disabled="(check_for_guard(props.player) && !c.guard)">
STUN
</button>
</template> -->
<!-- </CardPreview> -->
<!-- </div> -->
</div>
</div>
</template>
<style scoped>
.warning {
background-color: rgba(255, 0, 0, 0.7);
color: white;
padding: 5px;
border-radius: 5px;
width: 100px;
font-size: 0.85em;
margin-left: 5px;
visibility: hidden;
}
.warning.visible {
visibility: visible;
}
.warning-fire-gem {
background-color: rgba(255, 165, 0, 0.7);
}
#turn_buttons {
grid-area: turn_buttons;
}
#end_turn {
z-index: 10;
height: 100%;
}
#end_turn.hidden {
visibility: hidden;
}
.player {
display: grid;
height: 100%;
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr;
/* border-top: 5px solid black; */
grid-template-areas:
"player_stats"
"player"
"turn_buttons";
justify-items: center;
transition: all 1s;
}
#current_player_board {
grid-area: player;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 1200px;
}
.damage_button {
background-color: #ff4d4d;
color: white;
border: none;
border-radius: 8px;
padding: 5px 25px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.damage_button:disabled {
background-color: rgba(255, 77, 77, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
}
.damage_button:hover {
background-color: #e60000;
}
.damage_button:disabled:hover {
background-color: rgba(230, 0, 0, 0.5);
}
.damage_button:active {
transform: translateY(2px);
box-shadow: none;
}
.all_damage_button {
padding: 5px 5px;
font-size: 14px;
}
.make_discard_button {
background-color: #55b42f;
color: white;
border: none;
border-radius: 8px;
padding: 5px 15px;
margin: 5px 0 0 0;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.make_discard_button:disabled {
background-color: rgba(85, 180, 47, 0.5);
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
cursor: not-allowed;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.make_discard_button:hover {
background-color: #03883a;
}
.make_discard_button:disabled:hover {
background-color: rgba(3, 136, 58, 0.5);
}
.make_discard_button:active {
transform: translateY(2px);
box-shadow: none;
}
.turn_icon {
position: relative;
}
#player_info {
display: flex;
align-items: center;
min-width: 200px;
}
#player_banner.big {
padding: 0;
left: 50%;
transform: translateX(-50%);
max-width: min-content;
border: 5px solid black;
border-radius: 10px;
filter: drop-shadow(2.5px 7.5px 1px rgba(0, 0, 0, 0.6));
backdrop-filter: blur(5px) brightness(80%) grayscale(80%);
}
#player_banner img {
height: 50px;
border-radius: 25px;
}
.turn_icon img {
height: 50px;
}
.turn_icon_number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.end_turn_button {
background-color: #4caf50;
/* Green background color */
color: white;
border: none;
border-radius: 8px;
padding: 13px 10px;
margin: 0 0 0 5px;
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
cursor: pointer;
transition: background-color 0.3s ease;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
}
.end_turn_button:hover {
background-color: #45a049;
/* Darker green on hover */
}
.end_turn_button:active {
transform: translateY(2px);
/* Add a slight press effect on click */
box-shadow: none;
/* Remove shadow on click */
}
.suicide_effect {
background: linear-gradient(to bottom right, #666666, #ccc);
color: red;
}
</style>
+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>
+60
View File
@@ -0,0 +1,60 @@
import { useAuthStore } from "#imports";
export default defineNuxtRouteMiddleware(async (to, from) => {
const authStore = useAuthStore();
const config = useRuntimeConfig();
// if (to.path == '/') {
// const code = to.query.code
// if (code) {
// if (!Array.isArray(code)) {
// try {
// await oauth2_register_discord(code)
// return navigateTo('/lobby')
// } catch {
// return navigateTo('/login')
// }
// } else {
// return navigateTo('/login')
// }
// }
// return navigateTo("/lobby")
// }
// if (!goStore.discordId) {
// const heron_session = useCookie('heron_session')
// if (!heron_session.value) { return navigateTo('/login') }
// try {
// await heron_cookie_login(heron_session.value)
// if (to.path == '/') {
// return navigateTo("/lobby")
// }
// } catch {
// return navigateTo('/login')
// }
// }
if (to.path.startsWith("/game")) {
return;
}
if (to.path == "/login") {
return;
}
if (to.path == "/lobby") {
return;
}
if (to.path == "/rules") {
return;
}
if (to.path == "/test") {
return;
}
// In a real app you would probably not redirect every route to `/`
// however it is important to check `to.path` before redirecting or you
// might get an infinite redirect loop
if (to.path !== "/") {
return navigateTo("/");
}
return navigateTo("/lobby");
});
-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
}
+15
View File
@@ -0,0 +1,15 @@
export function delay(delay: number) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
// export type ContainerHandler = {
// populateContainer: () => void;
// shuffleContainer: () => void;
// getCardsPosition: (cards: string) => any;
// startAnimation: () => void;
// endAnimation: () => void;
// };
// export const containerEvents = reactive(new Map<string, ContainerHandler>());
+150 -339
View File
@@ -1,375 +1,186 @@
import { type Card, create_card, type CardNetcode } from "./Card";
import {
type Effect,
create_effect,
type EffectNetcode,
EffectType,
CardEffects,
} from "./Effect";
import { type Game, create_game, type GameNetcode } from "./Game";
import type { GameObjectNetcode } from "./GameObject";
import { type Player, create_player, type PlayerNetcode } from "./Player";
import { type Team, create_team, type TeamNetcode } from "./Team";
import { type Turn, create_turn } from "./Turn";
import { type Lobby, create_lobby, type LobbyNetcode } from "./Lobby";
import type { Game, User } from "./interfaces";
import { type Ref } from "vue";
let socket: WebSocket;
export class GameObjectRegister {
readonly effects: Map<string, Effect>;
readonly cards: Map<string, Card>;
readonly teams: Map<string, Team>;
readonly players: Map<string, Player>;
readonly turns: Map<string, Turn>;
readonly games: Map<string, Game>;
context: "lobby" | "login" | "pregame" | "game" | "rules" = "login";
private readonly objects: Map<string, GameObjectNetcode>;
current_game: Ref<Game | null>;
lobby: Ref<Lobby | null>;
self: Ref<Player | null>;
zoomKeyPressed: Ref<boolean>
hoveredCard: Ref<number | undefined>
clientMouseX: Ref<number>
clientMouseY: Ref<number>
discordId: Ref<string>
constructor() {
this.effects = reactive(new Map());
this.cards = reactive(new Map());
this.teams = reactive(new Map());
this.players = reactive(new Map());
this.turns = reactive(new Map());
this.games = reactive(new Map());
this.objects = reactive(new Map());
this.current_game = ref(null);
this.lobby = ref(null);
this.self = ref(null);
this.zoomKeyPressed = ref(false);
this.hoveredCard = ref()
this.clientMouseX = ref(0)
this.clientMouseY = ref(0)
this.discordId = ref("")
}
update_object(obj: GameObjectNetcode) {
// console.log(obj);
// console.log(obj.uuid);
// console.log(this.objects.get(obj.uuid));
Object.assign(this.objects.get(obj.uuid) as GameNetcode, obj);
if (unref(this.current_game)?.uuid === obj.uuid) {
triggerRef(this.current_game);
} else if (unref(this.lobby)?.uuid === obj.uuid) {
triggerRef(this.lobby);
} else if (unref(this.self)?.uuid === obj.uuid) {
triggerRef(this.self);
} else if (unref(this.self)?.turn?.uuid === obj.uuid) {
triggerRef(this.self);
}
}
createObject = {
effect: (data: EffectNetcode) => {
const effect = create_effect(data, this);
this.effects.set(data.uuid, effect);
this.objects.set(data.uuid, data);
},
card: (data: CardNetcode) => {
const card = create_card(data, this);
this.cards.set(data.uuid, card);
this.objects.set(data.uuid, card);
},
player: (data: PlayerNetcode) => {
const player = create_player(data, this);
this.players.set(data.uuid, player);
this.objects.set(data.uuid, player);
},
team: (data: TeamNetcode) => {
const team = create_team(data, this);
this.teams.set(data.uuid, team);
this.objects.set(data.uuid, team);
},
game: (data: GameNetcode) => {
const game = create_game(data, this);
this.games.set(data.uuid, game);
this.objects.set(data.uuid, game);
},
turn: (data: any) => {
const turn = create_turn(data, this);
this.turns.set(data.uuid, turn);
this.objects.set(data.uuid, turn);
},
lobby: (data: LobbyNetcode) => {
this.lobby.value = create_lobby(data, this);
this.objects.set(data.uuid, this.lobby.value);
},
} as { [id: string]: (data: any) => any };
setSelf = (uuid: string) => {
this.self.value = this.players.get(uuid) ?? null;
};
setDiscordId = (username: string) => {
this.discordId.value = username
}
setCurrentGame = (uuid: string) => {
this.current_game.value = this.games.get(uuid) ?? null;
};
}
const goStore = reactive(new GameObjectRegister());
export const setup_websocket = () =>
new Promise<void>((resolve, reject) => {
if (socket) {
return resolve();
}
const config = useRuntimeConfig();
const heron_session = useCookie("heron_session");
socket = new WebSocket(config.public.websocketEndpoint);
socket.onopen = function (e) {
if (socket.readyState == 1) {
resolve();
}
console.log("[open] Connection established");
};
socket.onclose = function (event) {
if (event.wasClean) {
console.warn(
`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`
);
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
console.error("[close] Connection died");
}
};
socket.onerror = function (error) {
console.error(error);
};
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
if (data.type == "create" && data.data_type == "object") {
if (!(data.data.object_type in goStore.createObject)) {
throw new Error(
`Object ${data.data.object_type} couldn't be created`
);
}
goStore.createObject[data.data.object_type](data.data);
} else if (data.type == "update" && data.data_type == "object") {
goStore.update_object(data.data);
} else if (data.type === "context") {
goStore.context = data.data;
// console.log(data.data);
} else if (data.type === "set") {
if (data.data_type == "self") {
goStore.setSelf(data.data);
} else if (data.data_type == "current_game") {
goStore.setCurrentGame(data.data);
} else if (data.data_type == "session_cookie") {
heron_session.value = data.data;
} else if (data.data_type == "self_discord_id") {
goStore.setDiscordId(data.data)
}
} else {
// console.log(data.type, data.data_type, data.data);
}
};
export function subscribe(endpoint: string) {
const config = useRuntimeConfig();
const evtSource = new EventSource(config.public.endpoint + endpoint, {
withCredentials: true,
});
export async function heron_cookie_login(cookie: string) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "cookie_login",
data_type: "cookie",
data: cookie,
})
return evtSource;
}
export async function whoami() {
const config = useRuntimeConfig();
const req = await useFetch<User>(config.public.endpoint + "/whoami", {
credentials: "include",
server: false,
});
await req.execute();
if (req.error.value && req.error.value.statusCode == 401) {
// await navigateTo(config.public.discordLoginEndpoint, {
// external: true,
// });
throw new Error("You are not logged in");
}
if (req.error.value) {
throw new Error(
"Something went wrong while trying to get user information"
);
}
return req.data;
}
export async function createGame() {
const config = useRuntimeConfig();
const res = await $fetch<Game>(config.public.endpoint + "/games", {
method: "POST",
credentials: "include",
});
navigateTo(`/game?id=${res._id}`);
}
export async function startGame(game: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(
config.public.endpoint + `/games/${game}/start`,
{
method: "POST",
credentials: "include",
}
);
}
export async function oauth2_register_discord(code: string) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "discord_login",
data_type: "code",
data: code,
})
export async function joinGame(game: string) {
const config = useRuntimeConfig();
const res = await $fetch<Game>(
config.public.endpoint + `/games/${game}/join`,
{
method: "POST",
credentials: "include",
}
);
}
export async function register(username: string) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "register",
data_type: "username",
data: username,
})
);
}
export async function login(username: string) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "login",
data_type: "username",
data: username,
})
);
}
export async function setReady(value: boolean) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "ready",
data_type: "ready_state",
data: value,
})
);
}
export async function buy(card: Card) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "buy",
data: card.uuid,
})
);
}
export async function endTurn() {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "end_turn",
data: true,
})
);
}
export function playAllCards(cards: Card[]) {
cards.forEach((c: any) => {
playCard(c);
export async function endTurn(game: string) {
const config = useRuntimeConfig();
await $fetch(config.public.endpoint + `/games/${game}/endTurn`, {
method: "POST",
credentials: "include",
});
}
export async function playCard(card: Card) {
// console.log(card.uuid);
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "play_card",
data: card.uuid,
})
export async function lockCard(game: string, cardId: string) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/lockCard/${cardId}`,
{
method: "POST",
credentials: "include",
}
);
}
export async function lockCard(card: Card) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "lock_card",
data: card.uuid,
})
export async function buyCard(game: string, cardId: string) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/buyCard/${cardId}`,
{
method: "POST",
credentials: "include",
}
);
}
export async function useEffect(
effect: Effect,
target?: Effect | Card | Player | Team
export async function buyGem(game: string) {
const config = useRuntimeConfig();
await $fetch(config.public.endpoint + `/games/${game}/turn/buyGem`, {
method: "POST",
credentials: "include",
});
}
export async function useToken(
game: string,
tokenId: string,
cardId?: string,
playerId?: string
) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "use_effect",
data: { effect_id: effect.uuid, target: target?.uuid },
})
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/useToken/${tokenId}`,
{
method: "POST",
credentials: "include",
body: { cardId, playerId },
}
);
}
export async function discard(card: Card) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "discard",
data: card.uuid,
})
);
}
h;
export async function heal() {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "heal",
data: 1,
})
export async function lockEffect(game: string, effectId: string) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/lockEffect/${effectId}`,
{
method: "POST",
credentials: "include",
}
);
}
export async function damage_team(target: Team) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "damage_team",
data: target.uuid,
})
export async function makeDiscard(
game: string,
playerId: string,
amount: number = 1
) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/makeDiscard/${playerId}`,
{
method: "POST",
credentials: "include",
}
);
}
export async function damage_champion(target: Card) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "action",
data_type: "damage_champion",
data: target.uuid,
})
export async function damagePlayer(
game: string,
playerId: string,
amount: number = 1
) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/damagePlayer/${playerId}`,
{
method: "POST",
credentials: "include",
body: { amount },
}
);
}
export async function netcode_create_game() {
await setup_websocket();
socket.send(
JSON.stringify({
type: "create",
data_type: "game",
data: "bite",
})
export async function damageChampion(
game: string,
playerId: string,
cardId: string
) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/damageChampion/${cardId}`,
{
method: "POST",
credentials: "include",
body: { playerId },
}
);
}
export async function join_game(game_uuid: string) {
await setup_websocket();
socket.send(
JSON.stringify({
type: "join",
data_type: "game",
data: game_uuid,
})
export async function discardCard(game: string, cardId: string) {
const config = useRuntimeConfig();
await $fetch(
config.public.endpoint + `/games/${game}/turn/discardCard/${cardId}`,
{
method: "POST",
credentials: "include",
}
);
}
export default goStore;
+112
View File
@@ -0,0 +1,112 @@
export enum CardEffects {
//Base
GOLD = "gold",
DAMAGE = "damage",
HEAL = "heal",
PREPARE = "prepare",
STUN = "stun",
SACRIFICE = "sacrifice",
DRAW = "draw",
DRAW_AND_DISCARD = "draw_and_discard",
MAKE_DISCARD = "make_discard",
RESTACK_DISCARDED_CHAMPION = "restack_discarded_champion",
RESTACK_DISCARDED_CARD = "restack_discarded_card",
STACK_NEXT_ACTION_BOUGHT = "stack_next_action_bought",
STACK_NEXT_CARD_BOUGHT = "stack_next_card_bought",
PLAY_NEXT_CARD_BOUGHT = "play_next_card_bought",
//DLCs
BUY_FOR_FREE = "buy_for_free",
//Journeys
DAMAGE_ALL_CHAMPIONS = "damage_all_champions",
//Journeys: Hunters
DISCARD_X_AND_DRAW_X = "discard_x_and_draw_x",
PREPARE_ANOTHER_CHAMPION = "prepare_another_champion",
//Journey: Travelers
PREPARE_ALL_CHAMPIONS = "prepare_all_champions",
CHEAPER_CHAMPION = "cheaper_champion",
CHEAPER_CHAMPIONS_PASSIVE = "cheaper_champions_passive",
CHEAPER_ACTION = "cheaper_action",
CONTROL_OPPOSING_CHAMPION_PASSIVE = "control_opposing_champion_passive",
RESTACK_DISCARDED_ACTION = "restack_discarded_action",
//Ancestry
KEEP_IN_HAND = "keep_in_hand",
BUY_GEM_FOR_FREE = "buy_gem_for_free",
CHEAPER_SKILLS_PASSIVE = "cheaper_skills_passive",
CHEAPER_CARD_IF_HIGHER_PRICE = "cheaper_card_if_higher_price",
PICK_FACTION = "pick_faction",
}
export enum CardType {
HERO = "hero",
HERO_ABILITY = "hero_ability",
CURRENCY = "currency",
WEAPON = "weapon",
CHAMPION = "champion",
ACTION = "action",
ITEM = "item",
}
export interface GameObject {
_id: string;
}
export interface PlayingTurn {
currentTeam: string;
cardsLocked: string[];
lockedEffects: string[];
tokens: Effect[];
damage: number;
gold: number;
}
export interface Game extends GameObject {
fireGems: Container;
market: Container;
marketStack: Container;
teams: Team[];
currentTurn: PlayingTurn;
started: boolean;
owner: User;
}
export interface Team extends GameObject {
players: Player[];
health: number;
}
export interface Player extends GameObject {
board: Container;
discard: Container;
hand: Container;
stack: Container;
user: User;
discardMarkers: number;
fuckUMarkers: number;
}
export interface User extends GameObject {
userId: string;
avatar: string;
username: string;
}
export interface Container extends GameObject {
cards: Card[];
shuffling?: boolean;
}
export interface Effect extends GameObject {
effect: CardEffects;
}
export interface Card extends GameObject {
cardId: number;
cardType: CardType;
effects: Effect[];
defense?: number;
guard?: boolean;
}
+45
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 index.html;
try_files $uri $uri/ /index.html =404;
}
error_page 404 /usr/share/nginx/html/404.html;
# redirect server error pages to the static page /50x.html
#
# error_page 500 502 503 504 /50x.html;
# location = /50x.html {
# root /usr/share/nginx/html;
# }
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
+18 -8
View File
@@ -1,14 +1,24 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
ssr: false,
ssr: true,
runtimeConfig: {
public: {
websocketEndpoint: 'ws://127.0.0.1:8765', // can be overridden by NUXT_PUBLIC_API_BASE environment variable
discordLoginEndpoint: ''
}
welcomeMessage: "Hello",
endpoint: "https://playtest.legonzaur.fr/api", // can be overridden by NUXT_PUBLIC_ENDPOINT environment variable
discordLoginEndpoint: process.env.NUXT_PUBLIC_DISCORD_LOGIN_ENDPOINT,
devMode: false,
},
},
modules: [
'@pinia/nuxt',
],
})
modules: ["@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"],
vue: {
runtimeCompiler: true,
},
nitro: {
prerender: {
routes: ["/rules", "/login", "/game", "/lobby"],
crawlLinks: true,
},
},
});
+35
View File
@@ -8,10 +8,14 @@
"hasInstallScript": true,
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"gsap": "^3.12.5",
"nuxt": "^3.11.2",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@pinia-plugin-persistedstate/nuxt": "^1.2.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -2093,6 +2097,21 @@
"node": ">=0.10"
}
},
"node_modules/@pinia-plugin-persistedstate/nuxt": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@pinia-plugin-persistedstate/nuxt/-/nuxt-1.2.0.tgz",
"integrity": "sha512-2rtgx5viGSMQMCoFYZMHguA2FhFKCUvw0PwETfqQegsWeBHlqk1/D0G/9xqep8Hq+c1BuFx+jNLJzoLXtYfivg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.8.0",
"defu": "^6.1.2",
"pinia-plugin-persistedstate": ">=3.2.0"
},
"peerDependencies": {
"@pinia/nuxt": "^0.5.0"
}
},
"node_modules/@pinia/nuxt": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.5.1.tgz",
@@ -5652,6 +5671,12 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/gsap": {
"version": "3.12.5",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
},
"node_modules/gzip-size": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
@@ -7803,6 +7828,16 @@
}
}
},
"node_modules/pinia-plugin-persistedstate": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz",
"integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"pinia": "^2.0.0"
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz",
+7
View File
@@ -3,6 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"cards-gen": "sh -c 'docker run --rm --pull=always -v ./assets/images/cards/heron-svg:/app/output/heron git.legonzaur.fr/heronfief/cards-gen:main' && sudo chown -R $USER:$USER ./assets/images/cards/heron-svg",
"pull-images": "sh -c 'docker run --rm --pull=always -v ./public/img:/repo/images/skins git.legonzaur.fr/heronfief/cards-gen:main cp -r img/skins/ /repo/images/skins' && sudo chown -R $USER:$USER ./public/",
"fetch": "npm run cards-gen && npm run pull-images",
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
@@ -11,9 +14,13 @@
},
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"gsap": "^3.12.5",
"nuxt": "^3.11.2",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@pinia-plugin-persistedstate/nuxt": "^1.2.0"
}
}
+583
View File
@@ -0,0 +1,583 @@
<script setup lang="ts">
import LoadingScreen from "~/components/game/overlay/LoadingScreen.vue";
import { endTurn, subscribe, useToken, startGame, joinGame } from "~/netcode";
import { delay } from "~/netcode/events";
import {
type Container,
type Effect,
type Game,
type Player,
} from "~/netcode/interfaces";
import { playSound } from "~/helpers/audioHelpers";
const config = useRuntimeConfig();
const settingsStore = useSettingsStore();
const authStore = useAuthStore();
const route = useRoute();
const gameId = route.query.id;
const {
data: game,
pending,
error,
refresh,
execute,
} = await useFetch<Game>(config.public.endpoint + `/games/${gameId}`, {
credentials: "include",
server: false,
immediate: true,
});
await execute();
const containers = computed<Record<string, Container>>(() => ({
[game.value!.fireGems._id]: game.value!.fireGems,
[game.value!.market._id]: game.value!.market,
[game.value!.marketStack._id]: game.value!.marketStack,
...Object.fromEntries(
game
.value!.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
.map((p) => [
[p.board._id, p.board],
[p.discard._id, p.discard],
[p.hand._id, p.hand],
[p.stack._id, p.stack],
])
.flat(1)
),
}));
let eventSource: EventSource;
const actions = ref<(() => Promise<any>)[]>([]);
function queueAction(callback: () => Promise<any>) {
actions.value.push(callback);
if (actions.value.length == 1) {
execAction(callback);
}
}
async function execAction(callback: () => Promise<any>) {
await callback();
actions.value.shift();
if (actions.value.length > 0) {
execAction(actions.value[0]);
}
}
const handlers = {
start: async (data: any, game: Game) => {
game.started = true;
game.currentTurn = data.game.currentTurn;
},
populateContainer: async (data: any) => {
const fromContainer = containers.value[data.container._id as string];
playSound("/sounds/distribute.mp3");
for (const card of data.container.cards) {
fromContainer.cards.push(card);
// if (!settingsStore.fastAnimations) {
// await delay(settingsStore.animationSpeed);
// }
}
},
shuffleContainer: async (data: any) => {
playSound("/sounds/shuffle.mp3");
const container = containers.value[data.container as string];
container.shuffling = true;
await delay(settingsStore.animationSpeed * 2);
container.shuffling = false;
await delay(settingsStore.animationSpeed * 2);
},
distributeCards: async (data: any) => {
const fromContainer = containers.value[data.from as string];
const toContainer = containers.value[data.to as string];
if (settingsStore.fastAnimations) {
playSound("/sounds/distribute.mp3");
}
for (const card of data.cards) {
const index = fromContainer.cards.findIndex(
(c) => !c || c._id == card._id
);
fromContainer.cards.splice(index, 1);
if (!settingsStore.fastAnimations) {
await delay(settingsStore.animationSpeed);
playSound("/sounds/distribute.mp3");
}
toContainer.cards.push(card);
if (!settingsStore.fastAnimations) {
await delay(settingsStore.animationSpeed);
}
}
await delay(settingsStore.animationSpeed * 2);
},
endTurn: async (data: any, game: Game) => {
game.currentTurn = data.currentTurn;
await delay(settingsStore.animationSpeed);
const playingPlayer = game.teams.find(
(t) => t._id == game.currentTurn.currentTeam
)?.players[0];
if (playingPlayer) {
focusPlayer(playingPlayer);
await delay(settingsStore.animationSpeed);
}
},
lockCard: async (data: any, game: Game) => {
playSound("/sounds/lock.mp3");
game.currentTurn.cardsLocked.push(data.card);
await delay(settingsStore.animationSpeed);
},
destroyCard: async (data: any) => {
const fromContainer = containers.value[data.from as string];
const index = fromContainer.cards.findIndex(
(c) => !c || c._id == data.card
);
fromContainer.cards.splice(index, 1);
playSound("/sounds/destroy.mp3");
await delay(settingsStore.animationSpeed);
},
joinGame: async (data: any, game: Game) => {
playSound("/sounds/teleport.mp3");
game.teams.push(data.team);
},
lockEffect: async (data: any, game: Game) => {
game.currentTurn.lockedEffects.push(data.effect);
await delay(settingsStore.animationSpeed);
},
unlockEffect: async (data: any, game: Game) => {
const effectIndex = game.currentTurn.lockedEffects.indexOf(data.effect);
game.currentTurn.lockedEffects.splice(effectIndex, 1);
await delay(settingsStore.animationSpeed);
},
"currentTurn.updateGold": async (data: any, game: Game) => {
console.log(`adding gold amount : ` + data.operation);
game.currentTurn.gold = data.gold;
},
"currentTurn.updateDamage": async (data: any, game: Game) => {
console.log(`adding damage amount : ` + data.operation);
game.currentTurn.damage = data.damage;
},
"currentTurn.addToken": async (data: any, game: Game) => {
game.currentTurn.tokens.push(data.token);
await delay(settingsStore.animationSpeed);
},
"currentTurn.useToken": async (data: any, game: Game) => {
const index = game.currentTurn.tokens.findIndex(
(t) => t._id == data.tokenId
);
game.currentTurn.tokens.splice(index, 1);
if (!settingsStore.fastAnimations) {
await delay(settingsStore.animationSpeed);
}
},
"team.updateHealth": async (data: any, game: Game) => {
const team = game.teams.find((t) => t._id == data.team);
if (!team) {
throw new Error("Team not found");
}
console.log(`adding health amount : ` + data.operation);
team.health = data.health;
},
"player.addFuckUMarker": async (data: any, game: Game) => {
console.log(`adding fuckUMarkers : ` + data.operation);
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
if (!targetPlayer) {
throw new Error("Cannot add fuckU Markers : player not found");
}
targetPlayer.fuckUMarkers = data.fuckUMarkers;
},
"player.removeFuckUMarker": async (data: any, game: Game) => {
console.log(`adding fuckUMarkers : ` + data.operation);
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
if (!targetPlayer) {
throw new Error("Cannot remove fuckU Markers : player not found");
}
targetPlayer.fuckUMarkers = data.fuckUMarkers;
},
"player.addDiscardMarker": async (data: any, game: Game) => {
console.log(`adding discardMarkers : ` + data.operation);
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
if (!targetPlayer) {
throw new Error("Cannot add discard Markers : player not found");
}
targetPlayer.discardMarkers = data.discardMarkers;
},
"player.removeDiscardMarker": async (data: any, game: Game) => {
console.log(`adding fuckUMarkers : ` + data.operation);
const targetPlayer = playerList.value?.find((p) => p._id == data.playerId);
if (!targetPlayer) {
throw new Error("Cannot remove discard Markers : player not found");
}
targetPlayer.discardMarkers = data.discardMarkers;
},
killTeam: async (data: any, game: Game) => {
const index = game.teams.findIndex((t) => t._id == data.teamId);
if (!index) {
console.error("team index not found. Desyncs might happen");
await refresh();
return;
}
game.teams.splice(index, 1);
},
consumeEffect: async (data: any) => {
console.warn("consumeEffect not implemented");
},
endGame: async () => {
alert("game ended");
},
};
onBeforeMount(async () => {
if (game.value == null) {
navigateTo("/lobby");
throw new Error("game not found");
}
if (game.value.started) {
focusedPlayer.value = game.value.teams.find(
(t) => t._id == game.value?.currentTurn.currentTeam
)?.players[0];
} else {
focusedPlayer.value = selfPlayer.value ?? playerList.value![0];
}
eventSource = subscribe(`/games/${gameId}/subscribe`);
eventSource.addEventListener("message", async (message) => {
const current_game = game.value;
if (!current_game) {
throw new Error("game not found");
}
const data = JSON.parse(message.data);
queueAction(async () => {
if (data.type in handlers) {
await handlers[data.type as keyof typeof handlers](data, current_game);
} else {
console.error(data.type + " not implemented");
}
});
});
});
const playerList = computed(
() =>
game.value &&
game.value.teams.reduce((acc, t) => acc.concat(t.players), [] as Player[])
);
const selfPlayer = computed(() =>
authStore.logged
? playerList.value?.find((p) => p.user.userId == authStore.selfUser.userId)
: undefined
);
const isTurn = computed(
() =>
(game.value &&
selfPlayer.value &&
game.value.teams.find((t) => t.players.includes(selfPlayer.value!))
?._id == game.value.currentTurn.currentTeam) ??
false
);
const turnChangeAnimationSpeed = computed(
() => settingsStore.animationSpeed * 2 + "ms"
);
// TODO : current turn sound
// TODO : winner screen
// TODO : join sound
const showResults = ref(false);
const isWinner = ref(false);
const loaded = ref(false);
const discard_unwrapped = ref(false);
const focusedPlayer = ref<Player | undefined>(undefined);
function showLoserScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true;
isWinner.value = false;
}
function showWinnerScreen() {
// playSound('/sounds/ding.mp3')
showResults.value = true;
isWinner.value = true;
}
function focusPlayer(p: Player) {
focusedPlayer.value = p;
}
function useTokenClick(t: Effect) {
console.log(t);
useToken(game.value!._id, t._id);
}
function showDiscard() {
discard_unwrapped.value = true;
}
onUnmounted(() => {
if (eventSource) {
eventSource.close();
}
});
</script>
<template>
<div @contextmenu.prevent="false" id="game_board">
<AnimatedBackground
@ready="loaded = true"
:isTurn="isTurn ?? false"
></AnimatedBackground>
<ClientOnly>
<template v-if="game && focusedPlayer && playerList">
<!-- <Transition name="slide-left" mode="out-in"> -->
<div id="stack_discard" :key="focusedPlayer?._id">
<div id="stack">
<GameCardGrouped
:container="focusedPlayer.stack"
:wrapped="true"
:hidden="true"
>
</GameCardGrouped>
</div>
<GameOverlayDiscard
:game="game"
:player="focusedPlayer"
></GameOverlayDiscard>
</div>
<!-- </Transition> -->
<GameMarket
:game="game"
:market="game.market"
:fire_gems="game.fireGems"
:marketStack="game.marketStack"
>
</GameMarket>
<div>
<GameToken
:token="t"
v-for="t in game.currentTurn.tokens"
@useToken="useTokenClick"
></GameToken>
</div>
<div id="player_list_container">
<div id="player_list">
<GamePlayerListElement
v-for="p in playerList"
:player="p"
:game="game"
@click="focusPlayer(p)"
:focused="focusedPlayer == p"
:is-self-turn="isTurn"
:self="selfPlayer == p"
>
</GamePlayerListElement>
</div>
</div>
<div id="current_player">
<Transition name="slide-left" mode="out-in">
<GamePlayerSlot
v-if="focusedPlayer"
:player="focusedPlayer"
:game="game"
:key="focusedPlayer._id"
></GamePlayerSlot>
</Transition>
</div>
<GameOverlayResultDialog
:isVisible="showResults"
:won="isWinner"
:players="playerList"
@close="showResults = false"
>
</GameOverlayResultDialog>
<GamePlayerSelf
v-if="selfPlayer"
:container="selfPlayer.hand"
></GamePlayerSelf>
</template>
</ClientOnly>
<!-- <LoadingScreen v-if="!loaded"></LoadingScreen> -->
<footer>
<GameOverlayOptionsDialog></GameOverlayOptionsDialog>
<ClientOnly>
<div id="turn_buttons" v-if="game">
<template v-if="game.started">
<button
id="end_turn"
class="end_turn_button turn_icon"
:class="{ hidden: !isTurn }"
@click="endTurn(game._id)"
v-if="isTurn"
>
END TURN
</button>
</template>
<template v-else-if="authStore.logged">
<button
class="end_turn_button turn_icon"
@click="startGame(game._id)"
v-if="game.owner.userId == authStore.selfUser.userId"
>
Start game
</button>
<button
class="end_turn_button turn_icon"
@click="joinGame(game._id)"
v-else-if="
playerList?.every(
(p) => p.user.userId != authStore.selfUser.userId
)
"
>
Join game
</button>
<button v-else>Leave game</button>
</template>
</div>
</ClientOnly>
<button @click="navigateTo('/lobby')">Return to lobby</button>
</footer>
</div>
</template>
<style scoped>
footer {
margin: 10px;
display: flex;
flex-direction: row;
grid-area: footer;
height: 50px;
/* background: red; */
justify-content: space-between;
}
#game_status {
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.75);
}
#game_board {
display: grid;
grid-template-columns: 1fr min-content 1fr;
grid-template-rows: min-content min-content 1fr min-content;
grid-template-areas:
"stack_discard market turn_stats"
"player_list player_list player_list"
"current_player current_player current_player"
"footer footer footer";
height: 100%;
}
#player_list {
display: flex;
flex-direction: inherit;
justify-content: center;
}
#current_player {
grid-area: current_player;
display: flex;
flex-direction: column;
justify-content: center;
overflow-x: clip;
/* background: url('/img/bg-darkgrass.png'); */
}
#stack_discard {
grid-area: stack_discard;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
#stack,
#discard {
display: flex;
flex-direction: row;
}
#discard {
cursor: pointer;
}
.discard_overlay {
z-index: 9;
display: flex;
/* background: blue; */
pointer-events: none;
gap: 2px;
flex-wrap: wrap-reverse;
align-content: flex-end;
cursor: default;
position: absolute;
padding-bottom: inherit;
padding-left: 40px;
height: calc(100% - 64px);
padding-top: 64px;
width: calc(100% -40px);
margin-left: 0;
}
#discard_unwrapped_bg {
display: none;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 8;
background: rgba(0, 0, 0, 0.5);
pointer-events: all;
padding: inherit;
}
#discard_unwrapped_bg.unwrapped {
display: block;
}
#market {
grid-area: market;
display: flex;
flex-direction: row;
justify-content: center;
}
#player_list_container {
grid-area: player_list;
display: flex;
flex-direction: row;
justify-content: center;
/* background-image: url("/img/bg-darkgrass.png"); */
}
#player_list {
/* border: 5px solid #000; */
display: flex;
}
.slide-left-enter-active,
.slide-left-leave-active {
transition: all v-bind("settingsStore.animationSpeed * 2 + 'ms'") ease-out;
}
.slide-left-enter-from {
opacity: 0;
transform: translateX(100%);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
}
</style>
-38
View File
@@ -1,38 +0,0 @@
<script setup lang="ts">
import goStore, { setReady } from "@/netcode";
</script>
<template>
<div class="root_page_div">
<div id="game_status">
<template v-if="goStore.self && !goStore.current_game?.started">
<input @click.prevent="setReady(($event.target as HTMLInputElement).checked)" type="checkbox" id="ready"
:checked="goStore.self?.ready" />
<label for="ready">Ready</label>
</template>
</div>
<LoginView v-if="goStore.context === 'login'"></LoginView>
<GameBoard v-if="goStore.context != 'lobby' && goStore.context != 'login'"></GameBoard>
<LobbyView v-if="goStore.context == 'lobby'"></LobbyView>
<RulesDisplay v-if="goStore.context == 'rules'"></RulesDisplay>
</div>
</template>
<style>
.root_page_div {
height: 100%;
}
#game_status {
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, .75);
}
</style>
+304
View File
@@ -0,0 +1,304 @@
<script setup lang="ts">
// import { playSound } from '~/helpers/audioHelpers';
import { playSound } from "~/helpers/audioHelpers";
import { createGame, subscribe } from "~/netcode";
import type { Game, Player, User } from "~/netcode/interfaces";
const authStore = useAuthStore();
let eventSource: EventSource;
const config = useRuntimeConfig();
const req = await useFetch<Array<Game>>(config.public.endpoint + "/games", {
credentials: "include",
server: false,
});
let games = req.data;
const erika_alive = ref(true);
const router = useRouter();
function killErika() {
playSound("/sounds/splat1.mp3");
erika_alive.value = false;
}
onBeforeMount(async () => {
eventSource = subscribe("/games/subscribe");
eventSource.addEventListener("message", (message) => {
const data = JSON.parse(message.data);
if (data.type == "create") {
games.value?.push(data);
} else if (data.type == "delete") {
const index = games.value?.findIndex((g) => g._id == data._id);
if (index == -1 || !index) {
throw new Error("Couldn't delete game");
}
games.value?.splice(index, 1);
} else if (data.type == "joinGame") {
games.value?.find((g) => g._id == data.game)?.teams.push(data.team);
} else if (data.type == "start") {
const targetGame = games.value?.find((g) => g._id == data.game);
if (targetGame) {
targetGame.started = true;
}
}
});
});
onUnmounted(() => {
eventSource.close();
});
</script>
<template>
<div class="background">
<header>
<ClientOnly>
<GamePlayerIcon
v-if="authStore.logged"
:user="(authStore.selfUser as User)"
/>
<button v-else @click="navigateTo('/login')">Login</button>
</ClientOnly>
</header>
<main>
<div class="title">Entrez dans le Royaume de Héron !</div>
<ClientOnly>
<template v-if="authStore.logged">
<div class="create-game-zone">
<button
@click="createGame"
v-if="
!games?.some(
(g) => g.owner.userId === authStore.selfUser.userId
)
"
>
Create Game
</button>
</div>
</template>
<div class="games-list-zone">
<div v-for="g in games">
<button
v-if="g"
@click="router.push({ path: `/game`, query: { id: g._id } })"
:class="
authStore.selfUser.userId == g.owner.userId ? 'own_game' : ''
"
>
<template
v-if="
g.teams.every((t) =>
t.players.every(
(p) => p.user.userId !== authStore.selfUser.userId
)
)
"
>
SPECTATE
</template>
<template v-else>
<template v-if="!g.started"> JOIN BACK </template>
<template v-else> CONTINUE </template>
</template>
<br />
{{
g.teams.reduce(
(prev, curr) => prev.concat(curr.players),
[] as Player[]
).length
}}
current players:<br /><span>{{
g.teams
.reduce(
(prev, curr) => prev.concat(curr.players),
[] as Player[]
)
.map((p) => p.user.username)
.join(",")
}}</span>
</button>
</div>
</div>
</ClientOnly>
</main>
<div class="overlay" v-if="erika_alive && !config.public.devMode">
<div class="erika-zone">
<img
@click="router.push('/rules')"
class="overlay-image"
src="https://static.wikia.nocookie.net/umineko/images/9/9d/PC_Erika.Casual.Sprite_36.png"
alt="pétasse"
/>
<div class="overlay-button">
<button class="kill-erika-button" @click.stop="killErika()">
Tuer Erika
</button>
</div>
</div>
<div class="speech-bubble" @click.prevent.self>
<p>
Ohoho!<br />Je suis Erika Furudo, la détective la plus forte du monde
!
</p>
<p>Je vais vous montrer comment jouer à Héron Realms !</p>
<p>
Cliquez moi-dessus pour afficher les
<span style="color: red">règles</span>.
</p>
<p>
Ou sinon, commencez direct en créant une partie ou rejoingnant une
partie existante.
</p>
</div>
</div>
</div>
</template>
<style scoped>
.background {
background-color: #f0f0f0;
background-image: url("https://www.pockettactics.com/wp-content/sites/pockettactics/2023/01/fire-emblem-engage-review-29.jpg");
background-size: cover;
display: grid;
grid-template-rows: 100px 1fr;
grid-template-columns: 1fr;
align-items: center;
height: 100vh;
width: 100vw;
}
.title {
font-family: "Kanit", "Roboto", sans-serif;
font-size: 64px;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
main {
display: flex;
flex-direction: column;
gap: 100px;
margin-bottom: 100px;
}
.create-game-zone,
header {
display: flex;
align-items: center;
align-content: center;
justify-content: flex-start;
gap: 10px;
}
header {
flex-direction: row;
backdrop-filter: blur(50px);
height: 100%;
}
.create-game-zone {
flex-direction: column;
}
.create-game-zone button {
background-color: #4caf50;
}
.games-list-zone {
display: flex;
flex-direction: column;
gap: 20px;
}
.games-list-zone button {
background-color: rgba(0, 0, 0, 0.8);
padding: 20px 24px;
height: 120px;
}
.games-list-zone button.own_game {
border: solid yellow 5px;
}
.overlay {
height: 100%;
width: 100%;
position: absolute;
pointer-events: none;
}
.erika-zone {
position: absolute;
top: 60%;
display: flex;
justify-content: right;
z-index: 2;
height: 40%;
width: 100%;
object-fit: cover;
}
.overlay-image {
position: absolute;
height: 100%;
cursor: pointer;
pointer-events: all;
}
.overlay-button {
position: absolute;
z-index: 1;
width: 300px;
box-sizing: border-box;
pointer-events: none;
}
.kill-erika-button {
display: none;
pointer-events: all;
background-color: #f44336;
border: none;
color: white;
text-align: center;
text-decoration: none;
font-family: "Roboto", sans-serif;
font-size: 16px;
margin: auto;
cursor: pointer;
border-radius: 12px;
padding: 10px 24px;
height: 50px;
transition-duration: 0.4s;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.speech-bubble {
position: absolute;
top: 60%;
right: 260px;
z-index: 1;
background-color: rgba(225, 225, 225, 0.8);
padding: 10px;
border-radius: 10px;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
font-family: "Kanit", "Roboto", sans-serif;
font-size: 16px;
color: black;
text-align: center;
max-width: 420px;
line-height: 1.5;
}
.erika-zone:hover .overlay-button .kill-erika-button {
display: inline-block;
}
</style>
+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>
+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.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More