init
30
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
29
README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# wilg-game
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Game</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
jsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
1372
package-lock.json
generated
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "wilg-game",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.12.1",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"vite": "^5.2.8"
|
||||
}
|
||||
}
|
||||
5
postcss.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/audio/fish.wav
Normal file
BIN
public/audio/move.wav
Normal file
BIN
public/back/32.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
public/back/33.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
public/back/40.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/back/41.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/back/42.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/back/45.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
public/back/47.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
public/back/50.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/grass/1.png
Normal file
|
After Width: | Height: | Size: 979 B |
BIN
public/images/grass/2.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/grass/3.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/header.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/images/header_border.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/logo.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
public/images/margin.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
public/images/shore.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/source/cloud/cloud_shape1_1.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/images/source/cloud/cloud_shape1_2.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/source/cloud/cloud_shape1_3.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/source/cloud/cloud_shape1_4.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/source/cloud/cloud_shape1_5.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
public/images/source/cloud/cloud_shape2_1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/source/cloud/cloud_shape2_2.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/source/cloud/cloud_shape2_3.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/source/cloud/cloud_shape2_4.png
Normal file
|
After Width: | Height: | Size: 793 B |
BIN
public/images/source/cloud/cloud_shape2_5.png
Normal file
|
After Width: | Height: | Size: 437 B |
BIN
public/images/source/cloud/cloud_shape3_1.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/images/source/cloud/cloud_shape3_2.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/source/cloud/cloud_shape3_3.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/source/cloud/cloud_shape3_4.png
Normal file
|
After Width: | Height: | Size: 953 B |
BIN
public/images/source/cloud/cloud_shape3_5.png
Normal file
|
After Width: | Height: | Size: 651 B |
BIN
public/images/source/cloud/cloud_shape4_1.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/images/source/cloud/cloud_shape4_2.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/source/cloud/cloud_shape4_3.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/source/cloud/cloud_shape4_4.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/source/cloud/cloud_shape4_5.png
Normal file
|
After Width: | Height: | Size: 657 B |
BIN
public/images/source/cloud/cloud_shape5_1.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
public/images/source/cloud/cloud_shape5_2.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/images/source/cloud/cloud_shape5_3.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/source/cloud/cloud_shape5_4.png
Normal file
|
After Width: | Height: | Size: 869 B |
BIN
public/images/source/cloud/cloud_shape5_5.png
Normal file
|
After Width: | Height: | Size: 570 B |
BIN
public/images/source/cloud/cloud_shape6_1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/source/cloud/cloud_shape6_2.png
Normal file
|
After Width: | Height: | Size: 909 B |
BIN
public/images/source/cloud/cloud_shape6_3.png
Normal file
|
After Width: | Height: | Size: 523 B |
BIN
public/images/source/cloud/cloud_shape6_4.png
Normal file
|
After Width: | Height: | Size: 352 B |
BIN
public/images/source/cloud/cloud_shape6_5.png
Normal file
|
After Width: | Height: | Size: 253 B |
BIN
public/images/source/fish/1.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
public/images/source/fish/10.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
public/images/source/fish/11.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
public/images/source/fish/12.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
public/images/source/fish/13.png
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
public/images/source/fish/14.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
public/images/source/fish/15.png
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
public/images/source/fish/16.png
Normal file
|
After Width: | Height: | Size: 336 KiB |
BIN
public/images/source/fish/17.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
public/images/source/fish/18.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
public/images/source/fish/19.png
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
public/images/source/fish/2.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
public/images/source/fish/20.png
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
public/images/source/fish/21.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
public/images/source/fish/22.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
public/images/source/fish/23.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
public/images/source/fish/4.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
public/images/source/fish/5.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
public/images/source/fish/6.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
public/images/source/fish/7.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
public/images/source/fish/8.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
public/images/source/fish/9.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
public/images/source/sprites.xcf
Normal file
BIN
public/images/sprites.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
public/images/water/1.png
Normal file
|
After Width: | Height: | Size: 821 B |
BIN
public/images/water/2.png
Normal file
|
After Width: | Height: | Size: 818 B |
BIN
public/images/water/3.png
Normal file
|
After Width: | Height: | Size: 755 B |
206
src/App.vue
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import router from "@/router";
|
||||
import { useApplicationStore } from './stores/application';
|
||||
import { storeToRefs } from "pinia";
|
||||
import RangeSliderComponent from '@/components/common/RangeSliderComponent.vue'
|
||||
import { usePlayerStore } from './stores/player';
|
||||
import i18n from '@/i18n/i18n'
|
||||
const application = useApplicationStore()
|
||||
const player = usePlayerStore()
|
||||
const { settings } = storeToRefs(application)
|
||||
|
||||
const changeLanguage = (lang) => {
|
||||
application.language = lang
|
||||
i18n.global.locale.value = application.language
|
||||
}
|
||||
const saveUsername = (event) => {
|
||||
if (event.target.value.length > 0) player.username = event.target.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="header-wrapper">
|
||||
<button class="header-button " @click="application.openSettings()">{{ $t('button.settings') }}</button>
|
||||
<button class="header-button " @click="application.openMenu()">{{ $t('button.menu') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings" v-if="application.showSettings">
|
||||
<button class="button-close" @click="application.closeSettings()">{{ $t('button.close') }}</button>
|
||||
<div class="settings-group">
|
||||
<div class="settings-group-title">{{ $t('settings.sound.title') }}</div>
|
||||
<div class="mute-wrapper">
|
||||
<div class="mute-title">{{ $t('settings.sound.mute') }}</div>
|
||||
<input type="checkbox" v-model="settings.sound.muted">
|
||||
</div>
|
||||
<RangeSliderComponent v-model="settings.sound.master" :name="$t('settings.sound.master')" />
|
||||
<RangeSliderComponent v-model="settings.sound.music" :name="$t('settings.sound.music')" />
|
||||
<RangeSliderComponent v-model="settings.sound.game" :name="$t('settings.sound.effects')" />
|
||||
<RangeSliderComponent v-model="settings.sound.ambient" :name="$t('settings.sound.ambient')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu" v-if="application.showMenu">
|
||||
<button class="button-close" @click="application.closeMenu()">{{ $t('button.close') }}</button>
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-title">{{ $t('title.player') }}</div>
|
||||
<input class="input-menu-username" type="text" @change="saveUsername($event)" :value="player.username" />
|
||||
<div class="menu-player-id">{{ player.id }}</div>
|
||||
</div>
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-title">{{ $t('title.game') }}</div>
|
||||
<button class="menu-button" @click="router.replace({ name: 'index' }); application.closeMenu()">{{
|
||||
$t('button.mainScreen') }}</button>
|
||||
<button class="menu-button">{{ $t('button.instruction') }}</button>
|
||||
<button class="menu-button">{{ $t('button.rules') }}</button>
|
||||
</div>
|
||||
<div class="menu-group-bottom">
|
||||
<div class="menu-group-title">{{ $t('title.language') }}</div>
|
||||
<button class="menu-button-language" @click="changeLanguage('en')">EN</button>
|
||||
<button class="menu-button-language" @click="changeLanguage('ru')">RU</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-overlay" v-if="application.showMenu || application.showSettings"></div>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
position: sticky;
|
||||
background-image: url('/public/images/header.png');
|
||||
background-color: #72c37a;
|
||||
}
|
||||
|
||||
.header-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem 0 1rem;
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
margin-top: 10px;
|
||||
height: 42px;
|
||||
min-width: 130px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-overlay {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
z-index: 98;
|
||||
top:0;
|
||||
right: 0;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
overflow-x: hidden;
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.menu-group-bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.menu-group-title {
|
||||
font-size: x-large;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.menu-button-language {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-button-language+.menu-button-language {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.menu-player-id {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: smaller;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.settings {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
padding: 2rem;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.settings-group-title {
|
||||
font-size: x-large;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mute-wrapper {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 20% 70% 10%;
|
||||
align-content: baseline;
|
||||
justify-content: center;
|
||||
padding: 0 2rem 1rem 2rem;
|
||||
}
|
||||
|
||||
|
||||
.button-close {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-menu-username {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: black;
|
||||
|
||||
}
|
||||
|
||||
.input-menu-username &:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
</style>
|
||||
61
src/assets/animation.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
@keyframes water {
|
||||
0% {
|
||||
background-image: url('/public/images/water/1.png');
|
||||
}
|
||||
|
||||
25% {
|
||||
background-image: url('/public/images/water/2.png');
|
||||
}
|
||||
|
||||
50% {
|
||||
background-image: url('/public/images/water/3.png');
|
||||
}
|
||||
|
||||
75% {
|
||||
background-image: url('/public/images/water/2.png');
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes empty {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes caught {
|
||||
0% {
|
||||
-webkit-transform: scale(0) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1) rotate(-360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hit {
|
||||
0% {
|
||||
background-color: rgba(0, 102, 143, 1);
|
||||
border-radius: 50%;
|
||||
height: 30%;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.lake-cell {
|
||||
animation: water 2s infinite;
|
||||
}
|
||||
106
src/assets/item.css
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/* FISH */
|
||||
.lobby {
|
||||
[class*="fish-"]::after {
|
||||
opacity: 1;
|
||||
}
|
||||
.margin::after {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
[class*="fish-"]::after, .margin::after, .empty-empty::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
scale: 0.9;
|
||||
opacity: 0.2;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 80%;
|
||||
background-position: center;
|
||||
|
||||
}
|
||||
.hit[class*="fish-"]::after {
|
||||
animation: caught 0.5s;
|
||||
opacity: 1;
|
||||
}
|
||||
.hit::before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
animation: hit 0.5s;
|
||||
}
|
||||
.fish-1::after {
|
||||
background: url('/public/images/sprites.png') 0 0;
|
||||
}
|
||||
.fish-2::after {
|
||||
background: url('/public/images/sprites.png') -48px 0;
|
||||
}
|
||||
.fish-3::after {
|
||||
background: url('/public/images/sprites.png') -96px 0;
|
||||
}
|
||||
.fish-4::after {
|
||||
background: url('/public/images/sprites.png') -144px 0;
|
||||
}
|
||||
.fish-5::after {
|
||||
background: url('/public/images/sprites.png') -192px 0;
|
||||
}
|
||||
.fish-6::after {
|
||||
background: url('/public/images/sprites.png') -240px 0;
|
||||
}
|
||||
.fish-7::after {
|
||||
background: url('/public/images/sprites.png') -288px 0;
|
||||
}
|
||||
.fish-8::after {
|
||||
background: url('/public/images/sprites.png') -336px 0;
|
||||
}
|
||||
.fish-9::after {
|
||||
background: url('/public/images/sprites.png') -384px 0;
|
||||
}
|
||||
.fish-10::after {
|
||||
background: url('/public/images/sprites.png') -432px 0;
|
||||
}
|
||||
.fish-11::after {
|
||||
background: url('/public/images/sprites.png') 0 -48px;
|
||||
}
|
||||
.fish-12::after {
|
||||
background: url('/public/images/sprites.png') -48px -48px;
|
||||
}
|
||||
.fish-13::after {
|
||||
background: url('/public/images/sprites.png') -96px -48px;
|
||||
}
|
||||
.fish-14::after {
|
||||
background: url('/public/images/sprites.png') -144px -48px;
|
||||
}
|
||||
.fish-15::after {
|
||||
background: url('/public/images/sprites.png') -192px -48px;
|
||||
}
|
||||
.fish-16::after {
|
||||
background: url('/public/images/sprites.png') -240px -48px;
|
||||
}
|
||||
.fish-17::after {
|
||||
background: url('/public/images/sprites.png') -288px -48px;
|
||||
}
|
||||
.fish-18::after {
|
||||
background: url('/public/images/sprites.png') -336px -48px;
|
||||
}
|
||||
.fish-19::after {
|
||||
background: url('/public/images/sprites.png') -384px -48px;
|
||||
}
|
||||
.fish-20::after {
|
||||
background: url('/public/images/sprites.png') -432px -48px;
|
||||
}
|
||||
.fish-21::after {
|
||||
background: url('/public/images/sprites.png') 0 -96px;
|
||||
}
|
||||
.fish-22::after {
|
||||
background: url('/public/images/sprites.png') -48px -96px;
|
||||
}
|
||||
|
||||
.margin::after{
|
||||
background: url('/public/images/sprites.png') -432px -432px;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.empty-empty::after {
|
||||
background: url('/public/images/sprites.png') -432px -432px;
|
||||
opacity: 0.8;
|
||||
animation: empty 0.5s;
|
||||
}
|
||||
242
src/assets/main.css
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
@import url('./animation.css');
|
||||
@import url('./item.css');
|
||||
|
||||
|
||||
:root {
|
||||
--cloud-color: rgba(255, 255, 255);
|
||||
--shadow-color: rgba(0, 0, 0);
|
||||
--water-color: rgba(111, 181, 213);
|
||||
--water-deep-color: rgba(0, 74, 230);
|
||||
--water-deep-border-color: rgba(0, 74, 230, 0.2);
|
||||
--wood-color: rgba(147, 117, 96);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ShantellSansItalic';
|
||||
src: url('./fonts/ShantellSans-Italic-VariableFont_BNCE,INFM,SPAC,wght.ttf') format("truetype-variations");
|
||||
font-weight: 300 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ShantellSans';
|
||||
src: url('./fonts/ShantellSans-VariableFont_BNCE,INFM,SPAC,wght.ttf') format("truetype-variations");
|
||||
font-weight: 300 800;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overscroll-behavior: none;
|
||||
font-family: 'ShantellSans';
|
||||
font-weight: 400;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
background: url('/public/images/grass/1.png') 48px 48px,
|
||||
url('/public/images/grass/2.png') 48px 48px,
|
||||
url('/public/images/grass/3.png') 48px 48px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: xx-large;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--cloud-color);
|
||||
text-shadow: .5rem .5rem 1rem var(--shadow-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: xx-large;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--cloud-color);
|
||||
text-shadow: .5rem .5rem 1rem var(--shadow-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
button {
|
||||
background-color: var(--wood-color);
|
||||
font-size: 1.5rem;
|
||||
color: var(--cloud-color);
|
||||
padding: 0.1rem 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.button-lobby {
|
||||
margin-bottom: 0;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.button-lobby + .button-lobby {
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.game, .lobby {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: 100px auto;
|
||||
grid-auto-rows: 0;
|
||||
height: calc(100% - 120px);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.player-view, .opponent-view { width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--water-color);
|
||||
}
|
||||
.lake-border {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
border: 18px solid transparent;
|
||||
border-image: url('/public/images/shore.png') 18 / 18px repeat;
|
||||
}
|
||||
.lake {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
justify-content: center;
|
||||
background-color: var(--water-color);
|
||||
}
|
||||
.lake-title {
|
||||
text-align: center;
|
||||
}
|
||||
.lake-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.lake-cell {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
cursor: pointer;
|
||||
background-image: url('/public/images/water/1.png');
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
position: relative;
|
||||
color: rgba(0, 102, 143, 0.3);
|
||||
}
|
||||
.lake-row + .lake-row {
|
||||
border-top: 1px var(--water-deep-border-color) solid;
|
||||
}
|
||||
.lake-cell + .lake-cell {
|
||||
border-left: 1px var(--water-deep-border-color) solid;
|
||||
}
|
||||
|
||||
|
||||
.hide {
|
||||
height: 0;
|
||||
animation: hide 2s;
|
||||
}
|
||||
|
||||
.player-view.active {
|
||||
animation: active 2s;
|
||||
}
|
||||
.player-view.hide {
|
||||
height: 0;
|
||||
animation: hide 2s;
|
||||
}
|
||||
|
||||
.opponent-view.active {
|
||||
animation: active-opponent 2s;
|
||||
}
|
||||
.opponent-view.hide {
|
||||
height: 0;
|
||||
animation: hide-opponent 2s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@keyframes hide {
|
||||
0% {
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
}
|
||||
50% {
|
||||
|
||||
height: 0;
|
||||
}
|
||||
100% {
|
||||
|
||||
height: 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes active {
|
||||
0% {
|
||||
|
||||
height: 0;
|
||||
|
||||
}
|
||||
50% {
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
100% {
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes hide-opponent {
|
||||
0% {
|
||||
/* border-top: 2rem #fff solid;*/
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
}
|
||||
50% {
|
||||
|
||||
height: 0;
|
||||
}
|
||||
100% {
|
||||
|
||||
height: 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes active-opponent {
|
||||
0% {
|
||||
/* border-top: 2rem #fff solid;*/
|
||||
height: 0;
|
||||
|
||||
}
|
||||
50% {
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
100% {
|
||||
max-width: calc(16px + 10 * 50px);
|
||||
width: 100%;
|
||||
max-height: calc(25px + 10 * 50px);
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
363
src/assets/preflight.css
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box; /* 1 */
|
||||
border-width: 0; /* 2 */
|
||||
border-style: solid; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
7. Disable tap highlights on iOS
|
||||
*/
|
||||
|
||||
html,
|
||||
:host {
|
||||
line-height: 1.5; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-moz-tab-size: 4; /* 3 */
|
||||
tab-size: 4; /* 3 */
|
||||
/* font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */
|
||||
/* font-feature-settings: theme('fontFamily.sans[1].fontFeatureSettings', normal); /* 5 */
|
||||
/* font-variation-settings: theme('fontFamily.sans[1].fontVariationSettings', normal); /* 6 */
|
||||
-webkit-tap-highlight-color: transparent; /* 7 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0; /* 1 */
|
||||
line-height: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
border-top-width: 1px; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0; /* 1 */
|
||||
border-color: inherit; /* 2 */
|
||||
border-collapse: collapse; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-feature-settings: inherit; /* 1 */
|
||||
font-variation-settings: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
font-weight: inherit; /* 1 */
|
||||
line-height: inherit; /* 1 */
|
||||
letter-spacing: inherit; /* 1 */
|
||||
color: inherit; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
padding: 0; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
input:where([type='button']),
|
||||
input:where([type='reset']),
|
||||
input:where([type='submit']) {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
background-color: transparent; /* 2 */
|
||||
background-image: none; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset default styling for dialogs.
|
||||
*/
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1; /* 1 */
|
||||
color: #9ca3af; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block; /* 1 */
|
||||
vertical-align: middle; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
27
src/classes/audioPlayer.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const gameSound = {
|
||||
move: new Audio('/audio/move.wav'),
|
||||
fish: new Audio('/audio/fish.wav'),
|
||||
}
|
||||
|
||||
export default class AudioPlayer {
|
||||
|
||||
constructor(settings) {
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
async gameAsync(name) {
|
||||
if (!navigator.userActivation.isActive) return
|
||||
return new Promise(ended=>{
|
||||
let sound = this.game(name)
|
||||
sound.onended = ended
|
||||
})
|
||||
}
|
||||
|
||||
game(name) {
|
||||
if (!navigator.userActivation.isActive) return
|
||||
gameSound[name].play()
|
||||
gameSound[name].muted = this.settings.muted
|
||||
gameSound[name].volume = (this.settings.game * this.settings.master) / 10000
|
||||
return gameSound[name]
|
||||
}
|
||||
}
|
||||
185
src/classes/boardConstructor.js
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
export default class boardConstructor {
|
||||
constructor(boardSize = 10, availableItems) {
|
||||
this.boardSize = boardSize;
|
||||
this.availableItems = availableItems
|
||||
}
|
||||
|
||||
generateItems() {
|
||||
let items = {};
|
||||
let itemsCount = 4;
|
||||
let curentSize = 1;
|
||||
for (let i = itemsCount; i > 0; i--) {
|
||||
let key = this.__generateItemId();
|
||||
items[key] = {
|
||||
id: key,
|
||||
size: curentSize,
|
||||
type: "fish",
|
||||
name: this.availableItems.pop(Math.floor(Math.random() * this.availableItems.length) + 1),
|
||||
orientation: "h",
|
||||
margin: 1,
|
||||
cells: [],
|
||||
marginCells: [],
|
||||
};
|
||||
if (i <= 1) {
|
||||
i = itemsCount;
|
||||
itemsCount--;
|
||||
curentSize++;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
generateBoard(items) {
|
||||
Object.keys(items).map((id) => {
|
||||
items[id].cells = [];
|
||||
items[id].marginCells = [];
|
||||
});
|
||||
let itemlist = Object.values(items);
|
||||
itemlist.sort((a, b) => b.size - a.size);
|
||||
while (itemlist.length > 0) {
|
||||
let currentItem = { ...itemlist.shift() };
|
||||
currentItem.orientation = Math.random() < 0.5 ? "h" : "v";
|
||||
while (!this.isItemAlreadyOnBoard(items[currentItem.id])) {
|
||||
let targetCell = Math.floor(
|
||||
Math.random() * this.boardSize * this.boardSize
|
||||
);
|
||||
currentItem.cells = this.getItemCells(targetCell, currentItem);
|
||||
currentItem.marginCells = this.getItemMarginCells(currentItem);
|
||||
if (
|
||||
this.isItemHasMarginCollision(currentItem, items) ||
|
||||
this.isItemHasCellsCollision(currentItem, items)
|
||||
)
|
||||
continue;
|
||||
items[currentItem.id] = currentItem;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
getItemCells(targetCell, item) {
|
||||
let cells = [];
|
||||
let cellRow = Math.floor(targetCell / this.boardSize);
|
||||
for (let i = 0; i <= item.size - 1; i++) {
|
||||
if (item.orientation == "h") {
|
||||
let currentCell = targetCell + i;
|
||||
if (
|
||||
currentCell <=
|
||||
cellRow * this.boardSize + (this.boardSize - 1)
|
||||
) {
|
||||
cells.push(currentCell);
|
||||
} else {
|
||||
cells.push(
|
||||
targetCell +
|
||||
(cellRow * this.boardSize +
|
||||
(this.boardSize - 1) -
|
||||
currentCell)
|
||||
);
|
||||
}
|
||||
} else if (item.orientation == "v") {
|
||||
let currentCell = targetCell + this.boardSize * i;
|
||||
if (currentCell < this.boardSize * this.boardSize) {
|
||||
cells.push(currentCell);
|
||||
} else {
|
||||
cells.push(targetCell - this.boardSize * (item.size - i));
|
||||
}
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
getItemMarginCells(item) {
|
||||
if (item.margin <= 0 || item.cells.length <= 0) return [];
|
||||
let marginCells = item.cells.map((cell) => {
|
||||
return this.__getCellMargin(cell, item.margin);
|
||||
});
|
||||
return marginCells
|
||||
.flat(1)
|
||||
.filter((cell) => item.cells.indexOf(cell) < 0);
|
||||
}
|
||||
|
||||
findItemIdInBoardCell(targetCell, items) {
|
||||
let item = Object.keys(items).filter((id) => {
|
||||
return items[id].cells.includes(targetCell);
|
||||
});
|
||||
return item[0];
|
||||
}
|
||||
|
||||
isItemAlreadyOnBoard(item) {
|
||||
return Object.values(item.cells).length > 0;
|
||||
}
|
||||
|
||||
isItemHasMarginCollision(item, items) {
|
||||
if (item.cells?.length <= 0 && item.cells.marginCells?.length <= 0)
|
||||
return false;
|
||||
return (
|
||||
Object.keys(items).filter((id) => {
|
||||
let collisionWithBoardItem =
|
||||
items[id].marginCells.filter((itemMarginCell) =>
|
||||
item.cells.includes(itemMarginCell)
|
||||
).length > 0;
|
||||
let collisionWithCurrentItem =
|
||||
items[id].cells.filter((itemCell) =>
|
||||
item.marginCells.includes(itemCell)
|
||||
).length > 0;
|
||||
return collisionWithBoardItem || collisionWithCurrentItem;
|
||||
})?.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
isItemHasCellsCollision(item, items) {
|
||||
if (item.cells?.length <= 0) return false;
|
||||
return (
|
||||
Object.keys(items).filter((id) => {
|
||||
return (
|
||||
items[id].cells.filter((itemCell) =>
|
||||
item.cells.includes(itemCell)
|
||||
).length > 0
|
||||
);
|
||||
})?.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
isAllItemsReady(items) {
|
||||
return (
|
||||
Object.keys(items).filter((id) => {
|
||||
return items[id].cells?.length <= 0;
|
||||
})?.length <= 0
|
||||
);
|
||||
}
|
||||
|
||||
__getCellMargin(cell, margin = 0) {
|
||||
let cellMargin = [];
|
||||
let cellRow = Math.floor(cell / this.boardSize);
|
||||
let maxCells = this.boardSize * this.boardSize - 1;
|
||||
let calcCell = null;
|
||||
for (
|
||||
let rowMargin = margin;
|
||||
rowMargin >= -Math.abs(margin);
|
||||
rowMargin--
|
||||
) {
|
||||
let currentRow = cellRow + rowMargin;
|
||||
let rowOffset = this.boardSize * rowMargin;
|
||||
let rowMin = currentRow * this.boardSize;
|
||||
let rowMax = rowMin + this.boardSize - 1;
|
||||
for (
|
||||
let colMargin = margin;
|
||||
colMargin >= -Math.abs(margin);
|
||||
colMargin--
|
||||
) {
|
||||
calcCell = cell + rowOffset + colMargin;
|
||||
if (
|
||||
calcCell >= 0 &&
|
||||
calcCell <= maxCells &&
|
||||
calcCell >= rowMin &&
|
||||
calcCell <= rowMax
|
||||
)
|
||||
cellMargin.push(calcCell);
|
||||
}
|
||||
}
|
||||
return cellMargin;
|
||||
}
|
||||
|
||||
__generateItemId() {
|
||||
return Math.random().toString(36).replace("0.", "");
|
||||
}
|
||||
}
|
||||
39
src/classes/scrollable.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
export default class Scrollable {
|
||||
canScrolling = false;
|
||||
x = 0;
|
||||
y = 0;
|
||||
constructor(elemet) {
|
||||
this.elemet = elemet;
|
||||
}
|
||||
|
||||
start(event) {
|
||||
if (event.changedTouches != undefined) {
|
||||
this.x = event.changedTouches[0].screenX;
|
||||
this.y = event.changedTouches[0].screenY;
|
||||
} else {
|
||||
this.x = event.screenX;
|
||||
this.y = event.screenY;
|
||||
}
|
||||
this.canScrolling = true;
|
||||
}
|
||||
move(event) {
|
||||
if (this.canScrolling == false) return;
|
||||
let x =
|
||||
event.changedTouches != undefined
|
||||
? event.changedTouches[0].screenX
|
||||
: event.screenX;
|
||||
let y =
|
||||
event.changedTouches != undefined
|
||||
? event.changedTouches[0].screenY
|
||||
: event.screenY;
|
||||
this.elemet.value.scrollLeft += this.x - x;
|
||||
this.elemet.value.scrollTop += this.y - y;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
end() {
|
||||
this.canScrolling = false;
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
130
src/components/common/RangeSliderComponent.vue
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
min: {
|
||||
default: 0,
|
||||
type: Number
|
||||
},
|
||||
max: {
|
||||
default: 100,
|
||||
type: Number
|
||||
},
|
||||
step: {
|
||||
default: 5,
|
||||
type: Number
|
||||
},
|
||||
})
|
||||
|
||||
const modelValue = defineModel()
|
||||
|
||||
onMounted(() => {
|
||||
value.value = modelValue.value
|
||||
draw(value.value)
|
||||
})
|
||||
|
||||
|
||||
const range = ref()
|
||||
const selector = ref()
|
||||
|
||||
const isDragging = ref(false)
|
||||
const value = ref(0)
|
||||
|
||||
const getValue = (event) => {
|
||||
if (isDragging.value == false) return value.value
|
||||
let rect = range.value.getBoundingClientRect()
|
||||
let x = 0
|
||||
if (event.changedTouches == undefined) {
|
||||
x = event.clientX
|
||||
} else {
|
||||
x = event.changedTouches[0].clientX
|
||||
}
|
||||
let countedValue = props.min;
|
||||
countedValue = x - rect.left;
|
||||
countedValue = countedValue / rect.width;
|
||||
countedValue = countedValue * props.max;
|
||||
countedValue = Math.round(countedValue / props.step) * props.step;
|
||||
countedValue = countedValue / (props.max - props.min);
|
||||
countedValue = Math.round(countedValue * 100);
|
||||
if (countedValue > props.max) countedValue = props.max;
|
||||
if (countedValue < props.min) countedValue = props.min;
|
||||
draw(countedValue)
|
||||
return countedValue
|
||||
}
|
||||
const draw = (countedValue) => {
|
||||
countedValue = countedValue * (props.max - props.min);
|
||||
countedValue = Math.round(countedValue / props.max);
|
||||
countedValue = props.max - countedValue;
|
||||
if (countedValue > 100) countedValue = 100;
|
||||
if (countedValue < 0) countedValue = 0;
|
||||
selector.value.style.width = (100 - countedValue) + '%'
|
||||
}
|
||||
const start = (event) => {
|
||||
isDragging.value = true
|
||||
}
|
||||
const select = (event) => {
|
||||
let v = getValue(event)
|
||||
value.value = v
|
||||
}
|
||||
const submit = (event) => {
|
||||
select(event);
|
||||
modelValue.value = value.value
|
||||
isDragging.value = false
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="range">
|
||||
<div class="range-label">{{ props.name }}</div>
|
||||
<div class="range-wrapper">
|
||||
<div class="range-slider" ref="range" @mousedown="start($event)" @mousemove="select($event)"
|
||||
@mouseup="submit($event)" @touchstart="start($event)" @touchmove="select($event)"
|
||||
@touchend="submit($event)">
|
||||
<div class="range-slider-completed" ref="selector"></div>
|
||||
<div class="range-value">{{ value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.range-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-content: baseline;
|
||||
justify-content: center;
|
||||
padding: 0 2rem 1rem 2rem;
|
||||
}
|
||||
.range-label {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: large;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.range-value {
|
||||
font-size: large;
|
||||
font-weight: 600;
|
||||
}
|
||||
.range-slider {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background-color: rgba(127, 165, 209, 0.349);
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.range-slider-completed {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: inherit;
|
||||
transition: 0.1s;
|
||||
}
|
||||
</style>
|
||||
20
src/i18n/i18n.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createI18n } from 'vue-i18n'
|
||||
import pluralRules from "./rules/pluralization.js"
|
||||
import numberFormats from "./rules/numbers.js"
|
||||
import datetimeFormats from "./rules/datetime.js"
|
||||
import en from './locales/en.json'
|
||||
import ru from './locales/ru.json'
|
||||
|
||||
const i18n = createI18n({
|
||||
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
messages: {
|
||||
en,
|
||||
ru
|
||||
},
|
||||
pluralRules,
|
||||
numberFormats,
|
||||
datetimeFormats
|
||||
})
|
||||
export default i18n
|
||||
44
src/i18n/locales/en.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"application": {
|
||||
"name": "Fishing"
|
||||
},
|
||||
"button": {
|
||||
"exit": "Exit",
|
||||
"close": "Close",
|
||||
"newGame": "New Game",
|
||||
"settings": "Settings",
|
||||
"menu": "Menu",
|
||||
"singlePlayer": "Single Player",
|
||||
"multiPlayer": "Multi Player",
|
||||
"generate": "Release the fish",
|
||||
"ready": "Ready",
|
||||
"cancel": "Cancel",
|
||||
"mainScreen": "Go to main screen",
|
||||
"instruction": "How to play",
|
||||
"rules": "Game rules"
|
||||
},
|
||||
"settings": {
|
||||
"sound": {
|
||||
"title": "Sound",
|
||||
"mute": "Mute",
|
||||
"master": "Master",
|
||||
"effects": "Effects",
|
||||
"music": "Music",
|
||||
"ambient": "Ambient"
|
||||
}
|
||||
},
|
||||
"game": {
|
||||
"yourTurn": "Your turn",
|
||||
"opponentTurn": "Opponent`s turn",
|
||||
"over": "Game Over",
|
||||
"win": "You win!",
|
||||
"loose": "You loose!"
|
||||
},
|
||||
"title": {
|
||||
"game": "Game",
|
||||
"language": "Language",
|
||||
"player": "Player"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
42
src/i18n/locales/ru.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"application": {
|
||||
"name": "Рыбалка"
|
||||
},
|
||||
"button": {
|
||||
"exit": "Выйти",
|
||||
"close": "Закрыть",
|
||||
"newGame": "Новая игра",
|
||||
"settings": "Настройки",
|
||||
"menu": "Меню",
|
||||
"singlePlayer": "Одиночная игра",
|
||||
"multiPlayer": "Играть с другом",
|
||||
"generate": "Запустить рыбу",
|
||||
"ready": "Готов",
|
||||
"cancel": "Отмена",
|
||||
"mainScreen": "На главный экран",
|
||||
"instruction": "Как играть",
|
||||
"rules": "Правила"
|
||||
},
|
||||
"settings": {
|
||||
"sound": {
|
||||
"title": "Звук",
|
||||
"mute": "Выключить",
|
||||
"master": "Общая",
|
||||
"effects": "Эффекты",
|
||||
"music": "Музыка",
|
||||
"ambient": "Окружение"
|
||||
}
|
||||
},
|
||||
"game": {
|
||||
"yourTurn": "Ваш ход",
|
||||
"opponentTurn": "Ход соперника",
|
||||
"over": "Игра окончена",
|
||||
"win": "Вы выиграли!",
|
||||
"loose": "Вы проиграли!"
|
||||
},
|
||||
"title": {
|
||||
"game": "Игра",
|
||||
"language": "Язык",
|
||||
"player": "Игрок"
|
||||
}
|
||||
}
|
||||
20
src/i18n/rules/datetime.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export default {
|
||||
en: {
|
||||
shortFormat: {
|
||||
dateStyle: "short"
|
||||
},
|
||||
longFormat: {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
weekday: 'short', hour: 'numeric', minute: 'numeric'
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
shortFormat: {
|
||||
dateStyle: "short"
|
||||
},
|
||||
longFormat: {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
weekday: 'short', hour: 'numeric', minute: 'numeric'
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/i18n/rules/numbers.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export default {
|
||||
en: {
|
||||
currencyFormat: {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
currencyFormat: {
|
||||
style: "currency",
|
||||
currency: "RUB"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/i18n/rules/pluralization.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
function ruPluralizationRules(
|
||||
choice,
|
||||
choicesLength
|
||||
) {
|
||||
if (choice === 0) {
|
||||
return 0
|
||||
}
|
||||
const teen = choice > 10 && choice < 20
|
||||
const endsWithOne = choice % 10 === 1
|
||||
if (!teen && endsWithOne) {
|
||||
return 1
|
||||
}
|
||||
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
||||
return 2
|
||||
}
|
||||
return choicesLength < 4 ? 2 : 3
|
||||
}
|
||||
export default {
|
||||
ru: ruPluralizationRules
|
||||
};
|
||||