mirror of
https://github.com/Aviortheking/games.git
synced 2025-04-24 11:52:09 +00:00
feat: move to Astro and mostly reworked the Pokémon Shuffle game
Signed-off-by: Avior <github@avior.me>
This commit is contained in:
parent
4bb1f17467
commit
c42311eaae
22
.dockerignore
Normal file
22
.dockerignore
Normal file
@ -0,0 +1,22 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
slicers/*
|
||||
|
||||
# Coverage
|
||||
coverage/
|
@ -6,8 +6,9 @@ indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
|
||||
[*.md]
|
||||
# Yaml Standard
|
||||
[*.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = false
|
||||
|
@ -1,8 +0,0 @@
|
||||
node_modules
|
||||
|
||||
out
|
||||
.next
|
||||
|
||||
next-env.d.ts
|
||||
*.js
|
||||
__tests__
|
191
.eslintrc.js
191
.eslintrc.js
@ -1,191 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
SharedArrayBuffer: "readonly"
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module"
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect"
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
rules: {
|
||||
indent: [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
|
||||
quotes: "off",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{ avoidEscape: true }
|
||||
],
|
||||
|
||||
semi: "off",
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
|
||||
"no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }],
|
||||
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
"@typescript-eslint/array-type": [
|
||||
"error",
|
||||
{
|
||||
default: 'generic'
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{
|
||||
accessibility: "explicit"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"error",
|
||||
{
|
||||
multiline: {
|
||||
delimiter: "none",
|
||||
requireLast: true
|
||||
},
|
||||
singleline: {
|
||||
delimiter: "comma",
|
||||
requireLast: false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/member-ordering": "error",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/prefer-for-of": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
|
||||
"@typescript-eslint/triple-slash-reference": "error",
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unified-signatures": "error",
|
||||
"arrow-body-style": "error",
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
|
||||
complexity: "off",
|
||||
"constructor-super": "error",
|
||||
curly: "error",
|
||||
"dot-notation": "error",
|
||||
"eol-last": "error",
|
||||
eqeqeq: [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"guard-for-in": "error",
|
||||
"id-blacklist": [
|
||||
"error",
|
||||
"any",
|
||||
"Number",
|
||||
"number",
|
||||
"String",
|
||||
"string",
|
||||
"Boolean",
|
||||
"boolean",
|
||||
"Undefined"
|
||||
],
|
||||
"id-match": "error",
|
||||
"max-classes-per-file": [
|
||||
"error",
|
||||
1
|
||||
],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
code: 200
|
||||
}
|
||||
],
|
||||
"new-parens": "error",
|
||||
"no-bitwise": "error",
|
||||
"no-caller": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-debugger": "error",
|
||||
"no-empty": "error",
|
||||
"no-eval": "error",
|
||||
"no-fallthrough": "off",
|
||||
"no-invalid-this": "off",
|
||||
"no-multiple-empty-lines": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-shadow": [
|
||||
"error",
|
||||
{
|
||||
hoist: "all"
|
||||
}
|
||||
],
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-underscore-dangle": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-var": "error",
|
||||
"object-shorthand": "error",
|
||||
"one-var": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"prefer-const": "error",
|
||||
"quote-props": [
|
||||
"error",
|
||||
"consistent-as-needed"
|
||||
],
|
||||
"radix": "error",
|
||||
|
||||
"space-before-function-paren": "off",
|
||||
"@typescript-eslint/space-before-function-paren": ["error", {
|
||||
asyncArrow: "always",
|
||||
anonymous: "never",
|
||||
named: "never"
|
||||
}],
|
||||
"spaced-comment": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "off",
|
||||
}
|
||||
};
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
35
.github/workflows/build_and_check.yml
vendored
Normal file
35
.github/workflows/build_and_check.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: Build, check & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Check
|
||||
run: npm run check
|
||||
|
||||
- name: Prepare Tests
|
||||
run: npm run install:test
|
||||
|
||||
- name: Test
|
||||
run : npm run test
|
42
.gitignore
vendored
42
.gitignore
vendored
@ -1,29 +1,29 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
node_modules/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env*
|
||||
|
||||
# debug
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
dist/
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
*~
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
slicers/*
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Playwright
|
||||
/playwright/
|
||||
|
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -1,3 +1,8 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"astro": "html"
|
||||
}
|
||||
}
|
||||
|
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@ -0,0 +1,53 @@
|
||||
# This Dockerfile allows you to run AstroJS in server mode
|
||||
|
||||
#########
|
||||
# Build #
|
||||
#########
|
||||
FROM docker.io/node:20-alpine as BUILD_IMAGE
|
||||
|
||||
# run as non root user
|
||||
USER node
|
||||
|
||||
# go to user repository
|
||||
WORKDIR /home/node
|
||||
|
||||
# Add package json
|
||||
ADD --chown=node:node package.json package-lock.json ./
|
||||
|
||||
# install dependencies from package lock
|
||||
RUN npm ci
|
||||
|
||||
# Add project files
|
||||
ADD --chown=node:node . .
|
||||
|
||||
# build
|
||||
RUN npm run build
|
||||
|
||||
# remove dev deps
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
##############
|
||||
# Production #
|
||||
##############
|
||||
FROM docker.io/node:20-alpine as PROD_IMAGE
|
||||
|
||||
# inform software to be in production
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
# run as non root user
|
||||
USER node
|
||||
|
||||
# go to work folder
|
||||
WORKDIR /home/node
|
||||
|
||||
# copy from build image
|
||||
COPY --chown=node:node --from=BUILD_IMAGE /home/node/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=BUILD_IMAGE /home/node/dist ./dist
|
||||
COPY --chown=node:node --from=BUILD_IMAGE /home/node/package.json /home/node/.env* ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# run it !
|
||||
CMD ["npm", "run", "start"]
|
38
Dockerfile.static
Normal file
38
Dockerfile.static
Normal file
@ -0,0 +1,38 @@
|
||||
# This Dockerfile allows you to run Astro in a static container (no server side)
|
||||
|
||||
#########
|
||||
# Build #
|
||||
#########
|
||||
FROM docker.io/node:20-alpine as BUILD_IMAGE
|
||||
|
||||
# run as non root user
|
||||
USER node
|
||||
|
||||
# go to user repository
|
||||
WORKDIR /home/node
|
||||
|
||||
# Add package json
|
||||
ADD --chown=node:node package.json package-lock.json ./
|
||||
|
||||
# install dependencies from package lock
|
||||
RUN npm ci
|
||||
|
||||
# Add project files
|
||||
ADD --chown=node:node . .
|
||||
|
||||
# build
|
||||
RUN npm run build
|
||||
|
||||
##############
|
||||
# Production #
|
||||
##############
|
||||
FROM docker.io/nginx:1-alpine
|
||||
|
||||
# go to NGINX folder
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# Copy the nginx config
|
||||
ADD ./.docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy dist fro mthe build image
|
||||
COPY --from=BUILD_IMAGE /home/node/dist ./
|
37
README.md
37
README.md
@ -1 +1,36 @@
|
||||
# Avior Games
|
||||
# Fi3D Slicer as a Service
|
||||
|
||||
## API key
|
||||
|
||||
add `Authorization: Bearer {token}`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| endpoint | method | permission required | cookie access | api access | Description |
|
||||
| :----------------------------: | :----: | :-----------------: | :-----------: | :--------: | :---------------------------------------: |
|
||||
| /api/v1/users | GET | user.list | no | no | List every user accounts |
|
||||
| /api/v1/users | POST | user.create | no | no | Create a new account |
|
||||
| /api/v1/users/{userId} | GET | user.get | yes | yes | Get a user's informations |
|
||||
| /api/v1/users/{userId} | PUT | user.set | yes | yes | Set a user's informations |
|
||||
| /api/v1/users/{userId}/configs | GET | configs.get | yes | yes | get the list of the user's configurations |
|
||||
| /api/v1/users/{userId}/configs | POST | configs.create | yes | yes | Add a new configuration to the user |
|
||||
| /api/v1/users/{userId}/keys | GET | keys.get | yes | no | get the list of API key for the user |
|
||||
| /api/v1/users/{userId}/keys | POST | keys.create | yes | no | create a new API key for the user |
|
||||
| /api/v1/slice/{configId} | POST | slice.slice | yes | yes | run the main website action |
|
||||
|
||||
endpoints not available through API can still be accessed by admins with the `admin.` prefix to the permission
|
||||
|
||||
## API Key Permissions
|
||||
|
||||
### `slicing:*` permissions related to the slicing
|
||||
|
||||
| name | Description |
|
||||
| :-----------: | :---------------------: |
|
||||
| slicing:slice | Slice the specified STL |
|
||||
|
||||
### `configs:*` permissions related to the configuration files
|
||||
|
||||
| name | description |
|
||||
| :------------: | :--------------------------------: |
|
||||
| configs:create | Create a new configuration file |
|
||||
| configs:get | Get an existing configuration file |
|
||||
|
46
astro.config.mjs
Normal file
46
astro.config.mjs
Normal file
@ -0,0 +1,46 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
import tailwind from "@astrojs/tailwind"
|
||||
import node from "@astrojs/node"
|
||||
|
||||
// const faviconHook = {
|
||||
// name: 'Favicon',
|
||||
// hooks: {
|
||||
// "astro:build:setup": async () => {
|
||||
// await Manifest.create('./src/assets/favicon.png', {
|
||||
// name: 'Template'
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind()],
|
||||
compressHTML: true,
|
||||
build: {
|
||||
assets: 'assets',
|
||||
inlineStylesheets: 'auto'
|
||||
},
|
||||
server: {
|
||||
host: true
|
||||
},
|
||||
trailingSlash: 'never',
|
||||
vite: {
|
||||
server: {
|
||||
watch: {
|
||||
// support WSL strange things
|
||||
usePolling: !!process.env.WSL_DISTRO_NAME
|
||||
}
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
assets: true
|
||||
},
|
||||
|
||||
// Customizable depending on goal
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: "standalone"
|
||||
}),
|
||||
site: 'https://print.dzeio.com',
|
||||
})
|
@ -1,68 +0,0 @@
|
||||
@startuml classes
|
||||
!theme toy
|
||||
|
||||
class GameEngine {
|
||||
- scenes: Record<string, Scene>
|
||||
|
||||
+ addScene(scene: Scene): void
|
||||
+ setScene(id: string): void
|
||||
+ removeScene(scene: string | Scene): void
|
||||
+ start(): void
|
||||
+ pause(): void
|
||||
+ destroy(): void
|
||||
}
|
||||
|
||||
class Scene {
|
||||
+ id: string
|
||||
|
||||
+ setCamera(id: String): void
|
||||
+ addComponents(components: ...Component2D): void
|
||||
+ removeComponents(components: ...(Component2D | string)): void
|
||||
}
|
||||
|
||||
GameEngine "1" -- "0-*" Scene : "contains"
|
||||
|
||||
class Component {
|
||||
+ id: string
|
||||
+ position: Vector2D
|
||||
+ renderer: Renderer
|
||||
+ position: Vector2D
|
||||
+ scale: Vector2D
|
||||
+ collider: Collider
|
||||
+ origin: Vector2D
|
||||
+ childs: Array<Component2D>
|
||||
+ debug: boolean
|
||||
|
||||
+ init(): void
|
||||
+ update(): void
|
||||
}
|
||||
|
||||
Scene "1" -- "0-*" Component : "contains"
|
||||
|
||||
class Camera extends Component {
|
||||
|
||||
}
|
||||
|
||||
class Pointer extends Component {}
|
||||
|
||||
class Asset {}
|
||||
|
||||
class Sound {}
|
||||
|
||||
class Tileset {}
|
||||
|
||||
Component -- Component
|
||||
Tileset -- Asset
|
||||
|
||||
class Vector2D {
|
||||
+ x: number
|
||||
+ y: number
|
||||
}
|
||||
|
||||
class Collider {}
|
||||
|
||||
Component "1" -- "0-1" Collider
|
||||
|
||||
Component "1" -- "0-*" Vector2D
|
||||
|
||||
@enduml
|
@ -1,117 +0,0 @@
|
||||
// V1.1.2 - Moved webpack5 to top level
|
||||
// Updated to commit from 2021-05-18
|
||||
// https://github.com/vercel/next.js/commits/canary/packages/next/next-server/server/config-shared.ts
|
||||
/**
|
||||
* @type {import("next/dist/next-server/server/config-shared").NextConfig & import("next/dist/next-server/server/config-shared").defaultConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
// Experimentals
|
||||
experimental: {
|
||||
plugins: true,
|
||||
profiling: process.env.NODE_ENV === 'developpment',
|
||||
sprFlushToDisk: true,
|
||||
workerThreads: true,
|
||||
|
||||
pageEnv: true,
|
||||
optimizeImages: true,
|
||||
optimizeCss: true,
|
||||
|
||||
scrollRestoration: true,
|
||||
scriptLoader: true,
|
||||
stats: process.env.NODE_ENV === 'developpment',
|
||||
externalDir: true,
|
||||
|
||||
serialWebpackBuild: true,
|
||||
|
||||
conformance: true,
|
||||
|
||||
turboMode: true,
|
||||
eslint: true,
|
||||
// Bugged
|
||||
// https://github.com/vercel/next.js/issues/18913
|
||||
// reactRoot: true,
|
||||
enableBlurryPlaceholder: true,
|
||||
disableOptimizedLoading: false,
|
||||
},
|
||||
|
||||
// Non experimental config
|
||||
target: 'server',
|
||||
poweredByHeader: false,
|
||||
trailingSlash: false,
|
||||
optimizeFonts: true,
|
||||
reactStrictMode: true,
|
||||
webpack5: true,
|
||||
|
||||
// Futures
|
||||
future: {
|
||||
strictPostcssConfiguration: true,
|
||||
excludeDefaultMomentLocales: true
|
||||
},
|
||||
|
||||
// Headers and rewrites
|
||||
async headers() {
|
||||
// CSS no CSP, x-xss-protection
|
||||
const CSP = {
|
||||
key: 'Content-Security-Policy',
|
||||
value:
|
||||
// default-src is set to self because prefetch-src is not working propelly see: https://bugs.chromium.org/p/chromium/issues/detail?id=801561
|
||||
"default-src 'self'; " +
|
||||
"frame-ancestors 'none'; " +
|
||||
"form-action 'self'; " +
|
||||
"manifest-src 'self'; " +
|
||||
"prefetch-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://proxy.dzeio.com; " +
|
||||
"style-src 'self' 'unsafe-inline'; " +
|
||||
"img-src data: 'self'; " +
|
||||
"font-src 'self'; " +
|
||||
"connect-src 'self' https://proxy.dzeio.com; " +
|
||||
"base-uri 'self';"
|
||||
}
|
||||
const XXssProtection = {
|
||||
key: 'X-XSS-Protection',
|
||||
value: '1; mode=block'
|
||||
}
|
||||
// JS no x-xss-protection
|
||||
|
||||
const headers = [{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY'
|
||||
}, {
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff'
|
||||
}, {
|
||||
key: 'Referrer-Policy',
|
||||
value: 'strict-origin-when-cross-origin'
|
||||
}, {
|
||||
key: 'Permissions-Policy',
|
||||
value: 'geolocation=(), microphone=(), interest-cohort=()'
|
||||
}, {
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload'
|
||||
}, {
|
||||
key: 'X-Download-Options',
|
||||
value: 'noopen'
|
||||
}, {
|
||||
key: 'Expect-CT',
|
||||
value: 'max-age=86400, enforce'
|
||||
}]
|
||||
const excludedExtensions = ['js', 'css', 'json', 'ico', 'png']
|
||||
.map((ext) => `(?!\\.${ext}$)`).join('|')
|
||||
return [{
|
||||
source: `/:path*((?!^\\/_next\\/image)|${excludedExtensions})`,
|
||||
headers: [...headers, XXssProtection, CSP]
|
||||
}, {
|
||||
source: '/',
|
||||
headers: [...headers, XXssProtection, CSP]
|
||||
}, {
|
||||
// No CSP, XXssProtection
|
||||
source: `/:path*(\\.${excludedExtensions}$)`,
|
||||
headers: headers
|
||||
}, {
|
||||
// No CSP, XXssProtection
|
||||
source: '/_next/image',
|
||||
headers: headers
|
||||
}]
|
||||
},
|
||||
}
|
||||
module.exports = nextConfig
|
5
e2e/README.md
Normal file
5
e2e/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# e2e
|
||||
|
||||
Hold End 2 End tests
|
||||
|
||||
currently WIP
|
8
e2e/example.spec.ts
Normal file
8
e2e/example.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Astro/);
|
||||
})
|
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
@ -1,23 +0,0 @@
|
||||
// Add support for Stylus/LESS
|
||||
const preCSS = require('next-pre-css')
|
||||
// Use Compose plugin for easier maintenance
|
||||
const withPlugins = require('next-compose-plugins')
|
||||
|
||||
const {PHASE_DEVELOPMENT_SERVER} = require('next/constants')
|
||||
const nextConfig = require('./dzeio.next.config')
|
||||
|
||||
module.exports = withPlugins([
|
||||
[preCSS, {
|
||||
cssModules: true,
|
||||
cssLoaderOptions: {
|
||||
localIdentName: "[hash:base64:6]",
|
||||
},
|
||||
[PHASE_DEVELOPMENT_SERVER]: {
|
||||
cssLoaderOptions: {
|
||||
localIdentName: "[path][name]__[local]"
|
||||
}
|
||||
}
|
||||
}]
|
||||
],
|
||||
nextConfig
|
||||
)
|
52
nginx.conf
Normal file
52
nginx.conf
Normal file
@ -0,0 +1,52 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 3000;
|
||||
listen [::]:3000;
|
||||
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /500.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html /$uri /index.html;
|
||||
}
|
||||
|
||||
# Plausible script
|
||||
location /js/index.js {
|
||||
# Change this if you use a different variant of the script
|
||||
proxy_pass https://plausible.io/js/script.js;
|
||||
proxy_set_header Host plausible.io;
|
||||
|
||||
# Tiny, negligible performance improvement. Very optional.
|
||||
proxy_buffering on;
|
||||
}
|
||||
|
||||
# Plausible script
|
||||
location /api/event {
|
||||
proxy_pass https://plausible.io/api/event;
|
||||
proxy_set_header Host plausible.io;
|
||||
proxy_buffering on;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
}
|
14959
package-lock.json
generated
14959
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@ -1,41 +1,36 @@
|
||||
{
|
||||
"name": "@avior/games",
|
||||
"version": "1.1.0",
|
||||
"name": "@fi3d/slicer-as-a-service",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"server": "next start",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"test": "jest --config jext.config.js"
|
||||
"dev": "astro dev",
|
||||
"start": "node ./dist/server/entry.mjs",
|
||||
"build": "astro build",
|
||||
"check": "npm run check:astro && npm run check:typescript",
|
||||
"check:astro": "astro check",
|
||||
"check:typescript": "tsc --noEmit",
|
||||
"test": "npm run test:unit && npm run test:e2e",
|
||||
"test:unit": "vitest --coverage --run",
|
||||
"test:e2e": "playwright test",
|
||||
"install:test": "playwright install --with-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dzeio/object-util": "^1.2.0",
|
||||
"critters": "^0.0.10",
|
||||
"easy-sitemap": "^1.0.0",
|
||||
"next": "^11.0.0",
|
||||
"next-compose-plugins": "^2.2.0",
|
||||
"next-plausible": "^1.6.1",
|
||||
"next-pre-css": "^1.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-feather": "^2.0.9",
|
||||
"stylus": "^0.54.7",
|
||||
"stylus-loader": "^6.0.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.1.3",
|
||||
"webpack": "^5.37.1"
|
||||
"@astrojs/node": "^5",
|
||||
"@astrojs/tailwind": "^4",
|
||||
"@dzeio/listener": "^1.0.3",
|
||||
"@dzeio/logger": "^3",
|
||||
"@dzeio/object-util": "^1",
|
||||
"@dzeio/url-manager": "^1",
|
||||
"astro": "^2",
|
||||
"easy-sitemap": "^1.1.2",
|
||||
"lucide-astro": "^0.262.0",
|
||||
"stylus": "^0.59.0",
|
||||
"tailwindcss": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@types/favicons": "^6.2.0",
|
||||
"@types/node": "^15.6.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.1",
|
||||
"@typescript-eslint/parser": "^4.14.1",
|
||||
"eslint": "^7.1.0",
|
||||
"eslint-config-next": "^10.2.2",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"ts-node": "^9.1.1"
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@types/node": "^20",
|
||||
"@vitest/coverage-v8": "^0.33.0",
|
||||
"vitest": "^0.33.0"
|
||||
}
|
||||
}
|
||||
|
62
playwright.config.ts
Normal file
62
playwright.config.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost:3000',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
outputDir: './playwright/results',
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined as any,
|
||||
reporter: process.env.CI ? 'list' : [['html', {
|
||||
outputFolder: './playwright/report',
|
||||
open: 'never'
|
||||
}]],
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
})
|
23
src/GameEngine/2D/Collider/BoxCollider2D.ts
Normal file
23
src/GameEngine/2D/Collider/BoxCollider2D.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/* eslint-disable max-len */
|
||||
import Collider from '.'
|
||||
import type Vector2D from '../Vector2D'
|
||||
|
||||
export default class BoxCollider2D extends Collider<{
|
||||
center?: Vector2D
|
||||
scale?: Vector2D
|
||||
}> {
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns [Vector2D, Vector2D, Vector2D, Vector2D] the four points of the box collider
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
public pos(): [Vector2D, Vector2D, Vector2D, Vector2D] {
|
||||
return [
|
||||
this.component.calculatePositionFor('topLeft', true),
|
||||
this.component.calculatePositionFor('topRight', true),
|
||||
this.component.calculatePositionFor('bottomLeft', true),
|
||||
this.component.calculatePositionFor('bottomRight', true),
|
||||
]
|
||||
}
|
||||
}
|
131
src/GameEngine/2D/Collider/Checker.ts
Normal file
131
src/GameEngine/2D/Collider/Checker.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import Collider from '.'
|
||||
import GameEngine from '../..'
|
||||
import ComponentRenderer from '../../Components/ComponentRenderer'
|
||||
import CircleRenderer from '../../Renderer/CircleRenderer'
|
||||
import MathUtils from '../../libs/MathUtils'
|
||||
import Vector2D from '../Vector2D'
|
||||
import BoxCollider2D from './BoxCollider2D'
|
||||
import Circlecollider2D from './CircleCollider2D'
|
||||
import PointCollider2D from './PointCollider2D'
|
||||
|
||||
export default class Checker {
|
||||
public static boxCircleCollision(box: BoxCollider2D, circle: Circlecollider2D): boolean {
|
||||
// clamp(value, min, max) - limits value to the range min..max
|
||||
|
||||
// Find the closest point to the circle within the rectangle
|
||||
const [topLeft, bottomRight] = box.pos()
|
||||
const center = circle.center()
|
||||
const radius = circle.radius()
|
||||
const closestX = MathUtils.clamp(center.x, topLeft.x, bottomRight.x)
|
||||
const closestY = MathUtils.clamp(center.y, topLeft.y, bottomRight.y)
|
||||
|
||||
// Calculate the distance between the circle's center and this closest point
|
||||
const distanceX = center.x - closestX
|
||||
const distanceY = center.y - closestY
|
||||
|
||||
// If the distance is less than the circle's radius, an intersection occurs
|
||||
const distanceSquared = distanceX * distanceX + distanceY * distanceY
|
||||
return distanceSquared < radius * radius
|
||||
}
|
||||
|
||||
public static circleCircleCollision(circle1: Circlecollider2D, circle2: Circlecollider2D) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* check if a point collider with a rectangle (can handle rotated rectangles)
|
||||
* @param pointCollider the point collider
|
||||
* @param boxCollider the box collider
|
||||
* @returns if the boxes collide
|
||||
*/
|
||||
public static pointBoxCollision(pointCollider: PointCollider2D, boxCollider: BoxCollider2D) {
|
||||
const point = pointCollider.pos()
|
||||
.rotate(boxCollider.component.getMiddle(true), -boxCollider.component.getAbsoluteRotation())
|
||||
const [topLeftRotated, , , bottomRightRotated ] = boxCollider.pos()
|
||||
const topLeft = topLeftRotated
|
||||
.rotate(boxCollider.component.getMiddle(true), -boxCollider.component.getAbsoluteRotation())
|
||||
const bottomRight = bottomRightRotated
|
||||
.rotate(boxCollider.component.getMiddle(true), -boxCollider.component.getAbsoluteRotation())
|
||||
|
||||
return point
|
||||
.isIn(topLeft, bottomRight)
|
||||
}
|
||||
|
||||
/**
|
||||
* dumb way to check currntly
|
||||
*
|
||||
* TODO: Handle rotation
|
||||
*/
|
||||
public static boxBoxCollision(box1: BoxCollider2D, box2: BoxCollider2D): boolean {
|
||||
return this.rectangleRectangleCollisionV2(box1, box2)
|
||||
}
|
||||
|
||||
public static posBoxBoxCollision(box1: [Vector2D, Vector2D], box2: [Vector2D, Vector2D]) {
|
||||
return box1[1].x > box2[0].x && // self bottom higher than other top
|
||||
box1[0].x < box2[1].x &&
|
||||
box1[1].y > box2[0].y &&
|
||||
box1[0].y < box2[1].y
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public static detectCollision(collider1: Collider, collider2: Collider, reverse = false): boolean {
|
||||
if (collider1 instanceof BoxCollider2D && collider2 instanceof Circlecollider2D) {
|
||||
return this.boxCircleCollision(collider1, collider2)
|
||||
} else if (collider1 instanceof Circlecollider2D && collider2 instanceof Circlecollider2D) {
|
||||
return this.circleCircleCollision(collider2, collider1)
|
||||
} else if (collider1 instanceof BoxCollider2D && collider2 instanceof BoxCollider2D) {
|
||||
return this.boxBoxCollision(collider2, collider1)
|
||||
} else if (collider1 instanceof BoxCollider2D && collider2 instanceof PointCollider2D) {
|
||||
return this.pointBoxCollision(collider2, collider1)
|
||||
}
|
||||
|
||||
if (!reverse) {
|
||||
return this.detectCollision(collider2, collider1, true)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public static rectangleRectangleCollisionV2(box1: BoxCollider2D, box2: BoxCollider2D) {
|
||||
const rect1 = box1.pos()
|
||||
const rect2 = box2.pos()
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const p1 = rect1[i]
|
||||
const p2 = rect1[(i + 1) % 4]
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const p3 = rect2[j]
|
||||
const p4 = rect2[(j + 1) % 4]
|
||||
|
||||
if (this.lineLineCollision([p1, p2], [p3, p4])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* I don't understand this shit (from god)
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
private static lineLineCollision([a, b]: [Vector2D, Vector2D], [c, d]: [Vector2D, Vector2D]) {
|
||||
const den = (d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y)
|
||||
const num1 = (d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)
|
||||
const num2 = (b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)
|
||||
|
||||
if (den === 0) {
|
||||
return num1 === 0 && num2 === 0
|
||||
}
|
||||
|
||||
const r = num1 / den
|
||||
const s = num2 / den
|
||||
|
||||
return r >= 0 && r <= 1 && s >= 0 && s <= 1
|
||||
}
|
||||
|
||||
|
||||
}
|
16
src/GameEngine/2D/Collider/CircleCollider2D.ts
Normal file
16
src/GameEngine/2D/Collider/CircleCollider2D.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import Collider from '.'
|
||||
import Vector2D from '../Vector2D'
|
||||
|
||||
export default class Circlecollider2D extends Collider<{
|
||||
radius?: number
|
||||
offset?: Vector2D
|
||||
}> {
|
||||
|
||||
public center(): Vector2D {
|
||||
return new Vector2D(0)
|
||||
}
|
||||
|
||||
public radius(): number {
|
||||
return this.params.radius ?? 0
|
||||
}
|
||||
}
|
16
src/GameEngine/2D/Collider/PointCollider2D.ts
Normal file
16
src/GameEngine/2D/Collider/PointCollider2D.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import Collider from '.'
|
||||
import Vector2D from '../Vector2D'
|
||||
|
||||
export default class PointCollider2D extends Collider<{
|
||||
center?: Vector2D
|
||||
scale?: Vector2D
|
||||
}> {
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Vector2D the position of the point
|
||||
*/
|
||||
public pos(): Vector2D {
|
||||
return this.component.getAbsolutePosition()
|
||||
}
|
||||
}
|
35
src/GameEngine/2D/Collider/index.ts
Normal file
35
src/GameEngine/2D/Collider/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type Component2D from '../../Component2D'
|
||||
|
||||
export interface BaseParams {
|
||||
tags?: string | null | Array<string> | undefined
|
||||
}
|
||||
|
||||
export default abstract class Collider<
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
T extends {} | void = {} | void
|
||||
> {
|
||||
/**
|
||||
* Colliders will only collide with othe that have the same type (undefined is a type)
|
||||
*
|
||||
* if type is null it will collide with everything
|
||||
*/
|
||||
public get tags() : string | null | Array<string> | undefined {
|
||||
return this.params.tags
|
||||
}
|
||||
|
||||
public set tags(value: string | null | Array<string> | undefined) {
|
||||
this.params.tags = value
|
||||
}
|
||||
|
||||
|
||||
public readonly params: T & BaseParams = {} as T & BaseParams
|
||||
|
||||
public constructor(
|
||||
public component: Component2D,
|
||||
it: T & BaseParams | void
|
||||
) {
|
||||
if (it) {
|
||||
this.params = it
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import GameEngine from '../..'
|
||||
import type Component2D from '../../Component2D'
|
||||
import Vector2D from '../Vector2D'
|
||||
|
||||
type BuiltinCollisionTypes = 'click' | 'pointerDown' | 'pointerUp'
|
||||
@ -22,7 +23,12 @@ export default class BoxCollider2D {
|
||||
|
||||
public pos(): [Vector2D, Vector2D] {
|
||||
const scale = this.scale.multiply(this.component.scale)
|
||||
const positionCenter = this.component.origin.sub(
|
||||
const positionCenter = GameEngine.getGameEngine().currentScene?.position.sum(this.component.origin.sub(
|
||||
new Vector2D(
|
||||
this.component.position.x,
|
||||
this.component.position.y
|
||||
)
|
||||
)) ?? this.component.origin.sub(
|
||||
new Vector2D(
|
||||
this.component.position.x,
|
||||
this.component.position.y
|
||||
@ -31,12 +37,22 @@ export default class BoxCollider2D {
|
||||
|
||||
const center = this.center.sum(positionCenter)
|
||||
return [new Vector2D(
|
||||
center.x - scale.x / 2,
|
||||
center.y - scale.y / 2
|
||||
center.x,
|
||||
center.y
|
||||
),
|
||||
new Vector2D(
|
||||
center.x + scale.x / 2,
|
||||
center.y + scale.y / 2
|
||||
center.x + scale.x,
|
||||
center.y + scale.y
|
||||
)]
|
||||
}
|
||||
|
||||
public collideWith(collider: BoxCollider2D) {
|
||||
const selfPos = this.pos()
|
||||
const otherPos = collider.pos()
|
||||
|
||||
return selfPos[1].x >= otherPos[0].x && // self bottom higher than other top
|
||||
selfPos[0].x <= otherPos[1].x &&
|
||||
selfPos[1].y >= otherPos[0].y &&
|
||||
selfPos[0].y <= otherPos[1].y
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,42 @@
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import BoxCollider2D from '../Collision/BoxCollider2D'
|
||||
import Component2D, { ComponentState } from '../../Component2D'
|
||||
import ComponentRenderer from '../../Components/ComponentRenderer'
|
||||
import RectRenderer from '../../Renderer/RectRenderer'
|
||||
import TextRenderer from '../../Renderer/TextRenderer'
|
||||
import Vector2D from '../Vector2D'
|
||||
|
||||
export default class ColliderDebugger extends Component2D {
|
||||
public constructor(component: Component2D, collider: BoxCollider2D) {
|
||||
super()
|
||||
this.collider = collider
|
||||
const [topLeft, bottomRight] = collider.pos()
|
||||
const size = topLeft.sub(bottomRight)
|
||||
this.position = topLeft
|
||||
this.scale = size
|
||||
this.origin = new Vector2D(-(this.scale.x / 2), -(this.scale.y / 2))
|
||||
this.renderer = new RectRenderer(this, {stroke: 'black'})
|
||||
// TODO: rework it
|
||||
export default class ColliderDebugger extends Component2D<{collision?: Array<string>}> {
|
||||
|
||||
public readonly name = 'ColliderDebugger'
|
||||
|
||||
public override renderer: RectRenderer = new RectRenderer(this, {stroke: 'transparent'})
|
||||
|
||||
private textRenderer!: TextRenderer
|
||||
|
||||
public override init() {
|
||||
if (!this.parent) {
|
||||
console.error('cant setup, no parent')
|
||||
return
|
||||
}
|
||||
this.collider = this.parent.collider
|
||||
this.position = new Vector2D(0)
|
||||
this.scale = this.parent.scale
|
||||
this.origin = this.parent.origin
|
||||
|
||||
const text = new ComponentRenderer()
|
||||
this.textRenderer = new TextRenderer(text, {
|
||||
color: 'black',
|
||||
size: 1
|
||||
})
|
||||
text.updateParam('renderer', this.textRenderer)
|
||||
this.childs.push(text)
|
||||
}
|
||||
|
||||
public update(state: ComponentState) {
|
||||
if (state.isColliding) {
|
||||
(this.renderer as RectRenderer).material = 'rgba(0, 255, 0, .7)'
|
||||
} else {
|
||||
(this.renderer as RectRenderer).material = undefined
|
||||
}
|
||||
public override update(state: ComponentState) {
|
||||
const len = state.collisions?.length ?? 0
|
||||
this.renderer.setProps({
|
||||
material: len === 0 ? null : `rgba(0, 255, 0, .${len})`
|
||||
})
|
||||
this.textRenderer.setProps({text: len.toString()})
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import Component2D from '../../Component2D'
|
||||
import Vector2D from '../Vector2D'
|
||||
import PointDebugger from './PointDebugger'
|
||||
|
||||
export default class ComponentDebug extends Component2D {
|
||||
public readonly name = 'ComponentDebug'
|
||||
public constructor(component: Component2D) {
|
||||
super()
|
||||
this.position = new Vector2D(0, 0)
|
||||
|
@ -1,14 +1,20 @@
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import Component2D from '../../Component2D'
|
||||
import RectRenderer from '../../Renderer/RectRenderer'
|
||||
import Vector2D from '../Vector2D'
|
||||
|
||||
export default class PointDebugger extends Component2D {
|
||||
public constructor(point: Vector2D, color = 'red') {
|
||||
super()
|
||||
this.scale = new Vector2D(1, 1)
|
||||
this.position = point
|
||||
interface Props {
|
||||
point: Vector2D
|
||||
color?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export default class PointDebugger extends Component2D<Props> {
|
||||
public override zIndex?: number = 900
|
||||
public override init() {
|
||||
this.scale = new Vector2D(this.params.size ?? 1)
|
||||
this.position = this.params.point
|
||||
// console.log('Debugging point at location', point)
|
||||
// this.origin = component.origin
|
||||
this.renderer = new RectRenderer(this, {material: color})
|
||||
this.renderer = new RectRenderer(this, {material: this.params.color ?? 'red'})
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import Component2D from '../../Component2D'
|
||||
import RectRenderer from '../../Renderer/RectRenderer'
|
||||
import Vector2D from '../Vector2D'
|
||||
|
||||
export default class TilingDebugger extends Component2D {
|
||||
public readonly name = 'TilingDebugger'
|
||||
public constructor() {
|
||||
super()
|
||||
for (let i0 = 0; i0 < 10; i0++) {
|
||||
@ -22,6 +23,7 @@ export default class TilingDebugger extends Component2D {
|
||||
}
|
||||
|
||||
class CaseDebugger extends Component2D {
|
||||
public readonly name = 'CaseDebugger'
|
||||
public renderer: RectRenderer = new RectRenderer(this, {stroke: 'black'})
|
||||
public constructor(pos: Vector2D) {
|
||||
super()
|
||||
|
@ -1,9 +1,82 @@
|
||||
export default class Vector2D {
|
||||
public constructor(
|
||||
public x: number,
|
||||
public y: number
|
||||
) {}
|
||||
import GameEngine from '..'
|
||||
import MathUtils from '../libs/MathUtils'
|
||||
|
||||
/* eslint-disable id-length */
|
||||
export default class Vector2D {
|
||||
|
||||
public x: number
|
||||
public y: number
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public constructor(
|
||||
x: number | [number, number] | Vector2D | {x: number, y: number},
|
||||
y?: number
|
||||
) {
|
||||
// init from a vector
|
||||
if (x instanceof Vector2D) {
|
||||
this.x = x.x
|
||||
this.y = x.y
|
||||
return
|
||||
}
|
||||
// init from an object with x & y as parameters
|
||||
if (typeof x === 'object' && 'x' in x && 'y' in x && typeof x.x === 'number' && typeof x.y === 'number') {
|
||||
this.x = x.x
|
||||
this.y = x.y
|
||||
return
|
||||
}
|
||||
|
||||
// init from an array
|
||||
if (Array.isArray(x) && x.length === 2 && typeof x[0] === 'number' && typeof x[1] === 'number') {
|
||||
this.x = x[0]
|
||||
this.y = x[1]
|
||||
return
|
||||
}
|
||||
|
||||
// handle x & y are numbers
|
||||
if (typeof x === 'number') {
|
||||
this.x = x
|
||||
if (typeof y === 'number') {
|
||||
this.y = y
|
||||
} else {
|
||||
this.y = x
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// no init available
|
||||
throw new Error(`can't create vector from input x: "${x}" (${typeof x}), y: "${y}" (${typeof y})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* return the coordinates from the browser to a GE one
|
||||
* @param clientX the x position from the client (excluding the X scroll)
|
||||
* @param clientY the y position from the client (excluding the Y scroll)
|
||||
* @returns the vector with the position in the game
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
public static fromBrowser(clientX: number, clientY: number) {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
if (!ge) {
|
||||
return new Vector2D(0)
|
||||
}
|
||||
|
||||
return new Vector2D(
|
||||
// get X position on browser
|
||||
((clientX ?? 0) + window.scrollX - ge.canvas.offsetLeft) /
|
||||
(ge.currentScene?.scale ?? 1) * ge.getXCaseCount() /
|
||||
ge.canvas.offsetWidth + (ge.currentScene?.position?.x ?? 0),
|
||||
// get Y position on browser
|
||||
((clientY ?? 0) + window.scrollY - ge.canvas.offsetTop) /
|
||||
(ge.currentScene?.scale ?? 1) * ge.getYCaseCount() /
|
||||
ge.canvas.offsetHeight + (ge.currentScene?.position?.y ?? 0)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* return a new vector multiplied with the current one
|
||||
* @param v vector
|
||||
* @returns a new vector
|
||||
*/
|
||||
public multiply(v: Vector2D): Vector2D {
|
||||
return new Vector2D(
|
||||
v.x * this.x,
|
||||
@ -11,17 +84,36 @@ export default class Vector2D {
|
||||
)
|
||||
}
|
||||
|
||||
public sum(v: Vector2D): Vector2D {
|
||||
/**
|
||||
* return a new vector summed with the current one
|
||||
* @param v vector or x to add
|
||||
* @param y y to add
|
||||
* @returns a new vector
|
||||
*/
|
||||
public sum(v: Vector2D | number, y?: number): Vector2D {
|
||||
if (typeof v === 'number') {
|
||||
return new Vector2D(this.x + v, this.y + (y ?? v))
|
||||
}
|
||||
return new Vector2D(
|
||||
v.x + this.x,
|
||||
v.y + this.y
|
||||
)
|
||||
}
|
||||
|
||||
public sub(v: Vector2D): Vector2D {
|
||||
public neg(): Vector2D {
|
||||
return new Vector2D(
|
||||
v.x - this.x,
|
||||
v.y - this.y
|
||||
-this.x,
|
||||
-this.y
|
||||
)
|
||||
}
|
||||
|
||||
public sub(x: Vector2D | number, y?: number): Vector2D {
|
||||
if (typeof x === 'number') {
|
||||
return new Vector2D(this.x - x, this.y - (y ?? x))
|
||||
}
|
||||
return new Vector2D(
|
||||
x.x - this.x,
|
||||
x.y - this.y
|
||||
)
|
||||
}
|
||||
|
||||
@ -46,8 +138,123 @@ export default class Vector2D {
|
||||
)
|
||||
}
|
||||
|
||||
public set(x: number, y: number) {
|
||||
/**
|
||||
* return a new Vector with the this minus the other vector/x,y
|
||||
*/
|
||||
public minus(x: Vector2D | number, y?: number) {
|
||||
return this.sum(
|
||||
typeof x === 'number' ? -x : -x.x,
|
||||
y? -y : typeof x === 'number' ? -x : -x.y,
|
||||
)
|
||||
}
|
||||
|
||||
public set(x: number | Vector2D, y?: number) {
|
||||
|
||||
if (typeof x === 'object') {
|
||||
this.x = x.x
|
||||
this.y = x.y
|
||||
return this
|
||||
}
|
||||
|
||||
this.x = x
|
||||
if (!y) {
|
||||
this.y = x
|
||||
} else {
|
||||
this.y = y
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public setY(y: number) {
|
||||
this.y = y
|
||||
return this
|
||||
}
|
||||
|
||||
public setX(x: number) {
|
||||
this.x = x
|
||||
return this
|
||||
}
|
||||
|
||||
public clone() {
|
||||
return new Vector2D(this.x, this.y)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `${this.x}, ${this.y}`
|
||||
}
|
||||
|
||||
public toFixed(fractionDigits?: number) {
|
||||
return `${this.x.toFixed(fractionDigits)}, ${this.y.toFixed(fractionDigits)}`
|
||||
}
|
||||
|
||||
public equal(x: Vector2D | number, y?: number): boolean {
|
||||
let otherX = 0
|
||||
let otherY = y ?? 0
|
||||
if (x instanceof Vector2D) {
|
||||
otherX = x.x
|
||||
otherY = x.y
|
||||
} else {
|
||||
otherX = x
|
||||
if (!y) {
|
||||
otherY = x
|
||||
}
|
||||
}
|
||||
return otherX === this.x && otherY === this.y
|
||||
}
|
||||
|
||||
public toArray(): [number, number] {
|
||||
return [this.x, this.y]
|
||||
}
|
||||
|
||||
/**
|
||||
* return the angle to make [this] look at [point]
|
||||
* @param point the second point
|
||||
*
|
||||
* @returns the angle in degrees
|
||||
*/
|
||||
public angle(point: Vector2D): number {
|
||||
return Math.atan2(
|
||||
this.y - point.y,
|
||||
this.x - point.x
|
||||
) * 180 / Math.PI + 180
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public toBrowser(): [number, number] {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
const offsetLeft = ge.canvas.offsetLeft
|
||||
const offsetTop = ge.canvas.offsetTop
|
||||
|
||||
const canvasW = ge.canvas.offsetWidth
|
||||
const canvasH = ge.canvas.offsetHeight
|
||||
|
||||
const finalX = offsetLeft + canvasW * (this.x - ge.currentScene!.position.x) / ge.getXCaseCount() * ge.currentScene!.scale
|
||||
const finalY = offsetTop + canvasH * (this.y - ge.currentScene!.position.y) / ge.getYCaseCount() * ge.currentScene!.scale
|
||||
|
||||
return [
|
||||
finalX,
|
||||
finalY
|
||||
]
|
||||
}
|
||||
|
||||
public distance(b: Vector2D): Vector2D {
|
||||
return new Vector2D(
|
||||
this.x - b.x,
|
||||
this.y - b.y
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* rotate the vector by the {origin}
|
||||
* @param origin the point of rotation
|
||||
* @param degrees the number of degrees of rotation
|
||||
*/
|
||||
public rotate(origin: Vector2D, degrees: number): Vector2D {
|
||||
const radians = MathUtils.toRadians(degrees)
|
||||
const tmp = this.sum(-origin.x, -origin.y)
|
||||
return new Vector2D(
|
||||
origin.x + (tmp.x * Math.cos(radians) - tmp.y * Math.sin(radians)),
|
||||
origin.y + (tmp.x * Math.sin(radians) + tmp.y * Math.cos(radians))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,82 @@
|
||||
import Listener from '@dzeio/listener'
|
||||
import Vector2D from './2D/Vector2D'
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum AssetStatus {
|
||||
NOT_LOADED,
|
||||
LOADING,
|
||||
LOADED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset management Class
|
||||
*/
|
||||
export default class Asset {
|
||||
export default class Asset extends Listener<{
|
||||
loaded: (width: number, height: number) => void
|
||||
error: (error?: string) => void
|
||||
}> {
|
||||
|
||||
public static assets: Record<string, Asset> = {}
|
||||
|
||||
public isLoaded = false
|
||||
public status: AssetStatus = AssetStatus.NOT_LOADED
|
||||
|
||||
private image!: HTMLImageElement
|
||||
|
||||
private constructor(
|
||||
private path: string
|
||||
) {}
|
||||
public readonly path: string
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public static init(path: string) {
|
||||
public static init(path: string): Asset {
|
||||
if (!this.assets[path]) {
|
||||
this.assets[path] = new Asset(path)
|
||||
}
|
||||
return this.assets[path]
|
||||
return this.assets[path] as Asset
|
||||
}
|
||||
|
||||
public async load() {
|
||||
if (this.status === AssetStatus.LOADED || this.status === AssetStatus.LOADING) {
|
||||
return
|
||||
}
|
||||
this.status = AssetStatus.LOADING
|
||||
return new Promise<void>((res, rej) => {
|
||||
this.image = new Image()
|
||||
this.image.src = this.path
|
||||
this.image.onload = () => {
|
||||
// console.log('resource loaded', this.path, this.image.width, this.image.height)
|
||||
if (this.image.width === 0 && this.image.height === 0) {
|
||||
this.emit('error', 'sizeZero')
|
||||
throw new Error(`resource (${this.path}) not correctly loaded!, width && height at 0`)
|
||||
}
|
||||
this.isLoaded = true
|
||||
this.status = AssetStatus.LOADED
|
||||
this.emit('loaded', this.image.width, this.image.height)
|
||||
res()
|
||||
}
|
||||
this.image.onerror = rej
|
||||
this.image.onerror = () => {
|
||||
console.error('Error loading image')
|
||||
this.status = AssetStatus.ERROR
|
||||
this.emit('error', 'defaultError')
|
||||
rej(`resource (${this.path}) could not be loaded`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async get() {
|
||||
if (!this.isLoaded) {
|
||||
await this.load()
|
||||
public get() {
|
||||
if (this.status !== AssetStatus.LOADED) {
|
||||
throw new Error(`Can't get (${this.path}) because it is not loaded, please load it before`)
|
||||
}
|
||||
return this.image
|
||||
}
|
||||
|
||||
public size(): Vector2D {
|
||||
if (this.status !== AssetStatus.LOADED) {
|
||||
console.error(`Can't get (${this.path}) because it is not loaded, please load it before`)
|
||||
return new Vector2D(0)
|
||||
}
|
||||
return new Vector2D(this.image.width, this.image.height)
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,82 @@
|
||||
import BoxCollider2D from './2D/Collision/BoxCollider2D'
|
||||
/* eslint-disable complexity */
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
import GameEngine from '.'
|
||||
import Collider from './2D/Collider'
|
||||
import Vector2D from './2D/Vector2D'
|
||||
import Renderer from './Renderer'
|
||||
import Scene from './Scene'
|
||||
|
||||
/**
|
||||
* Component base Props
|
||||
*
|
||||
* it contains internal elements that manage how the component works
|
||||
*
|
||||
* it's elements are kept between frames
|
||||
*/
|
||||
export interface ComponentProps {
|
||||
// /**
|
||||
// * Component unique ID
|
||||
// */
|
||||
// id: number
|
||||
// /**
|
||||
// * Component Renderer
|
||||
// */
|
||||
// renderer?: Renderer
|
||||
zIndex?: number
|
||||
// position?: Vector2D
|
||||
// scale?: Vector2D
|
||||
// collider?: Collider | Array<Collider>
|
||||
// parent?: Component2D
|
||||
// origin?: Vector2D
|
||||
// childs?: Array<Component2D>
|
||||
// debug?: boolean
|
||||
// rotation?: number
|
||||
// enabled?: boolean
|
||||
|
||||
scene?: Scene
|
||||
initialized?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Component specific state
|
||||
* change for each frames
|
||||
*/
|
||||
export interface ComponentState {
|
||||
mouseHovering: boolean
|
||||
collisions?: Array<{
|
||||
collider: Collider
|
||||
component: Component2D
|
||||
tag: string | null | undefined
|
||||
}>
|
||||
|
||||
/**
|
||||
* is it is collinding return the type of collision
|
||||
* define if a collision check has been done
|
||||
*/
|
||||
isColliding?: string
|
||||
collisionChecked?: boolean
|
||||
collisionCheckNeeded?: boolean
|
||||
/**
|
||||
* Temporary state containing the previous collision check position
|
||||
*/
|
||||
previousPosition?: Vector2D
|
||||
}
|
||||
|
||||
/**
|
||||
* Component internal states definition
|
||||
*
|
||||
* TODO: Verify cache interest
|
||||
*/
|
||||
interface ComponentCache {
|
||||
absolutePosition?: {
|
||||
previousPosition?: Vector2D
|
||||
cacheResult?: Vector2D
|
||||
previousRotation?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type StaticComponent<
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
T extends {} | void = {} | void
|
||||
Props extends {} | void = {} | void,
|
||||
Component extends Component2D<Props> = Component2D<Props>
|
||||
> =
|
||||
new (params: T | undefined) => Component2D<T>
|
||||
new (props: Props | undefined) => Component
|
||||
|
||||
/**
|
||||
* 2D Component
|
||||
@ -24,7 +86,25 @@ export default abstract class Component2D<
|
||||
T extends {} | void = {} | void
|
||||
> {
|
||||
|
||||
public params: T = {} as T
|
||||
private static components = 0
|
||||
|
||||
/**
|
||||
* the component properties
|
||||
*/
|
||||
public readonly props: ComponentProps = {}
|
||||
|
||||
/**
|
||||
* Component specific state
|
||||
* change for each frames
|
||||
*/
|
||||
public readonly state: ComponentState = {}
|
||||
|
||||
/**
|
||||
* unique number for the component
|
||||
*
|
||||
* maximum number of components is equal to Number.MAX_VALUE or 1.7976931348623157e+308
|
||||
*/
|
||||
public id: number = Component2D.components++
|
||||
|
||||
/**
|
||||
* Indicate how the component is rendered
|
||||
@ -32,13 +112,22 @@ T extends {} | void = {} | void
|
||||
* @type {Renderer}
|
||||
* @memberof Component2D
|
||||
*/
|
||||
public renderer?: Renderer
|
||||
public renderer?: Renderer | null
|
||||
|
||||
/**
|
||||
* z Index to change the order of display
|
||||
*
|
||||
* (relative to parent if applicable)
|
||||
*/
|
||||
public zIndex?: number
|
||||
|
||||
/**
|
||||
* Component position relative to the parent position and to the component origin
|
||||
*
|
||||
* (see also: Component2D.origin)
|
||||
*
|
||||
* (relative to parent's position if applicatable)
|
||||
*
|
||||
* @type {Vector2D}
|
||||
* @memberof Component2D
|
||||
*/
|
||||
@ -47,7 +136,7 @@ T extends {} | void = {} | void
|
||||
/**
|
||||
* Component scale relative to 1 case size
|
||||
*
|
||||
* (see also: GameEngine.caseSize)
|
||||
* (relative to parent's position if applicatable)
|
||||
*
|
||||
* @type {Vector2D}
|
||||
* @memberof Component2D
|
||||
@ -57,13 +146,22 @@ T extends {} | void = {} | void
|
||||
/**
|
||||
* Component collider for events
|
||||
*
|
||||
* @type {BoxCollider2D}
|
||||
* @type {Collider}
|
||||
* @memberof Component2D
|
||||
*/
|
||||
public collider?: BoxCollider2D
|
||||
public collider: Collider | Array<Collider> | null = null
|
||||
|
||||
/**
|
||||
* Change the origin point (default to middle)
|
||||
* Parent component of self is any
|
||||
* this value is automatically set
|
||||
*
|
||||
* @type {Component2D}
|
||||
* @memberof Component2D
|
||||
*/
|
||||
public parent?: Component2D
|
||||
|
||||
/**
|
||||
* Change the origin point (default to top left)
|
||||
*/
|
||||
public origin: Vector2D = new Vector2D(0 , 0)
|
||||
|
||||
@ -86,12 +184,45 @@ T extends {} | void = {} | void
|
||||
*/
|
||||
public debug?: boolean
|
||||
|
||||
/**
|
||||
* Component rotation in Degrees
|
||||
*/
|
||||
public rotation = 0
|
||||
|
||||
/**
|
||||
* define if the component is enabled (update nor render is run if false)
|
||||
*/
|
||||
public enabled?: boolean = true
|
||||
|
||||
protected params: T = {} as T
|
||||
|
||||
private cache: ComponentCache = {}
|
||||
|
||||
public constructor(it: T | void) {
|
||||
if (it) {
|
||||
this.params = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a property of the component
|
||||
*
|
||||
* @param newProps the new props for the component
|
||||
*/
|
||||
public setProps(newProps: Partial<ComponentProps>) {
|
||||
objectLoop(newProps, (value, obj) => {
|
||||
if (obj === 'zIndex') {
|
||||
this.zIndex = value as number
|
||||
}
|
||||
if (this.props[obj] === value) {
|
||||
return
|
||||
}
|
||||
this.props[obj] = value as any
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Function run when the component is initialized
|
||||
*/
|
||||
@ -103,5 +234,167 @@ T extends {} | void = {} | void
|
||||
*/
|
||||
public update?(state: ComponentState): Promise<void> | void
|
||||
|
||||
public destroy?(): Promise<void> | void
|
||||
/**
|
||||
* run just before the component is going to be destroyed
|
||||
*/
|
||||
public destroy?(): void
|
||||
|
||||
|
||||
/**
|
||||
* get the element absolute position depending on it's rotation, origin and parent's absolute position
|
||||
*/
|
||||
public getAbsolutePosition(calculateRotation = true): Vector2D {
|
||||
let pos = this.position.sum(
|
||||
this.scale.multiply(this.origin)
|
||||
)
|
||||
|
||||
if (this.parent) {
|
||||
pos = pos.sum(this.parent.getAbsolutePosition(calculateRotation))
|
||||
}
|
||||
|
||||
const rotation = this.getAbsoluteRotation()
|
||||
if (this.cache.absolutePosition?.previousPosition && this.cache.absolutePosition.previousPosition.equal(pos) && this.cache.absolutePosition.previousRotation === rotation) {
|
||||
return this.cache.absolutePosition.cacheResult as Vector2D
|
||||
}
|
||||
|
||||
if (this.rotation && calculateRotation) {
|
||||
const res = pos.rotate(this.getMiddle(true), this.getAbsoluteRotation())
|
||||
// FIXME: should not modify the component position here
|
||||
pos.set(res)
|
||||
this.cache.absolutePosition = {
|
||||
cacheResult: res,
|
||||
previousPosition: pos,
|
||||
previousRotation: rotation
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
/**
|
||||
* get the component's absolute rotation
|
||||
* @returns the component's absolute rotation in degrees
|
||||
*/
|
||||
public getAbsoluteRotation(): number {
|
||||
if (this.parent) {
|
||||
return (this.parent.getAbsoluteRotation() + this.rotation) % 360
|
||||
}
|
||||
return (this.rotation + (GameEngine.getGameEngine().currentScene?.globalRotation ?? 0)) % 360
|
||||
}
|
||||
|
||||
public getAbsoluteZIndex(): number {
|
||||
const zIndex = this.zIndex ?? 0
|
||||
if (this.parent) {
|
||||
return this.parent.getAbsoluteZIndex() + zIndex
|
||||
}
|
||||
return zIndex
|
||||
}
|
||||
|
||||
public setState<K extends keyof ComponentState>(key: K, value: ComponentState[K]): void
|
||||
public setState(key: keyof ComponentState, value: any): void {
|
||||
this.state[key as keyof ComponentState] = value
|
||||
}
|
||||
|
||||
public updateParam<K extends keyof T>(key: K, value: Component2D<T>['params'][K]): void {
|
||||
if (this.params[key] !== value) {
|
||||
this.params[key] = value
|
||||
this.onParamUpdated(key as string, value)
|
||||
}
|
||||
}
|
||||
|
||||
public getParam<K extends keyof T>(key: K): Component2D<T>['params'][K]
|
||||
public getParam(key: string): any
|
||||
public getParam<K extends keyof T>(key: K | string): K | any {
|
||||
return this.params[key as keyof T]
|
||||
}
|
||||
|
||||
public getParams(): Component2D<T>['params'] {
|
||||
return this.params
|
||||
}
|
||||
|
||||
/**
|
||||
* return the real width of the element (including rotation and all the shit)
|
||||
*/
|
||||
public getCalculatedWidth(): number {
|
||||
const radians = this.rotation * (Math.PI / 180)
|
||||
|
||||
const rotatedWidth = Math.abs(this.scale.x * Math.cos(radians)) + Math.abs(this.scale.y * Math.sin(radians))
|
||||
return rotatedWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* return the real width of the element (including rotation and all the shit)
|
||||
*/
|
||||
public getCalculatedHeight(): number {
|
||||
const radians = this.rotation * (Math.PI / 180)
|
||||
const rotatedWidth = Math.abs(this.scale.y * Math.cos(radians)) + Math.abs(this.scale.x * Math.sin(radians))
|
||||
return rotatedWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* get the most top left position of the element
|
||||
*
|
||||
* if rotated it will give a point out of the element
|
||||
*
|
||||
* @param absolute give the relative or absolute position
|
||||
* @returns the point in space in a relative or absolute position
|
||||
*/
|
||||
public calculateTopLeftPosition(absolute = false): Vector2D {
|
||||
return this.getMiddle(absolute).sub(
|
||||
this.getCalculatedWidth() / 2,
|
||||
this.getCalculatedHeight() / 2
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* get the most bottom right position of the element
|
||||
*
|
||||
* if rotated it will give a point out of the element
|
||||
*
|
||||
* @param absolute give the relative or absolute position
|
||||
* @returns the point in space in a relative or absolute position
|
||||
*/
|
||||
public calculateBottomRightPosition(absolute = false): Vector2D {
|
||||
return this.getMiddle(absolute).sum(
|
||||
this.getCalculatedWidth() / 2,
|
||||
this.getCalculatedHeight() / 2
|
||||
)
|
||||
}
|
||||
|
||||
public calculatePositionFor(item: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight', absolute = false) {
|
||||
let pos = this.position
|
||||
if (absolute && this.parent) {
|
||||
pos = this.getAbsolutePosition(false)
|
||||
}
|
||||
|
||||
switch (item) {
|
||||
case 'topRight':
|
||||
pos = pos.sum(this.scale.x, 0)
|
||||
break
|
||||
case 'bottomLeft':
|
||||
pos = pos.sum(0, this.scale.y)
|
||||
break
|
||||
case 'bottomRight':
|
||||
pos = pos.sum(this.scale.x, this.scale.y)
|
||||
break
|
||||
}
|
||||
|
||||
return pos.rotate(this.getMiddle(absolute), this.getAbsoluteRotation())
|
||||
}
|
||||
|
||||
/**
|
||||
* return the center point of the component
|
||||
* @param absolute return the absolute position instead of the local position
|
||||
*
|
||||
* @returns the location of the middle of the component
|
||||
*/
|
||||
public getMiddle(absolute = false): Vector2D {
|
||||
if (absolute) {
|
||||
return this.getAbsolutePosition(false).sum(this.scale.div(2))
|
||||
}
|
||||
return this.position.sum(this.scale.div(2))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
protected onParamUpdated(key: string, value: any) {}
|
||||
}
|
||||
|
@ -1,8 +1,161 @@
|
||||
import GameEngine from '..'
|
||||
import Vector2D from '../2D/Vector2D'
|
||||
import Component2D from '../Component2D'
|
||||
import TextRenderer from '../Renderer/TextRenderer'
|
||||
import { apply } from '../libs/CodeUtils'
|
||||
import MathUtils from '../libs/MathUtils'
|
||||
import Cursor from './Cursor'
|
||||
|
||||
/**
|
||||
* Currently not working Camera implementation
|
||||
*/
|
||||
export default class Camera {
|
||||
public topLeft = new Vector2D(0.5, 0.5)
|
||||
export default class Camera extends Component2D<{
|
||||
position?: Vector2D
|
||||
zoom?: number
|
||||
debug?: boolean
|
||||
minX?: number
|
||||
maxX?: number
|
||||
minY?: number
|
||||
maxY?: number
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
disableZoom?: boolean
|
||||
childs?: Array<Component2D>
|
||||
}> {
|
||||
public name = 'Camera'
|
||||
// public position: Vector2D = new Vector2D(0)
|
||||
|
||||
private _zoom = this.params.zoom ?? 1
|
||||
public get zoom() {
|
||||
return this._zoom
|
||||
}
|
||||
public set zoom(value) {
|
||||
if (value === this.params.zoom && this.params.disableZoom) {
|
||||
this._zoom = value
|
||||
}
|
||||
if (this.params.disableZoom) {
|
||||
return
|
||||
}
|
||||
const old = this._zoom
|
||||
this._zoom = MathUtils.clamp(value, this.params.minZoom ?? 0.01, this.params.maxZoom ?? Infinity)
|
||||
const ge = GameEngine.getGameEngine()
|
||||
this.scale = new Vector2D(
|
||||
ge.getXCaseCount() / this.zoom,
|
||||
ge.getYCaseCount() / this.zoom
|
||||
)
|
||||
this.onZoomChange(old, this._zoom)
|
||||
}
|
||||
|
||||
public getScale(): Vector2D {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
this.scale = new Vector2D(
|
||||
ge.getXCaseCount() / this.zoom,
|
||||
ge.getYCaseCount() / this.zoom
|
||||
)
|
||||
return this.scale
|
||||
}
|
||||
|
||||
public init(): void | Promise<void> {
|
||||
if (this.params.position) {
|
||||
this.position = this.params.position
|
||||
}
|
||||
if (this.params.zoom) {
|
||||
this.setZoom(this.params.zoom)
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.renderer = new TextRenderer(this, {
|
||||
color: 'black',
|
||||
text: '',
|
||||
size: 16
|
||||
})
|
||||
}
|
||||
if (this.params.childs) {
|
||||
this.childs.push(
|
||||
...this.params.childs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public update() {
|
||||
this.position.set(
|
||||
MathUtils.clamp(this.position.x, this.params.minX ?? -Infinity, this.params.maxX ?? Infinity),
|
||||
MathUtils.clamp(this.position.y, this.params.minY ?? -Infinity, this.params.maxY ?? Infinity)
|
||||
)
|
||||
let needCursorUpdate = false
|
||||
const scene = GameEngine.getGameEngine()?.currentScene
|
||||
if (!scene) {
|
||||
return
|
||||
}
|
||||
|
||||
if (scene.scale !== this.zoom) {
|
||||
scene.scale = this.zoom
|
||||
needCursorUpdate = true
|
||||
}
|
||||
|
||||
if (!scene.position.equal(this.position)) {
|
||||
scene.position.set(this.position)
|
||||
needCursorUpdate = true
|
||||
}
|
||||
|
||||
if (needCursorUpdate) {
|
||||
const cursor = scene.getComponents().find((it) => it instanceof Cursor) as Cursor | undefined
|
||||
cursor?.triggerUpdate()
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
apply(this.renderer as TextRenderer, (it) => {
|
||||
it.setProps({
|
||||
text: `pos: ${this.position.toFixed(3)}, scale: ${this.getScale().toFixed(3)}, zoom: ${this.zoom}`,
|
||||
size: 16 / this._zoom
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public setZoomAndPos(zoom: number, x: number | Vector2D, y?: number) {
|
||||
this._zoom = zoom
|
||||
this.position.set(x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value zoom with 1 being the base
|
||||
*/
|
||||
public setZoom(value: number) {
|
||||
this.zoom = value
|
||||
}
|
||||
|
||||
public addToZoom(value: number, min?: number, max?: number) {
|
||||
this.zoom += value
|
||||
if (min && min > this.zoom) {
|
||||
this.zoom = min
|
||||
}
|
||||
if (max && max < this.zoom) {
|
||||
this.zoom = max
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
private onZoomChange(oldZoom: number, newZoom: number) {
|
||||
if (oldZoom === newZoom) {
|
||||
return
|
||||
}
|
||||
|
||||
const ge = GameEngine.getGameEngine()
|
||||
const cursor = ge.currentScene?.getComponents().find((it) => it instanceof Cursor) as Cursor | undefined
|
||||
const amount = 1 - oldZoom / newZoom
|
||||
const scale = this.getScale()
|
||||
let at: Vector2D
|
||||
if (cursor) {
|
||||
at = cursor.position
|
||||
} else {
|
||||
at = this.position.sum(scale.div(2))
|
||||
}
|
||||
const newX = this.position.x + (at.x - this.position.x) * amount
|
||||
const newY = this.position.y + (at.y - this.position.y) * amount
|
||||
|
||||
this.position.set(
|
||||
MathUtils.clamp(newX, this.params.minX ?? -Infinity, this.params.maxX ?? Infinity),
|
||||
MathUtils.clamp(newY, this.params.minY ?? -Infinity, this.params.maxY ?? Infinity)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
54
src/GameEngine/Components/ComponentRenderer.ts
Normal file
54
src/GameEngine/Components/ComponentRenderer.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import BoxCollider2D from '../2D/Collider/BoxCollider2D'
|
||||
import Vector2D from '../2D/Vector2D'
|
||||
import Component2D, { ComponentState } from '../Component2D'
|
||||
import Renderer from '../Renderer'
|
||||
import { apply } from '../libs/CodeUtils'
|
||||
import Cursor from './Cursor'
|
||||
|
||||
export default class ComponentRenderer extends Component2D<{
|
||||
renderer?: Renderer
|
||||
position?: Vector2D
|
||||
scale?: Vector2D
|
||||
onClick?: () => void
|
||||
onDown?: () => void
|
||||
onUp?: () => void
|
||||
collisionTag?: string | Array<string>
|
||||
}> {
|
||||
|
||||
public name = 'ComponentRenderer'
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public update(states: ComponentState): void | Promise<void> {
|
||||
if (this.params.onClick && !this.collider) {
|
||||
this.collider = apply(new BoxCollider2D(this, {scale: new Vector2D(2)}), (it) => it.tags = this.params.collisionTag)
|
||||
} else if (this.params.onClick || this.params.onDown) {
|
||||
const collision = states.collisions?.find((it) => it.component instanceof Cursor)
|
||||
const cursor = collision?.component as Cursor
|
||||
if (cursor && !cursor.isDown && cursor.wasDown) {
|
||||
this.params.onClick?.()
|
||||
this.params.onUp?.()
|
||||
} else if (cursor && cursor.isDown && !cursor.wasDown) {
|
||||
this.params.onDown?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateRenderer(key: string, value: any) {
|
||||
this.renderer?.setProps({[key]: value})
|
||||
}
|
||||
|
||||
protected onParamUpdated(key: string, value: any): void {
|
||||
if (this.params.renderer) {
|
||||
this.renderer = this.params.renderer
|
||||
}
|
||||
|
||||
if (this.params.position) {
|
||||
this.position = this.params.position
|
||||
}
|
||||
|
||||
if (this.params.scale) {
|
||||
this.scale = this.params.scale
|
||||
}
|
||||
}
|
||||
|
||||
}
|
372
src/GameEngine/Components/Cursor.ts
Normal file
372
src/GameEngine/Components/Cursor.ts
Normal file
@ -0,0 +1,372 @@
|
||||
import GameEngine from '..'
|
||||
import PointCollider2D from '../2D/Collider/PointCollider2D'
|
||||
import Vector2D from '../2D/Vector2D'
|
||||
import Component2D from '../Component2D'
|
||||
import MultiRenderer from '../Renderer/MultiRenderer'
|
||||
import RectRenderer from '../Renderer/RectRenderer'
|
||||
import TextRenderer from '../Renderer/TextRenderer'
|
||||
import { apply } from '../libs/CodeUtils'
|
||||
import Camera from './Camera'
|
||||
|
||||
type MoveEvents = 'leftClickDown' | 'middleClickDown'
|
||||
|
||||
interface ButtonState {
|
||||
/**
|
||||
* left button is down (refreshed each frames)
|
||||
*/
|
||||
isDown: boolean
|
||||
/**
|
||||
* left button was down previous frame (refreshed each frames)
|
||||
*/
|
||||
wasDown: boolean
|
||||
/**
|
||||
* left button is down in realtime
|
||||
*/
|
||||
eventDown: boolean
|
||||
}
|
||||
|
||||
export default class Cursor extends Component2D<{
|
||||
debug?: boolean
|
||||
disableZoom?: boolean
|
||||
workOverInterface?: boolean
|
||||
/**
|
||||
* allow the cursor to move the view
|
||||
*/
|
||||
enabledMove?: boolean | MoveEvents | Array<MoveEvents>
|
||||
zIndex?: number
|
||||
}> {
|
||||
public name = 'Cursor'
|
||||
|
||||
/**
|
||||
* cursor position
|
||||
*/
|
||||
public override position: Vector2D = new Vector2D(0,0)
|
||||
|
||||
public leftBtn = {
|
||||
isDown: false,
|
||||
wasDown: false,
|
||||
eventDown: false,
|
||||
canChange: true
|
||||
}
|
||||
|
||||
public rightBtn = {
|
||||
isDown: false,
|
||||
wasDown: false,
|
||||
eventDown: false,
|
||||
canChange: true
|
||||
}
|
||||
|
||||
public middleBtn = {
|
||||
isDown: false,
|
||||
wasDown: false,
|
||||
eventDown: false,
|
||||
canChange: true
|
||||
}
|
||||
|
||||
public override origin: Vector2D = new Vector2D(0)
|
||||
|
||||
public override scale: Vector2D = new Vector2D(1)
|
||||
|
||||
public override collider: PointCollider2D = new PointCollider2D(this, {
|
||||
tags: 'cursor'
|
||||
})
|
||||
|
||||
private touchZoom = 0
|
||||
|
||||
private oldPosition: [number, number] | null = null
|
||||
|
||||
private base!: Window | Element
|
||||
|
||||
/**
|
||||
* is the cursor click is down
|
||||
*/
|
||||
public get isDown(): boolean {
|
||||
return this.leftBtn.isDown || this.rightBtn.isDown || this.middleBtn.isDown
|
||||
}
|
||||
|
||||
/**
|
||||
* was the cursor click down on previous frame
|
||||
*/
|
||||
public get wasDown(): boolean {
|
||||
return this.leftBtn.wasDown || this.rightBtn.wasDown || this.middleBtn.wasDown
|
||||
}
|
||||
|
||||
public override init(): void | Promise<void> {
|
||||
this.base = window ?? GameEngine.getGameEngine().canvas
|
||||
this.base.addEventListener('mousemove', this.onMouseMove)
|
||||
this.base.addEventListener('mousedown', this.onMouseDown)
|
||||
this.base.addEventListener('touchmove', this.onTouchMove, { passive: false })
|
||||
this.base.addEventListener('touchstart', this.onTouchStart)
|
||||
if (this.params.zIndex) {
|
||||
this.zIndex = this.params.zIndex
|
||||
}
|
||||
|
||||
// add up events on document so they are catched everywhere
|
||||
window.addEventListener('mouseup', this.onMouseUp)
|
||||
window.addEventListener('touchend', this.onTouchEnd)
|
||||
|
||||
// this.debug = this.params?.debug
|
||||
if (this.params.debug) {
|
||||
this.renderer = new MultiRenderer(this, {
|
||||
renderers: [
|
||||
new RectRenderer(this, {material: 'blue'}),
|
||||
new TextRenderer(this, {
|
||||
color: 'black',
|
||||
text: '',
|
||||
size: 16,
|
||||
overrideSizeLimit: true
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
this.base.removeEventListener('mousemove', this.onMouseMove as any)
|
||||
this.base.removeEventListener('mousedown', this.onMouseDown as any)
|
||||
this.base.removeEventListener('touchmove', this.onTouchMove as any)
|
||||
this.base.removeEventListener('touchstart', this.onTouchStart as any)
|
||||
|
||||
// add up events on document so they are catched everywhere
|
||||
window.removeEventListener('mouseup', this.onMouseUp)
|
||||
window.removeEventListener('touchend', this.onTouchEnd)
|
||||
}
|
||||
|
||||
public override update(): void | Promise<void> {
|
||||
this.leftBtn.wasDown = this.leftBtn.isDown
|
||||
this.leftBtn.isDown = this.leftBtn.eventDown
|
||||
this.leftBtn.canChange = true
|
||||
this.rightBtn.wasDown = this.rightBtn.isDown
|
||||
this.rightBtn.isDown = this.rightBtn.eventDown
|
||||
this.rightBtn.canChange = true
|
||||
this.middleBtn.wasDown = this.middleBtn.isDown
|
||||
this.middleBtn.isDown = this.middleBtn.eventDown
|
||||
this.middleBtn.canChange = true
|
||||
if (this.params.debug) {
|
||||
console.log('update')
|
||||
;((this.renderer as MultiRenderer).props.renderers?.[1] as TextRenderer).setProps({text: `${JSON.stringify(this.leftBtn, undefined, '\t')}\npos: ${this.position.toFixed(3)}`})
|
||||
}
|
||||
}
|
||||
|
||||
public triggerUpdate() {
|
||||
this.updatePosition(...this.oldPosition ?? [0, 0])
|
||||
}
|
||||
|
||||
public setPosition(clientX: number, clientY: number) {
|
||||
this.updatePosition(clientX, clientY)
|
||||
}
|
||||
|
||||
public getPosition() {
|
||||
return this.position
|
||||
}
|
||||
|
||||
private onMouseMove = (ev: MouseEvent) => {
|
||||
if (this.params.debug) {
|
||||
console.log('onMouseMove')
|
||||
}
|
||||
this.onMove(ev)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
private onMouseDown = (ev: MouseEvent) => {
|
||||
if (this.params.debug) {
|
||||
console.log('onMouseDown')
|
||||
}
|
||||
switch (ev.button) {
|
||||
case 0:
|
||||
this.leftBtn.eventDown = true
|
||||
this.leftBtn.canChange = false
|
||||
break
|
||||
case 2:
|
||||
this.rightBtn.eventDown = true
|
||||
this.rightBtn.canChange = false
|
||||
break
|
||||
case 1:
|
||||
this.middleBtn.eventDown = true
|
||||
this.middleBtn.canChange = false
|
||||
break
|
||||
default:
|
||||
console.warn('WTF is this mouse button')
|
||||
break
|
||||
}
|
||||
this.onDown(ev)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
private onMouseUp = (ev: MouseEvent) => {
|
||||
if (this.params.debug) {
|
||||
console.log('onMouseUp')
|
||||
}
|
||||
switch (ev.button) {
|
||||
case 0:
|
||||
this.leftBtn.eventDown = false
|
||||
this.leftBtn.canChange = false
|
||||
break
|
||||
case 2:
|
||||
if (!this.rightBtn.canChange) {
|
||||
break
|
||||
}
|
||||
this.rightBtn.eventDown = false
|
||||
this.rightBtn.canChange = false
|
||||
break
|
||||
case 1:
|
||||
if (!this.middleBtn.canChange) {
|
||||
break
|
||||
}
|
||||
this.middleBtn.eventDown = false
|
||||
this.middleBtn.canChange = false
|
||||
break
|
||||
default:
|
||||
console.warn('WTF is this mouse button')
|
||||
break
|
||||
}
|
||||
this.onUp(ev)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
private onTouchMove = (ev: TouchEvent) => {
|
||||
if (this.params.debug) {
|
||||
console.log('onTouchMove')
|
||||
}
|
||||
this.leftBtn.eventDown = true
|
||||
if (ev.touches.length >= 2 && !this.params.disableZoom) {
|
||||
ev.preventDefault()
|
||||
|
||||
// launch the onMove event with the pointer position being at the center of both points
|
||||
const xMin = Math.min(ev.touches[0].pageX, ev.touches[1].pageX)
|
||||
const xMax = Math.max(ev.touches[0].pageX, ev.touches[1].pageX)
|
||||
const yMin = Math.min(ev.touches[0].pageY, ev.touches[1].pageY)
|
||||
const yMax = Math.max(ev.touches[0].pageY, ev.touches[1].pageY)
|
||||
|
||||
this.onMove([
|
||||
xMin + (xMax - xMin) / 2,
|
||||
yMin + (yMax - yMin) / 2
|
||||
])
|
||||
|
||||
const cam = GameEngine
|
||||
.getGameEngine()
|
||||
.currentScene
|
||||
?.camera
|
||||
if (!cam) {
|
||||
return
|
||||
}
|
||||
|
||||
const nv = Math.hypot(
|
||||
ev.touches[0].pageX - ev.touches[1].pageX,
|
||||
ev.touches[0].pageY - ev.touches[1].pageY
|
||||
)
|
||||
|
||||
cam.addToZoom(-((this.touchZoom - nv) / 100), 1)
|
||||
this.touchZoom = nv
|
||||
return
|
||||
}
|
||||
this.onMove(ev.touches.item(0) ?? undefined)
|
||||
}
|
||||
|
||||
private onTouchStart = (ev: TouchEvent) => {
|
||||
if (this.params.debug) {
|
||||
console.log('onTouchStart')
|
||||
}
|
||||
this.leftBtn.eventDown = true
|
||||
this.onDown(ev.touches.item(0) ?? undefined)
|
||||
if (ev.touches.length >= 2) {
|
||||
this.touchZoom = Math.hypot(
|
||||
ev.touches[0].pageX - ev.touches[1].pageX,
|
||||
ev.touches[0].pageY - ev.touches[1].pageY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private onTouchEnd = (ev: TouchEvent) => {
|
||||
if (this.params.debug) {
|
||||
console.log('onTouchEnd')
|
||||
}
|
||||
this.leftBtn.eventDown = false
|
||||
this.onUp(ev.touches.item(0) ?? undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch the onMove events
|
||||
*/
|
||||
private onMove(ev?: MouseEvent | Touch | [number, number]) {
|
||||
if (Array.isArray(ev)) {
|
||||
this.updatePosition(
|
||||
ev[0],
|
||||
ev[1]
|
||||
)
|
||||
return
|
||||
}
|
||||
if (ev) {
|
||||
// console.log('onMove')
|
||||
this.updatePosition(
|
||||
ev.clientX ?? 0,
|
||||
ev.clientY ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch the onDown events
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
private onDown(ev?: MouseEvent | Touch) {
|
||||
if (ev) {
|
||||
if ((ev.target as HTMLElement).nodeName !== 'CANVAS' && !this.params.workOverInterface) {
|
||||
return
|
||||
}
|
||||
// console.log('onDown')
|
||||
this.updatePosition(
|
||||
ev.clientX ?? 0,
|
||||
ev.clientY ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* catch the onUp events
|
||||
*/
|
||||
private onUp(ev?: MouseEvent | Touch) {
|
||||
// console.log('onUp')
|
||||
if (ev) {
|
||||
this.updatePosition(
|
||||
ev.clientX ?? 0,
|
||||
ev.clientY ?? 0
|
||||
)
|
||||
}
|
||||
this.oldPosition = null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
private updatePosition(clientX: number, clientY: number) {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
if (!ge) {
|
||||
return
|
||||
}
|
||||
const moveEvents: Array<MoveEvents> =
|
||||
this.params.enabledMove === true && ['leftClickDown', 'middleClickDown'] ||
|
||||
typeof this.params.enabledMove === 'string' && [this.params.enabledMove] ||
|
||||
(Array.isArray(this.params.enabledMove) ? this.params.enabledMove : [])
|
||||
|
||||
let doMove = false
|
||||
for (const event of moveEvents) {
|
||||
if (event === 'leftClickDown' && this.leftBtn.isDown) {
|
||||
doMove = true
|
||||
break
|
||||
}
|
||||
if (event === 'middleClickDown' && this.middleBtn.isDown) {
|
||||
doMove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (doMove && this.oldPosition) {
|
||||
const camera = ge.currentScene?.components.find((it) => it instanceof Camera) as Camera | undefined
|
||||
if (camera) {
|
||||
const diff = [this.oldPosition[0] - clientX, this.oldPosition[1] - clientY]
|
||||
camera.position = camera.position.sum(diff[0] / 5 / camera.zoom, diff[1] / 5 / camera.zoom)
|
||||
}
|
||||
}
|
||||
this.oldPosition = [clientX, clientY]
|
||||
this.position.set(Vector2D.fromBrowser(clientX, clientY))
|
||||
}
|
||||
}
|
@ -1,57 +1,29 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
import ComponentDebug from 'GameEngine/2D/Debug/ComponentDebug'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
|
||||
import GameEngine from '..'
|
||||
import Vector2D from '../2D/Vector2D'
|
||||
import Component2D from '../Component2D'
|
||||
import TextRenderer from '../Renderer/TextRenderer'
|
||||
|
||||
export default class FPSCounter extends Component2D<{textColor?: string, size?: number}> {
|
||||
export default class FPSCounter extends Component2D<{size?: number}> {
|
||||
public name = 'FPSCounter'
|
||||
|
||||
public override position: Vector2D = new Vector2D(0)
|
||||
public override scale: Vector2D = new Vector2D(100, 1)
|
||||
public override renderer: TextRenderer = new TextRenderer(this, {text: 'loading...', color: 'black', stroke: 'white'})
|
||||
|
||||
public position: Vector2D = new Vector2D(10,8)
|
||||
public scale: Vector2D = new Vector2D(1, 1)
|
||||
public origin: Vector2D = new Vector2D(0, 0)
|
||||
public childs: Array<Component2D> = [new ComponentDebug(this)]
|
||||
|
||||
public renderer: TextRenderer = new TextRenderer(this, {text: 'loading...'})
|
||||
|
||||
private length = 1
|
||||
private previousFrameTimes: Array<number> = []
|
||||
private lastUpdate = window.performance.now() * 1000
|
||||
|
||||
public init() {
|
||||
const fps = GameEngine.getGameEngine().options?.goalFramerate
|
||||
if (!fps || fps < 1) {
|
||||
this.length = 60
|
||||
} else {
|
||||
this.length = fps
|
||||
}
|
||||
|
||||
if (this.params.textColor) {
|
||||
this.renderer.color = this.params.textColor
|
||||
}
|
||||
public override init() {
|
||||
|
||||
if (this.params.size) {
|
||||
this.renderer.size = this.params.size
|
||||
this.renderer.setProps({size: this.params.size})
|
||||
}
|
||||
}
|
||||
|
||||
public update() {
|
||||
const t = GameEngine.getGameEngine().lastFrame
|
||||
// if (!t) {return}
|
||||
// console.log(this.previousFrameTimes, t)
|
||||
const diff = t - this.lastUpdate
|
||||
this.lastUpdate = t
|
||||
this.previousFrameTimes.push(diff)
|
||||
if (this.previousFrameTimes.length > this.length) {
|
||||
this.previousFrameTimes.shift()
|
||||
}
|
||||
const time = (this.previousFrameTimes.reduce((p, c) => p + c, 0)) / this.previousFrameTimes.length
|
||||
if (time === 0) {
|
||||
this.renderer.text = 'a lot'
|
||||
} else {
|
||||
this.renderer.text = (1000 / time).toFixed(2)
|
||||
|
||||
public override update() {
|
||||
const perfs = GameEngine.getGameEngine().currentScene?.updatePerformances
|
||||
if (!perfs || perfs.total === -1) {
|
||||
this.renderer.setProps({text: 'Loading...'})
|
||||
return
|
||||
}
|
||||
this.renderer.setProps({text: (1000 / (perfs.total ?? 1)).toFixed(0)})
|
||||
}
|
||||
|
||||
}
|
||||
|
470
src/GameEngine/Controller.ts
Normal file
470
src/GameEngine/Controller.ts
Normal file
@ -0,0 +1,470 @@
|
||||
import { NotificationManager } from '@dzeio/components'
|
||||
import Listener from '@dzeio/listener'
|
||||
|
||||
export interface ControllerConnectionEvent {
|
||||
controller: ControllerInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Event containing a button state change
|
||||
*/
|
||||
export interface ControllerButtonEvent {
|
||||
/**
|
||||
* The key as a text
|
||||
*
|
||||
* can be the keyboard/gamepad character
|
||||
*/
|
||||
key: string
|
||||
|
||||
/**
|
||||
* Warning: prefer the use of 'key'
|
||||
*/
|
||||
keyId: number
|
||||
|
||||
/**
|
||||
* Say if the button is pressed or not
|
||||
*/
|
||||
pressed: boolean
|
||||
|
||||
/**
|
||||
* Reference to the controller
|
||||
*/
|
||||
controller: ControllerInterface
|
||||
}
|
||||
|
||||
export interface ControllerAxisEvent {
|
||||
|
||||
/**
|
||||
* axis id that changed
|
||||
*/
|
||||
axis: string
|
||||
|
||||
/**
|
||||
* axis new value as a value between 0 and 1
|
||||
*
|
||||
* @see Controller.setControllerDeadZone to setup the controller dead zone globally
|
||||
*/
|
||||
value: number
|
||||
|
||||
/**
|
||||
* Reference to the controller
|
||||
*/
|
||||
controller: ControllerInterface
|
||||
}
|
||||
|
||||
export interface ControllerStates {
|
||||
/**
|
||||
* timestamp of new the input changed value last time
|
||||
*/
|
||||
lastChange: number
|
||||
|
||||
/**
|
||||
* if the key being repeated
|
||||
*
|
||||
* mostly for internal usage
|
||||
*/
|
||||
repeat?: boolean
|
||||
|
||||
/**
|
||||
* the current value of the input
|
||||
*/
|
||||
value: number
|
||||
}
|
||||
|
||||
interface GamepadControllerInterface {
|
||||
/**
|
||||
* The Gamepad ID (mostly the name/brand with some unique identifier somewhere in the text)
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* the type of controller
|
||||
*
|
||||
* can be 'gamepad' | 'keyboard'
|
||||
*/
|
||||
type: 'gamepad'
|
||||
|
||||
/**
|
||||
* the Browser gamepad class
|
||||
*/
|
||||
gamepad: Gamepad
|
||||
|
||||
/**
|
||||
* the gamepad mapping
|
||||
*/
|
||||
mapping: GamepadMapping
|
||||
|
||||
/**
|
||||
* the input states
|
||||
*/
|
||||
states: Record<string | number, ControllerStates>
|
||||
}
|
||||
|
||||
type ControllerInterface = {
|
||||
/**
|
||||
* the type of controller
|
||||
*
|
||||
* can be 'gamepad' | 'keyboard'
|
||||
*/
|
||||
type: 'keyboard'
|
||||
|
||||
/**
|
||||
* The Gamepad ID (mostly the name/brand with some unique identifier somewhere in the text)
|
||||
*/
|
||||
id: 'keyboard'
|
||||
} | GamepadControllerInterface
|
||||
|
||||
/**
|
||||
* Gamepad mapping of IDs into human readable keys
|
||||
*/
|
||||
type GamepadMapping = Array<string | null>
|
||||
|
||||
/**
|
||||
* Nintendo Switch specific controls
|
||||
*/
|
||||
const SwitchMapping: GamepadMapping = [
|
||||
'b',
|
||||
'a',
|
||||
'x',
|
||||
'y',
|
||||
'screen',
|
||||
'l',
|
||||
'r',
|
||||
'zl',
|
||||
'zr',
|
||||
'select',
|
||||
'start',
|
||||
'home',
|
||||
'leftthumb',
|
||||
'rightthumb'
|
||||
]
|
||||
|
||||
/**
|
||||
* Xbox specific controls
|
||||
*/
|
||||
const XboxMapping: GamepadMapping = [
|
||||
'a',
|
||||
'b',
|
||||
null,
|
||||
'x',
|
||||
'y',
|
||||
null,
|
||||
'lb',
|
||||
'rb',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'start',
|
||||
null,
|
||||
'leftthumb',
|
||||
'rightthumb'
|
||||
]
|
||||
|
||||
/**
|
||||
* Default mapping made as a base
|
||||
*
|
||||
* to add a new mapping use this url
|
||||
* https://luser.github.io/gamepadtest/
|
||||
*/
|
||||
const DefaultMapping: GamepadMapping = [
|
||||
'a',
|
||||
'b',
|
||||
'y',
|
||||
'x',
|
||||
'l',
|
||||
'r',
|
||||
'zl',
|
||||
'zr',
|
||||
'select',
|
||||
'start'
|
||||
]
|
||||
|
||||
/**
|
||||
* This class allows you to get the controller states and update
|
||||
* your application using the different Controllers like a keyboard or a Gamepad
|
||||
*
|
||||
* Please use `Controller.destroy()` at the end of your
|
||||
* usage to finish the event listeners setup by the class
|
||||
*/
|
||||
export default class Controller extends Listener<{
|
||||
/**
|
||||
* event sent when a new connection is established
|
||||
*/
|
||||
connected: (ev: ControllerConnectionEvent) => void
|
||||
|
||||
/**
|
||||
* event sent when a connection is broken
|
||||
*/
|
||||
disconnected: (ev: ControllerConnectionEvent) => void
|
||||
|
||||
/**
|
||||
* event sent when the key is down
|
||||
*/
|
||||
keyDown: (ev: ControllerButtonEvent) => void
|
||||
|
||||
/**
|
||||
* event sent once the key is up
|
||||
*/
|
||||
keyUp: (ev: ControllerButtonEvent) => void
|
||||
|
||||
/**
|
||||
* Event sent when a key is pressed
|
||||
*/
|
||||
keyPress: (ev: ControllerButtonEvent) => void
|
||||
|
||||
/**
|
||||
* Event sent when an axe is moving
|
||||
*
|
||||
* Event is sent continiously until it goes back to 0
|
||||
*
|
||||
* @see Controller.setControllerDeadZone to setup the controller dead zone globally
|
||||
*/
|
||||
axisMove: (ev: ControllerAxisEvent) => void
|
||||
all: (eventName: string, ...args: Array<any>) => void
|
||||
}> {
|
||||
|
||||
/**
|
||||
* List of external gamepads
|
||||
*/
|
||||
private gamepads: Array<ControllerInterface> = []
|
||||
|
||||
/**
|
||||
* Gamepad key/axes loop
|
||||
*/
|
||||
private doLoop = false
|
||||
|
||||
/**
|
||||
* Controller axes dead zone
|
||||
*/
|
||||
private controllerDeadZone = 0.5
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
|
||||
// Add the gamepad event listeners
|
||||
window.addEventListener('gamepadconnected', this.onGamepadConnect)
|
||||
window.addEventListener('gamepaddisconnected', this.onGamepadDisconnect)
|
||||
|
||||
// add the keyboard event listeners
|
||||
document.addEventListener('keydown', this.keyboardKeyDownEvent)
|
||||
document.addEventListener('keyup', this.keyboardKeyPressEvent)
|
||||
document.addEventListener('keypress', this.keyboardKeyUpEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* set the controller (Gamepads only) axis dead zone
|
||||
* @param value value between 0 and 1
|
||||
*/
|
||||
public setControllerDeadZone(value: number) {
|
||||
if (value < 0 || value >= 1) {
|
||||
throw new Error(`Controller Dead Zone Out of bound (must respect 0 < value (${value}) < 1)`)
|
||||
}
|
||||
this.controllerDeadZone = value
|
||||
}
|
||||
|
||||
/**
|
||||
* terminate the class
|
||||
*/
|
||||
public destroy() {
|
||||
this.doLoop = false
|
||||
this.gamepads = []
|
||||
window.removeEventListener('gamepadconnected', this.onGamepadConnect)
|
||||
window.removeEventListener('gamepaddisconnected', this.onGamepadDisconnect)
|
||||
document.removeEventListener('keydown', this.keyboardKeyDownEvent)
|
||||
document.removeEventListener('keyup', this.keyboardKeyPressEvent)
|
||||
document.removeEventListener('keypress', this.keyboardKeyUpEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser keyboard event handler
|
||||
*/
|
||||
private keyboardKeyDownEvent = (ev: KeyboardEvent) => {
|
||||
this.emit('keyDown', {
|
||||
key: ev.key,
|
||||
keyId: ev.keyCode,
|
||||
pressed: true,
|
||||
controller: {
|
||||
type: 'keyboard',
|
||||
id: 'keyboard'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser keyboard event handler
|
||||
*/
|
||||
private keyboardKeyPressEvent = (ev: KeyboardEvent) => {
|
||||
this.emit('keyPress', {
|
||||
key: ev.key,
|
||||
keyId: ev.keyCode,
|
||||
pressed: true,
|
||||
controller: {
|
||||
type: 'keyboard',
|
||||
id: 'keyboard'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser keyboard event handler
|
||||
*/
|
||||
private keyboardKeyUpEvent = (ev: KeyboardEvent) => {
|
||||
this.emit('keyUp', {
|
||||
key: ev.key,
|
||||
keyId: ev.keyCode,
|
||||
pressed: false,
|
||||
controller: {
|
||||
type: 'keyboard',
|
||||
id: 'keyboard'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle gamepad disconnection
|
||||
*/
|
||||
private onGamepadDisconnect = (ev: GamepadEvent) => {
|
||||
const index = this.gamepads.findIndex((it) => it.id === ev.gamepad.id)
|
||||
if (index < 0) {
|
||||
return
|
||||
}
|
||||
const gamepad = this.gamepads.splice(index, 1)[0]
|
||||
console.log('Controller disconnected', gamepad.id)
|
||||
if (this.gamepads.length === 0) {
|
||||
this.doLoop = false
|
||||
}
|
||||
this.emit('disconnected', {
|
||||
controller: gamepad
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle gamepad connection
|
||||
*/
|
||||
private onGamepadConnect = (ev: GamepadEvent) => {
|
||||
const gp = ev.gamepad
|
||||
|
||||
if (!gp) {
|
||||
NotificationManager.addNotification('Gamepad connected but not usable by device')
|
||||
return
|
||||
}
|
||||
|
||||
// create it's interface
|
||||
const gamepad: ControllerInterface = {
|
||||
type: 'gamepad',
|
||||
id: gp.id,
|
||||
gamepad: gp,
|
||||
mapping: gp.id.includes('Switch') ? SwitchMapping : gp.id.includes('Xbox') ? XboxMapping : DefaultMapping,
|
||||
states: {}
|
||||
}
|
||||
|
||||
// add buttons to states
|
||||
for (let idx = 0; idx < gp.buttons.length; idx++) {
|
||||
const button = gp.buttons[idx]
|
||||
gamepad.states['button-' + idx] = {
|
||||
lastChange: new Date().getTime(),
|
||||
value: button.pressed ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
// add axis to states
|
||||
for (let idx = 0; idx < gp.axes.length; idx++) {
|
||||
const axe = gp.axes[idx]
|
||||
gamepad.states['axe-' + idx] = {
|
||||
lastChange: new Date().getTime(),
|
||||
value: axe
|
||||
}
|
||||
}
|
||||
|
||||
console.log('New Controller connected', gamepad.id)
|
||||
|
||||
// add it to the global gamepads list
|
||||
this.gamepads.push(gamepad)
|
||||
|
||||
this.emit('connected', {
|
||||
controller: gamepad
|
||||
})
|
||||
|
||||
// start gamepads polling for new states
|
||||
if (!this.doLoop) {
|
||||
this.doLoop = true
|
||||
this.update()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling to check if the gamepad has changes with it's buttons or not
|
||||
*/
|
||||
private update() {
|
||||
const now = new Date().getTime()
|
||||
|
||||
// loop through every gamepads
|
||||
for (let gIdx = 0; gIdx < this.gamepads.length; gIdx++) {
|
||||
const gamepad = this.gamepads[gIdx]
|
||||
if (gamepad.type !== 'gamepad') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Chromium specific as gamepad.gamepad is not updated
|
||||
gamepad.gamepad = navigator.getGamepads()[gIdx] ?? gamepad.gamepad
|
||||
|
||||
// loop through each buttons
|
||||
for (let idx = 0; idx < gamepad.gamepad.buttons.length; idx++) {
|
||||
const button = gamepad.gamepad.buttons[idx]
|
||||
const gs = gamepad.states['button-' + idx]
|
||||
const repeatedPress = gs.repeat || gs.lastChange + 300 < now
|
||||
// handle state change or press repetition
|
||||
if (button.pressed !== !!gs.value || button.pressed && repeatedPress) {
|
||||
if (button.pressed && gs.value && repeatedPress) {
|
||||
gs.repeat = true
|
||||
} else if (!button.pressed) {
|
||||
gs.repeat = false
|
||||
}
|
||||
|
||||
// send keypress event once
|
||||
if (button.pressed && !gs.value) {
|
||||
this.emit('keyPress', {
|
||||
key: gamepad.mapping[idx] ?? idx.toString(),
|
||||
keyId: idx,
|
||||
pressed: button.pressed,
|
||||
controller: gamepad
|
||||
})
|
||||
}
|
||||
|
||||
// send keydown/keyup event
|
||||
gs.lastChange = now
|
||||
gamepad.states['button-' + idx].value = button.pressed ? 1 : 0
|
||||
this.emit(button.pressed ? 'keyDown' : 'keyUp', {
|
||||
key: gamepad.mapping[idx] ?? idx.toString(),
|
||||
keyId: idx,
|
||||
pressed: button.pressed,
|
||||
controller: gamepad
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// loop through each axises
|
||||
for (let idx = 0; idx < gamepad.gamepad.axes.length; idx++) {
|
||||
let axe = gamepad.gamepad.axes[idx]
|
||||
if (Math.abs(axe) < this.controllerDeadZone) {
|
||||
axe = 0
|
||||
}
|
||||
|
||||
// emit event when value is not a 0
|
||||
if (axe !== gamepad.states['axe-' + idx].value || axe !== 0) {
|
||||
gamepad.states['axe-' + idx].value = axe
|
||||
this.emit('axisMove', {
|
||||
axis: idx.toString(),
|
||||
value: axe,
|
||||
controller: gamepad
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ask for new loop when available
|
||||
if (this.doLoop) {
|
||||
requestAnimationFrame(() => this.update())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import Event from '.'
|
||||
|
||||
export default class PointerEvents extends Event {
|
||||
public override init(): void {
|
||||
document.addEventListener('mousemove', this.basicEvent)
|
||||
document.addEventListener('mousedown', this.mouseDown)
|
||||
document.addEventListener('mouseup', this.mouseUp)
|
||||
}
|
||||
|
||||
public update() {
|
||||
// pouet
|
||||
}
|
||||
|
||||
public override destroy() {
|
||||
document.removeEventListener('mousemove', this.basicEvent)
|
||||
document.removeEventListener('mousedown', this.mouseDown)
|
||||
document.removeEventListener('mouseup', this.mouseUp)
|
||||
}
|
||||
|
||||
private basicEvent = (ev: MouseEvent) => {
|
||||
console.log('Mouse Event :D')
|
||||
}
|
||||
|
||||
private mouseUp = (ev: MouseEvent) => {
|
||||
this.basicEvent(ev)
|
||||
}
|
||||
|
||||
private mouseDown = (ev: MouseEvent) => {
|
||||
this.basicEvent(ev)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
|
||||
export default abstract class Event {
|
||||
public constructor(
|
||||
protected ge: GameEngine
|
||||
) {}
|
||||
|
||||
abstract init(): void
|
||||
abstract update(): void
|
||||
abstract destroy(): void
|
||||
}
|
61
src/GameEngine/Renderer/CircleRenderer.ts
Normal file
61
src/GameEngine/Renderer/CircleRenderer.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/* eslint-disable max-depth */
|
||||
import Renderer from '.'
|
||||
import type GameEngine from '..'
|
||||
|
||||
interface Params {
|
||||
material?: string
|
||||
stroke?: string | {
|
||||
color: string
|
||||
width: number
|
||||
dotted?: number
|
||||
position?: 'inside' | 'center' | 'outside'
|
||||
}
|
||||
}
|
||||
|
||||
export default class CircleRenderer extends Renderer<Params> {
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
await super.render(ge, ctx)
|
||||
const item = this.preRender(ctx, ge)
|
||||
|
||||
ctx.beginPath()
|
||||
const realX = item[0] + item[2] / 2
|
||||
const realY = item[1] + item[3] / 2
|
||||
ctx.arc(realX, realY, item[2] / 2, 0, 180 * Math.PI)
|
||||
|
||||
if (this.props.material) {
|
||||
ctx.fillStyle = this.props.material
|
||||
ctx.fill()
|
||||
}
|
||||
if (this.props.stroke) {
|
||||
if (typeof this.props.stroke === 'string') {
|
||||
ctx.strokeStyle = this.props.stroke
|
||||
ctx.stroke()
|
||||
} else {
|
||||
if (this.props.stroke.dotted) {
|
||||
ctx.setLineDash([this.props.stroke.dotted / 2, this.props.stroke.dotted])
|
||||
}
|
||||
|
||||
ctx.lineWidth = this.props.stroke.width * (ge.currentScene?.scale ?? 1)
|
||||
ctx.stroke()
|
||||
|
||||
if (this.props.stroke.dotted) {
|
||||
ctx.setLineDash([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
if (typeof this.props.stroke === 'string') {
|
||||
ctx.strokeStyle = this.props.stroke
|
||||
} else {
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.lineWidth = 1 * (ge.currentScene?.scale ?? 1)
|
||||
}
|
||||
ctx.strokeRect(...item)
|
||||
}
|
||||
|
||||
this.postRender(ctx, ge)
|
||||
}
|
||||
}
|
80
src/GameEngine/Renderer/ImageRenderer.ts
Normal file
80
src/GameEngine/Renderer/ImageRenderer.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import Renderer from '.'
|
||||
import GameEngine from '..'
|
||||
import Asset, { AssetStatus } from '../Asset'
|
||||
|
||||
interface Params {
|
||||
asset?: Asset
|
||||
stream?: boolean
|
||||
imageRotation?: number
|
||||
debug?: boolean
|
||||
/**
|
||||
* padding for each sides
|
||||
*/
|
||||
padding?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Add origin support
|
||||
*/
|
||||
export default class ImageRenderer extends Renderer<Params> {
|
||||
|
||||
protected defaultProps: Partial<Params> = {
|
||||
stream: true
|
||||
}
|
||||
|
||||
private padding = 0
|
||||
|
||||
public onUpdate(): void {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
this.padding = (this.props.padding ?? 0) * ge.caseSize.x * (ge.currentScene?.scale ?? 1)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
await super.render(ge, ctx)
|
||||
|
||||
if (!this.props.asset) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.asset.status !== AssetStatus.LOADED) {
|
||||
if (this.props.stream) {
|
||||
// load asset but do not stop threads
|
||||
this.props.asset.load()
|
||||
return
|
||||
} else {
|
||||
await this.props.asset.load()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.asset.status === AssetStatus.LOADING && this.props.stream) {
|
||||
return
|
||||
}
|
||||
|
||||
const padding = this.padding
|
||||
const size = this.props.asset.size()
|
||||
const final = this.preRender(ctx, ge, this.props.imageRotation)
|
||||
const x = final[0] + (padding ?? 0)
|
||||
const y = final[1] + (padding ?? 0)
|
||||
const width = Math.max(final[2] - (padding ?? 0) * 2, 0)
|
||||
const height = Math.max(final[3] - (padding ?? 0) * 2, 0)
|
||||
|
||||
if (this.debug || this.component.debug) {
|
||||
ctx.fillStyle = 'red'
|
||||
ctx.fillRect(...final)
|
||||
}
|
||||
ctx.drawImage(
|
||||
this.props.asset.get(),
|
||||
0,
|
||||
0,
|
||||
size.x,
|
||||
size.y,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
)
|
||||
|
||||
this.postRender(ctx, ge)
|
||||
}
|
||||
}
|
16
src/GameEngine/Renderer/MultiRenderer.ts
Normal file
16
src/GameEngine/Renderer/MultiRenderer.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import Renderer from '.'
|
||||
import type GameEngine from '..'
|
||||
|
||||
interface Params {
|
||||
renderers: Array<Renderer>
|
||||
}
|
||||
|
||||
export default class MultiRenderer extends Renderer<Params> {
|
||||
|
||||
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
await super.render(ge, ctx)
|
||||
for await (const renderer of this.props.renderers) {
|
||||
await renderer.render(ge, ctx)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +1,292 @@
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
import GameEngine from 'GameEngine'
|
||||
import Asset from 'GameEngine/Asset'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable max-depth */
|
||||
import { objectKeys, objectLoop } from '@dzeio/object-util'
|
||||
import Renderer from '.'
|
||||
import GameEngine from '..'
|
||||
|
||||
interface Params {
|
||||
material?: string | Asset
|
||||
stroke?: string | {color: string, width: number}
|
||||
export interface StrokeOptions {
|
||||
color: string
|
||||
width: number
|
||||
dotted?: number
|
||||
position?: 'inside' | 'center' | 'outside'
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export default class RectRenderer extends Renderer implements Params {
|
||||
// type Border = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
|
||||
// type Stroke = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
public material?: string | Asset
|
||||
public stroke?: string | {color: string, width: number}
|
||||
interface Params {
|
||||
material?: string | null
|
||||
alpha?: number
|
||||
stroke?: string | StrokeOptions | {
|
||||
top?: StrokeOptions
|
||||
right?: StrokeOptions
|
||||
bottom?: StrokeOptions
|
||||
left?: StrokeOptions
|
||||
}
|
||||
borderRadius?: number | {
|
||||
topLeft?: number
|
||||
topRight?: number
|
||||
bottomLeft?: number
|
||||
bottomRight?: number
|
||||
}
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
public constructor(component: Component2D, params?: Params) {
|
||||
super(component)
|
||||
objectLoop(params ?? {}, (v, k) => {this[k as 'material'] = v})
|
||||
export default class RectRenderer extends Renderer<Params> {
|
||||
|
||||
private borderRadius: {
|
||||
topLeft: number
|
||||
topRight: number
|
||||
bottomLeft: number
|
||||
bottomRight: number
|
||||
} = {
|
||||
topLeft: 0,
|
||||
topRight: 0,
|
||||
bottomLeft: 0,
|
||||
bottomRight: 0
|
||||
}
|
||||
|
||||
private stroke?: {
|
||||
top?: StrokeOptions
|
||||
right?: StrokeOptions
|
||||
bottom?: StrokeOptions
|
||||
left?: StrokeOptions
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
const position = this.getPosition()
|
||||
const item: [number, number, number, number] = [
|
||||
// source x
|
||||
// 0 - 1.5 - -1.5
|
||||
position.x * (ge.caseSize.x),
|
||||
// source y
|
||||
position.y * (ge.caseSize.y),
|
||||
// source end X
|
||||
this.component.scale.x * (ge.caseSize.x),
|
||||
// source end Y
|
||||
this.component.scale.y * (ge.caseSize.y)
|
||||
]
|
||||
await super.render(ge, ctx)
|
||||
const item = this.preRender(ctx, ge)
|
||||
const scaling = ge.currentScene?.scale ?? 1
|
||||
|
||||
if (this.material instanceof Asset) {
|
||||
ctx.drawImage(
|
||||
await this.material.get(),
|
||||
...item
|
||||
)
|
||||
return
|
||||
ctx.globalAlpha = this.props.alpha ?? 1
|
||||
|
||||
if (this.props.material) {
|
||||
ctx.fillStyle = this.props.material
|
||||
// ctx.fillRect(...item)
|
||||
this.roundRect(ctx, ...item, true, false)
|
||||
}
|
||||
if (this.material) {
|
||||
ctx.fillStyle = this.material
|
||||
ctx.fillRect(...item)
|
||||
if (this.stroke && objectKeys(this.stroke).length > 0) {
|
||||
|
||||
objectLoop(this.stroke, (options, strokePos) => {
|
||||
if (!options) {
|
||||
return
|
||||
}
|
||||
let offset = options.width * scaling / 2
|
||||
|
||||
if (options.position === 'inside') {
|
||||
offset = -offset
|
||||
} else if (options.position === 'center') {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
ctx.lineWidth = options.width * scaling
|
||||
const lineByTwo = ctx.lineWidth / 3
|
||||
|
||||
ctx.strokeStyle = options.color
|
||||
|
||||
const xStart = item[0] - offset
|
||||
const xEnd = item[0] + item[2] + offset
|
||||
const yStart = item[1] - offset
|
||||
const yEnd = item[1] + item[3] + offset
|
||||
|
||||
if (options.dotted) {
|
||||
ctx.setLineDash([options.dotted / 2 * scaling, options.dotted * scaling])
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
switch (strokePos) {
|
||||
case 'top':
|
||||
ctx.moveTo(xStart - lineByTwo, yStart)
|
||||
ctx.lineTo(xEnd + lineByTwo, yStart)
|
||||
break
|
||||
case 'bottom':
|
||||
ctx.moveTo(xStart - lineByTwo, yEnd)
|
||||
ctx.lineTo(xEnd + lineByTwo, yEnd)
|
||||
break
|
||||
case 'left':
|
||||
ctx.moveTo(xStart, yStart - lineByTwo)
|
||||
ctx.lineTo(xStart, yEnd + lineByTwo)
|
||||
break
|
||||
case 'right':
|
||||
ctx.moveTo(xEnd, yStart - lineByTwo)
|
||||
ctx.lineTo(xEnd, yEnd + lineByTwo)
|
||||
break
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
ctx.setLineDash([])
|
||||
})
|
||||
|
||||
}
|
||||
if (this.stroke) {
|
||||
|
||||
if (this.debug) {
|
||||
if (typeof this.stroke === 'string') {
|
||||
ctx.strokeStyle = this.stroke
|
||||
} else {
|
||||
ctx.strokeStyle = this.stroke.color
|
||||
ctx.lineWidth = this.stroke.width
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.lineWidth = 1 * scaling
|
||||
}
|
||||
ctx.strokeRect(...item)
|
||||
this.roundRect(ctx, ...item, false, true)
|
||||
// ctx.strokeRect(...item)
|
||||
}
|
||||
|
||||
this.postRender(ctx, ge)
|
||||
}
|
||||
|
||||
public onUpdate(): void {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
const scaling = ge.currentScene?.scale ?? 1
|
||||
const min = Math.min(this.component.scale.x / 2, this.component.scale.y / 2)
|
||||
|
||||
this.borderRadius = {
|
||||
topLeft: Math.min(typeof this.props.borderRadius === 'number' ? this.props.borderRadius : this.props.borderRadius?.topLeft ?? 0, min),
|
||||
topRight: Math.min(typeof this.props.borderRadius === 'number' ? this.props.borderRadius : this.props.borderRadius?.topRight ?? 0, min),
|
||||
bottomLeft: Math.min(typeof this.props.borderRadius === 'number' ? this.props.borderRadius : this.props.borderRadius?.bottomLeft ?? 0, min),
|
||||
bottomRight:Math.min(typeof this.props.borderRadius === 'number' ? this.props.borderRadius : this.props.borderRadius?.bottomRight ?? 0, min)
|
||||
}
|
||||
if (this.props.stroke) {
|
||||
if (typeof this.props.stroke === 'string') {
|
||||
const stroke = { color: this.props.stroke, width: 1 }
|
||||
this.stroke = {
|
||||
top: stroke,
|
||||
left: stroke,
|
||||
right: stroke,
|
||||
bottom: stroke
|
||||
}
|
||||
} else if ('color' in this.props.stroke) {
|
||||
this.stroke = {
|
||||
top: this.props.stroke,
|
||||
left: this.props.stroke,
|
||||
right: this.props.stroke,
|
||||
bottom: this.props.stroke
|
||||
}
|
||||
} else {
|
||||
this.stroke = this.props.stroke
|
||||
}
|
||||
} else {
|
||||
this.stroke = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a rounded rectangle using the current state of the canvas.
|
||||
* If you omit the last three params, it will draw a rectangle
|
||||
* outline with a 5 pixel border radius
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {Number} x The top left x coordinate
|
||||
* @param {Number} y The top left y coordinate
|
||||
* @param {Number} width The width of the rectangle
|
||||
* @param {Number} height The height of the rectangle
|
||||
* @param {Number} [radius.tl = 0] Top left
|
||||
* @param {Number} [radius.tr = 0] Top right
|
||||
* @param {Number} [radius.br = 0] Bottom right
|
||||
* @param {Number} [radius.bl = 0] Bottom left
|
||||
* @param {Boolean} [fill = false] Whether to fill the rectangle.
|
||||
* @param {Boolean} [stroke = true] Whether to stroke the rectangle.
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
private roundRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
fill = false,
|
||||
stroke = true
|
||||
) {
|
||||
ctx.beginPath()
|
||||
// topLeft point
|
||||
ctx.moveTo(x + this.borderRadius.topLeft, y)
|
||||
// line to top right
|
||||
ctx.lineTo(x + width - this.borderRadius.topRight, y)
|
||||
// curve for top right
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + this.borderRadius.topRight)
|
||||
// line to bottom right
|
||||
ctx.lineTo(x + width, y + height - this.borderRadius.bottomRight)
|
||||
// curve for the bottom right
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - this.borderRadius.bottomRight, y + height)
|
||||
// line to bottom left
|
||||
ctx.lineTo(x + this.borderRadius.bottomLeft, y + height)
|
||||
// curve for bottom left
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - this.borderRadius.bottomLeft)
|
||||
// line to top left
|
||||
ctx.lineTo(x, y + this.borderRadius.topLeft)
|
||||
// curve for top left
|
||||
ctx.quadraticCurveTo(x, y, x + this.borderRadius.topLeft, y)
|
||||
// end path
|
||||
ctx.closePath()
|
||||
// fill rect
|
||||
if (fill) {
|
||||
ctx.fill()
|
||||
}
|
||||
// stroke rect
|
||||
if (stroke) {
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
// private drawStroke(
|
||||
// ctx: CanvasRenderingContext2D,
|
||||
// ge: GameEngine,
|
||||
// width: number,
|
||||
// height: number,
|
||||
// stroke: Stroke,
|
||||
// options: StrokeOptions
|
||||
// ) {
|
||||
// const borders = this.getBorders(stroke)
|
||||
// const firstBorderRadius = this.getRadius(width, height, borders[0])
|
||||
// const secondBorderRadius = this.getRadius(width, height, borders[1])
|
||||
|
||||
// ctx.lineWidth = ge.currentScene?.scale ?? 1
|
||||
|
||||
// ctx.stroke()
|
||||
// }
|
||||
|
||||
// private getStrokeSize(
|
||||
// width: number,
|
||||
// height: number,
|
||||
// stroke: Stroke
|
||||
// ): [[number, number], [number, number]] {
|
||||
// const borders = this.getBorders(stroke)
|
||||
// const firstBorderRadius = this.getRadius(width, height, borders[0])
|
||||
// const secondBorderRadius = this.getRadius(width, height, borders[1])
|
||||
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * get the borders for a specific stroke
|
||||
// *
|
||||
// * @param stroke the stroke to get border
|
||||
// * @returns the name of the borders of the stroke
|
||||
// */
|
||||
// private getBorders(stroke: Stroke): [Border, Border] {
|
||||
// switch (stroke) {
|
||||
// case 'top': return ['topLeft', 'topRight']
|
||||
// case 'bottom': return ['bottomLeft', 'bottomRight']
|
||||
// case 'left': return ['topLeft', 'bottomLeft']
|
||||
// case 'right': return ['topRight', 'bottomRight']
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * get the border radius for a specific border
|
||||
// *
|
||||
// * @param width the width of the rectangle
|
||||
// * @param height the height of the rectangle
|
||||
// * @param border the border to find he radius
|
||||
// * @returns the radius of the specified border
|
||||
// */
|
||||
// private getRadius(width: number, height: number, border: Border): number {
|
||||
// if (!this.borderRadius) {
|
||||
// return 0
|
||||
// }
|
||||
// const min = Math.min(width / 2, height / 2)
|
||||
// if (typeof this.borderRadius === 'number') {
|
||||
// return Math.min(this.borderRadius, min)
|
||||
// }
|
||||
// return Math.min(this.borderRadius[border] ?? 0)
|
||||
// }
|
||||
}
|
||||
|
@ -1,44 +1,64 @@
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
import GameEngine from 'GameEngine'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import Renderer from '.'
|
||||
import type GameEngine from '..'
|
||||
|
||||
interface Params {
|
||||
text?: string
|
||||
size?: number
|
||||
weight?: 'bold'
|
||||
stroke?: string | {color: string, width: number}
|
||||
color?: string
|
||||
align?: 'left' | 'center' | 'right'
|
||||
overrideSizeLimit?: boolean
|
||||
}
|
||||
|
||||
export default class TextRenderer extends Renderer {
|
||||
export default class TextRenderer extends Renderer<Params> {
|
||||
|
||||
public text?: string
|
||||
public size?: number
|
||||
public weight?: 'bold'
|
||||
public color?: string
|
||||
private width?: number
|
||||
|
||||
public constructor(component: Component2D, params?: Params) {
|
||||
super(component)
|
||||
objectLoop(params ?? {}, (v, k) => {this[k as 'text'] = v})
|
||||
public async getWidth() {
|
||||
return this.width ?? -1
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
const position = this.getPosition()
|
||||
const item: [number, number] = [
|
||||
// source x
|
||||
// 0 - 1.5 - -1.5
|
||||
position.x * (ge.caseSize.x),
|
||||
// source y
|
||||
position.y * (ge.caseSize.y)
|
||||
]
|
||||
await super.render(ge, ctx)
|
||||
const globalScale = ge.currentScene?.scale ?? 1
|
||||
const item = this.preRender(ctx, ge)
|
||||
|
||||
const size = this.component.scale.y * ge.caseSize.y
|
||||
|
||||
// console.log
|
||||
if (this.text) {
|
||||
ctx.fillStyle = this.color ?? 'black'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
|
||||
ctx.font = `${this.weight ? `${this.weight} ` : ''}${size + (this.size ?? 0)}px sans-serif`
|
||||
ctx.fillText(this.text, ...item)
|
||||
if (typeof this.props.text !== 'string') {
|
||||
if (this.debug) {
|
||||
console.warn('no text, no display')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.textAlign = this.props.align ?? 'left'
|
||||
let posX = item[0]
|
||||
if (this.props.align === 'center') {
|
||||
posX += item[2] / 2
|
||||
} else if (this.props.align === 'right') {
|
||||
posX += item[2]
|
||||
}
|
||||
ctx.font = `${this.props.weight ? `${this.props.weight} ` : ''}${(this.props.size ?? size) / 16 * ge.caseSize.x * globalScale * 3}px sans-serif`
|
||||
if (this.props.color) {
|
||||
ctx.fillStyle = this.props.color ?? 'black'
|
||||
ctx.fillText(this.props.text, posX, item[1], this.props.overrideSizeLimit ? undefined : item[2])
|
||||
}
|
||||
if (this.props.stroke) {
|
||||
if (typeof this.props.stroke === 'string') {
|
||||
ctx.strokeStyle = this.props.stroke
|
||||
ctx.lineWidth = ge.currentScene?.scale ?? 1
|
||||
} else {
|
||||
ctx.strokeStyle = this.props.stroke.color
|
||||
ctx.lineWidth = this.props.stroke.width * (ge.currentScene?.scale ?? 1)
|
||||
}
|
||||
ctx.strokeText(this.props.text, item[0], item[1], this.props.overrideSizeLimit ? undefined : item[2])
|
||||
}
|
||||
this.width = ctx.measureText(this.props.text).width / ge.caseSize.x / globalScale
|
||||
|
||||
this.postRender(ctx, ge)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
import GameEngine from 'GameEngine'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import Tileset from 'GameEngine/Tileset'
|
||||
import Renderer from '.'
|
||||
import type GameEngine from '..'
|
||||
import type Tileset from '../Tileset'
|
||||
|
||||
interface Params {
|
||||
tileset?: Tileset
|
||||
@ -13,30 +10,25 @@ interface Params {
|
||||
/**
|
||||
* TODO: Add origin support
|
||||
*/
|
||||
export default class TileRenderer extends Renderer implements Params {
|
||||
export default class TileRenderer extends Renderer<Params> {
|
||||
|
||||
public tileset?: Tileset
|
||||
public id?: number
|
||||
|
||||
public constructor(component: Component2D, params?: Params) {
|
||||
super(component)
|
||||
objectLoop(params ?? {}, (v, k) => {this[k as 'id'] = v})
|
||||
}
|
||||
|
||||
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
if (!this.tileset || typeof this.id !== 'number') {
|
||||
public override async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
|
||||
await super.render(ge, ctx)
|
||||
if (!this.props.tileset || typeof this.props.id !== 'number') {
|
||||
return
|
||||
}
|
||||
const {sx, sy} = this.tileset.getSourceData(this.id)
|
||||
const {sx, sy} = this.props.tileset.getSourceData(this.props.id)
|
||||
const position = this.getPosition()
|
||||
await this.props.tileset.asset.load()
|
||||
ctx.drawImage(
|
||||
await this.tileset.asset.get(),
|
||||
this.props.tileset.asset.get(),
|
||||
sx,
|
||||
sy,
|
||||
this.tileset.width(this.id),
|
||||
this.tileset.height(this.id),
|
||||
position.x * (ge.caseSize.x),
|
||||
position.y * (ge.caseSize.y),
|
||||
this.props.tileset.width(this.props.id),
|
||||
this.props.tileset.height(this.props.id),
|
||||
position.x * ge.caseSize.x,
|
||||
position.y * ge.caseSize.y,
|
||||
(this.component.scale.x ?? ge.caseSize.x) * ge.caseSize.x,
|
||||
(this.component.scale.y ?? ge.caseSize.y) * ge.caseSize.y
|
||||
)
|
||||
|
@ -1,25 +1,157 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
import GameEngine from '..'
|
||||
import Vector2D from '../2D/Vector2D'
|
||||
import Component2D from '../Component2D'
|
||||
import MathUtils from '../libs/MathUtils'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export default abstract class Renderer {
|
||||
public constructor(
|
||||
protected component: Component2D
|
||||
) {}
|
||||
export default abstract class Renderer<Props extends object = {}> {
|
||||
|
||||
protected getPosition(): Vector2D {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
const realPosition = ge.currentScene?.camera.topLeft.sum(this.component.position)
|
||||
if (!realPosition) {
|
||||
console.error('no camera?!?')
|
||||
return this.component.position
|
||||
}
|
||||
return new Vector2D(
|
||||
realPosition.x - this.component.scale.x / 2 - this.component.origin.x,
|
||||
realPosition.y - this.component.scale.y / 2 - this.component.origin.y
|
||||
)
|
||||
/**
|
||||
* set the renderer in debug mode
|
||||
*/
|
||||
public debug = false
|
||||
|
||||
public readonly props: Props
|
||||
|
||||
protected readonly defaultProps: Partial<Props> = {}
|
||||
|
||||
private oldProps?: Props
|
||||
private needUpdate = true
|
||||
|
||||
public constructor(
|
||||
public readonly component: Component2D,
|
||||
props: Props = {} as Props
|
||||
) {
|
||||
this.props = {...this.defaultProps, ...props}
|
||||
}
|
||||
|
||||
public abstract render(ge: GameEngine, ctx: CanvasRenderingContext2D): Promise<void>
|
||||
public setProps(newProps: Partial<Props>) {
|
||||
objectLoop(newProps as any, (value, obj) => {
|
||||
if (this.props[obj as keyof Props] === value) {
|
||||
return
|
||||
}
|
||||
this.props[obj as keyof Props] = value
|
||||
this.needUpdate = true
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async render(_ge: GameEngine, _ctx: CanvasRenderingContext2D): Promise<void> {
|
||||
if (this.needUpdate) {
|
||||
this.onUpdate?.(this.oldProps ?? this.props)
|
||||
this.needUpdate = false
|
||||
this.oldProps = this.props
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
||||
public onUpdate?(oldProps: Props): void
|
||||
|
||||
/**
|
||||
* return the position of the component with the camera offset applied
|
||||
* @param component the component to get the real position
|
||||
* @returns the position with the camera offset applied
|
||||
*/
|
||||
protected getPosition(component: Component2D = this.component): Vector2D {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
let originComponent = component
|
||||
while (originComponent.parent) {
|
||||
originComponent = originComponent.parent
|
||||
}
|
||||
const realPosition = component.getAbsolutePosition()
|
||||
.rotate(
|
||||
originComponent.getAbsolutePosition(),
|
||||
this.component.getAbsoluteRotation()
|
||||
)
|
||||
.sum(
|
||||
-(ge.currentScene?.position?.x ?? 0),
|
||||
-(ge.currentScene?.position?.y ?? 0)
|
||||
)
|
||||
|
||||
return realPosition
|
||||
}
|
||||
|
||||
/**
|
||||
* transform the position of the object to the real position on the canvas
|
||||
* @returns the position of the element and scale translated to the canvas positioning
|
||||
*/
|
||||
protected realPosition(): [number, number, number, number] {
|
||||
const position = this.getPosition()
|
||||
return [
|
||||
this.translateToCanvas('x', position.x),
|
||||
this.translateToCanvas('y', position.y),
|
||||
this.translateToCanvas('x', this.component.scale.x),
|
||||
this.translateToCanvas('y', this.component.scale.y)
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the component
|
||||
*
|
||||
* It needs to be closed by rotateFinish
|
||||
* @param ctx the context
|
||||
* @param rotation rotation in degrees
|
||||
*/
|
||||
protected rotateStart(ctx: CanvasRenderingContext2D, sizes: [number, number, number, number], rotation: number) {
|
||||
const radians = MathUtils.toRadians(rotation)
|
||||
ctx.setTransform(
|
||||
1, // Horizontal Scaling
|
||||
0, // Horizontal Skewing
|
||||
0, // Vertical Skewing
|
||||
1, // Vertical Scaling
|
||||
sizes[0], // Horizontal Moving
|
||||
sizes[1] // Vertical Moving
|
||||
)
|
||||
ctx.rotate(radians)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ctx the context
|
||||
* @param ge the gmeEngine
|
||||
* @param additionnalRotation additionnal rotation
|
||||
* @returns x, y, width, height
|
||||
*/
|
||||
protected preRender(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
ge: GameEngine,
|
||||
additionnalRotation = 0
|
||||
): [number, number, number, number] {
|
||||
let position = this.realPosition()
|
||||
|
||||
this.rotateStart(
|
||||
ctx,
|
||||
position,
|
||||
this.component.getAbsoluteRotation() + additionnalRotation
|
||||
)
|
||||
|
||||
position = [
|
||||
0,
|
||||
0,
|
||||
position[2],
|
||||
position[3]
|
||||
]
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
protected postRender(ctx: CanvasRenderingContext2D, ge: GameEngine) {
|
||||
|
||||
// handle rotation reset
|
||||
ctx.resetTransform()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ctx the context
|
||||
*/
|
||||
protected rotateFinish(ctx: CanvasRenderingContext2D) {
|
||||
ctx.resetTransform()
|
||||
}
|
||||
|
||||
protected translateToCanvas(axis: 'x' | 'y', point: number): number {
|
||||
const ge = GameEngine.getGameEngine()
|
||||
const globalScale = ge.currentScene?.scale ?? 1
|
||||
return point * ge.caseSize[axis] * globalScale
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,97 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
import Camera from './Components/Camera'
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable max-depth */
|
||||
import Listener from '@dzeio/listener'
|
||||
import GameEngine from '.'
|
||||
import Checker from './2D/Collider/Checker'
|
||||
import Vector2D from './2D/Vector2D'
|
||||
import Component2D, { ComponentState } from './Component2D'
|
||||
import Camera from './Components/Camera'
|
||||
|
||||
export default class Scene {
|
||||
export default class Scene extends Listener<{
|
||||
componentAdded: (component: Component2D) => void
|
||||
componentRemoved: (component: Component2D) => void
|
||||
}> {
|
||||
public static scenes: Record<string, Scene> = {}
|
||||
|
||||
public background?: string
|
||||
public id: string
|
||||
|
||||
public camera: Camera = new Camera()
|
||||
public position: Vector2D = new Vector2D(0)
|
||||
public scale = 1
|
||||
public globalRotation = 0
|
||||
public components: Array<Component2D> = []
|
||||
public camera?: Camera
|
||||
|
||||
private components: Array<Component2D> = []
|
||||
public readonly updatePerformances: Record<string, number> = {
|
||||
preparation: -1,
|
||||
init: -1,
|
||||
collision: -1,
|
||||
update: -1,
|
||||
render: -1,
|
||||
total: -1
|
||||
}
|
||||
|
||||
private componentsInitialized: Array<boolean> = []
|
||||
private ge!: GameEngine
|
||||
private hasClickedComponent: number | undefined
|
||||
|
||||
|
||||
public constructor(sceneId: string) {
|
||||
super()
|
||||
Scene.scenes[sceneId] = this
|
||||
this.id = sceneId
|
||||
}
|
||||
|
||||
public requireCamera() {
|
||||
if (!this.camera) {
|
||||
throw new Error('Camera not initialized')
|
||||
}
|
||||
return this.camera
|
||||
}
|
||||
|
||||
public addComponent(...cp: Array<Component2D>) {
|
||||
return this.components.push(...cp)
|
||||
if (!this.camera) {
|
||||
const cam = cp.find((it) => it instanceof Camera) as Camera | undefined
|
||||
this.camera = cam
|
||||
}
|
||||
this.componentsInitialized.push(...cp.map(() => false))
|
||||
const size = this.components.push(...cp)
|
||||
cp.forEach((it) => this.emit('componentAdded', it))
|
||||
return size
|
||||
}
|
||||
|
||||
public addComponentAt(index: number, ...cp: Array<Component2D>) {
|
||||
if (!this.camera) {
|
||||
const cam = cp.find((it) => it instanceof Camera) as Camera | undefined
|
||||
this.camera = cam
|
||||
}
|
||||
const initStart = this.componentsInitialized.slice(0, index)
|
||||
const initEnd = this.componentsInitialized.slice(index)
|
||||
this.componentsInitialized = [...initStart, ...cp.map(() => false), ...initEnd]
|
||||
const start = this.components.slice(0, index)
|
||||
const end = this.components.slice(index)
|
||||
this.components = [...start, ...cp, ...end]
|
||||
cp.forEach((it) => this.emit('componentAdded', it))
|
||||
}
|
||||
|
||||
public getComponents(): Array<Component2D> {
|
||||
return this.components
|
||||
}
|
||||
|
||||
/**
|
||||
* delete the component
|
||||
* @param component the component or component's index
|
||||
*/
|
||||
public removeComponent(component: number | Component2D): Scene {
|
||||
component = typeof component !== 'number' ? this.components.findIndex((it) => it.id === (component as Component2D).id) : component
|
||||
if (component !== -1) {
|
||||
const cp = this.components.splice(component, 1)
|
||||
this.emit('componentRemoved', cp[0])
|
||||
this.componentsInitialized.splice(component, 1)
|
||||
cp[0].destroy?.()
|
||||
} else {
|
||||
console.warn('component not found')
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setGameEngine(ge: GameEngine) {
|
||||
@ -30,75 +100,294 @@ export default class Scene {
|
||||
|
||||
public async init() {
|
||||
for await (const component of this.components) {
|
||||
await component.init?.()
|
||||
await this.initComponent(component)
|
||||
}
|
||||
}
|
||||
|
||||
// private frameNumber = 0
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public async update() {
|
||||
for (let index = 0; index < this.components.length; index++) {
|
||||
await this.updateComponent(this.components[index], index)
|
||||
// console.log('new scene frame', this.count++)
|
||||
|
||||
let now = window.performance.now()
|
||||
const componentList = this.flattenComponents()
|
||||
this.updatePerformances.preparation = window.performance.now() - now
|
||||
|
||||
now = window.performance.now()
|
||||
await Promise.all(this.components.map((it) => this.initComponent(it)))
|
||||
// for await (const component of this.components) {
|
||||
// await this.initComponent(component)
|
||||
// }
|
||||
this.updatePerformances.init = window.performance.now() - now
|
||||
|
||||
// enabled && !checked && (!pPosition || diffPosition || checkNeeded)
|
||||
now = window.performance.now()
|
||||
const filterFn = (it: Component2D) =>
|
||||
it.enabled &&
|
||||
!it.state.collisionChecked &&
|
||||
(!it.state.previousPosition || !it.getAbsolutePosition().equal(it.state.previousPosition) || it.state.collisionCheckNeeded)
|
||||
let componentsToCheck: Array<Component2D> = componentList.filter(filterFn)
|
||||
let doWhileLimit = 0
|
||||
do {
|
||||
// console.log(componentsToCheck)
|
||||
const futureComponents: Array<Component2D> = []
|
||||
componentsToCheck.forEach((it) => {
|
||||
const collisions = this.checkColisions(it)
|
||||
const oldCollisions = it.state.collisions ?? []
|
||||
it.setState('previousPosition', it.getAbsolutePosition().clone())
|
||||
it.setState('collisionChecked', true)
|
||||
it.setState('collisions', collisions)
|
||||
if (oldCollisions.length > 0|| collisions.length > 0) {
|
||||
futureComponents.push(...[...oldCollisions, ...collisions].filter((coll) => !futureComponents.includes(coll.component)).map((coll) => {
|
||||
coll.component.setState('collisionCheckNeeded', true)
|
||||
return coll.component
|
||||
}))
|
||||
}
|
||||
})
|
||||
componentsToCheck = futureComponents.filter(filterFn)
|
||||
} while (componentsToCheck.length > 0 && doWhileLimit++ < componentList.length)
|
||||
this.updatePerformances.collision = window.performance.now() - now
|
||||
|
||||
now = window.performance.now()
|
||||
for await (const component of componentList) {
|
||||
// const cpNow = window.performance.now()
|
||||
await this.updateComponent(component)
|
||||
// const time = window.performance.now() - cpNow
|
||||
// if (time > 0.15) {
|
||||
// this.updatePerformances[`cp-${component.id} ${component.constructor.name}`] = window.performance.now() - cpNow
|
||||
// }
|
||||
}
|
||||
componentList.forEach((it) => {
|
||||
it.setState('collisionCheckNeeded', false)
|
||||
it.setState('collisionChecked', false)
|
||||
})
|
||||
this.updatePerformances.update = window.performance.now() - now
|
||||
|
||||
now = window.performance.now()
|
||||
const camera = this.camera
|
||||
if (!camera) {
|
||||
return
|
||||
}
|
||||
const camXY = camera.getAbsolutePosition(false)
|
||||
const camPos = [camXY.clone(), camera.getScale().sum(camXY)]
|
||||
|
||||
const componentsToRender = componentList.filter((component) => {
|
||||
const selfXY = component.getAbsolutePosition(false)
|
||||
const selfPos = [selfXY, selfXY.sum(component.scale)]
|
||||
|
||||
// basic check
|
||||
return selfPos[1].x >= camPos[0].x && // self bottom higher than other top
|
||||
selfPos[0].x <= camPos[1].x &&
|
||||
selfPos[1].y >= camPos[0].y &&
|
||||
selfPos[0].y <= camPos[1].y
|
||||
}).sort((a, b) => a.getAbsoluteZIndex() - b.getAbsoluteZIndex())
|
||||
|
||||
for await (const component of componentsToRender) {
|
||||
await this.renderComponent(component)
|
||||
}
|
||||
this.updatePerformances.render = window.performance.now() - now
|
||||
this.updatePerformances.total =
|
||||
this.updatePerformances.preparation +
|
||||
this.updatePerformances.collision +
|
||||
this.updatePerformances.update +
|
||||
this.updatePerformances.render
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const component of this.components) {
|
||||
this.destroyComponent(component)
|
||||
}
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
for await (const component of this.components) {
|
||||
await component.destroy?.()
|
||||
}
|
||||
public flattenComponents(): Array<Component2D> {
|
||||
return this.components.map(this.flattenComponent).flat()
|
||||
}
|
||||
|
||||
private async updateComponent(v: Component2D, index: number) {
|
||||
const debug = v.debug
|
||||
if (debug) {
|
||||
console.log('Processing Component', v)
|
||||
public orderComponentsByZIndex(): Array<Component2D> {
|
||||
return this.flattenComponents().sort((a, b) => a.getAbsoluteZIndex() - b.getAbsoluteZIndex())
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public checkColisions(component: Component2D): NonNullable<Component2D['state']['collisions']> {
|
||||
if (!component.collider || !component.enabled) {
|
||||
return []
|
||||
}
|
||||
const state: Partial<ComponentState> = {}
|
||||
// const width = (v.width() ?? 1) * this.ge.caseSize[0]
|
||||
// const height = (v.height() ?? 1) * this.ge.caseSize[1]
|
||||
if (v.collider && v.collider.type === 'click' && (this.hasClickedComponent === index || !this.hasClickedComponent)) {
|
||||
if (v.collider.pointColliding(this.ge.cursor.position, 'click')) {
|
||||
if (this.ge.cursor.isDown && !this.ge.cursor.wasDown) {
|
||||
state.isColliding = 'click'
|
||||
this.hasClickedComponent = index
|
||||
} else if (this.ge.cursor.isDown) {
|
||||
state.isColliding = 'down'
|
||||
this.hasClickedComponent = index
|
||||
|
||||
const list: Component2D['state']['collisions'] = []
|
||||
|
||||
for (const otherComponent of this.flattenComponents()) {
|
||||
if (
|
||||
!otherComponent.enabled ||
|
||||
otherComponent.id === component.id ||
|
||||
!otherComponent.collider
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const colliders = Array.isArray(component.collider) ? component.collider : [component.collider]
|
||||
const otherColliders = Array.isArray(otherComponent.collider) ? otherComponent.collider : [otherComponent.collider]
|
||||
|
||||
for (const collider of colliders) {
|
||||
for (const otherCollider of otherColliders) {
|
||||
// Check for collision
|
||||
if (collider.tags === null || otherCollider.tags === null) {
|
||||
if (Checker.detectCollision(collider, otherCollider)) {
|
||||
const tags = Array.isArray(collider.tags) ? collider.tags : [collider.tags]
|
||||
for (const tag of tags) {
|
||||
list.push({
|
||||
collider: collider,
|
||||
component: otherComponent,
|
||||
tag: tag
|
||||
})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const colliderTypes: Array<string | undefined> = Array.isArray(collider.tags) ? collider.tags : [collider.tags]
|
||||
const otherColliderTypes: Array<string | undefined> = Array.isArray(otherCollider.tags) ? otherCollider.tags : [otherCollider.tags]
|
||||
|
||||
const tagIdx = colliderTypes.filter((it) => otherColliderTypes.includes(it))
|
||||
if (
|
||||
tagIdx.length > 0 &&
|
||||
Checker.detectCollision(collider, otherCollider)
|
||||
) {
|
||||
for (const tag of tagIdx) {
|
||||
list.push({
|
||||
collider: collider,
|
||||
component: otherComponent,
|
||||
tag: tag
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.hasClickedComponent === index && !state.isColliding) {
|
||||
this.hasClickedComponent = undefined
|
||||
}
|
||||
// if (v.pos) {
|
||||
// const ax = v.pos.x * this.ge.caseSize[0]
|
||||
// const ay = v.pos.y * this.ge.caseSize[1]
|
||||
// state.mouseHovering =
|
||||
// this.ge.cursor.x >= ax && this.ge.cursor.x < (ax + width) &&
|
||||
// this.ge.cursor.y >= ay && this.ge.cursor.y < (ay + height)
|
||||
// state.mouseClicking = state.mouseHovering && this.ge.cursor.isDown
|
||||
// state.mouseClicked = state.mouseClicking && !this.ge.cursor.wasDown
|
||||
// }
|
||||
if (v.update) {
|
||||
if (debug) {
|
||||
console.log('Updating Component', v)
|
||||
}
|
||||
v.update(state as ComponentState)
|
||||
}
|
||||
|
||||
if (v.renderer) {
|
||||
if (debug) {
|
||||
console.log('Rendering Component', v)
|
||||
}
|
||||
// console.log('is rendering new element')
|
||||
await v.renderer.render(this.ge, this.ge.ctx)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
if (v.childs) {
|
||||
if (debug) {
|
||||
console.log('Processing childs', v)
|
||||
/**
|
||||
* check if an element collide with a specific position
|
||||
* @param vector the position to check
|
||||
*/
|
||||
public at(pos: Vector2D, hasCollider = true) {
|
||||
return this.components.filter((it) => {
|
||||
if (hasCollider && !it.collider) {
|
||||
return false
|
||||
}
|
||||
for (let cIndex = 0; cIndex < v.childs.length; cIndex++) {
|
||||
await this.updateComponent(v.childs[cIndex], cIndex)
|
||||
return pos.isIn(it.position, it.position.sum(it.scale))
|
||||
})
|
||||
}
|
||||
|
||||
public in(pos: [Vector2D, Vector2D], hasCollider = true) {
|
||||
return this.components.filter((it) => {
|
||||
if (hasCollider && !it.collider) {
|
||||
return false
|
||||
}
|
||||
return Checker.posBoxBoxCollision(
|
||||
pos,
|
||||
[it.position, it.position.sum(it.scale)]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private flattenComponent = (component: Component2D): Array<Component2D> => {
|
||||
if (!component.enabled) {
|
||||
return []
|
||||
}
|
||||
if (!component.childs) {
|
||||
return [component]
|
||||
}
|
||||
return [component, ...component.childs.map(this.flattenComponent).flat()]
|
||||
}
|
||||
|
||||
private async initComponent(component: Component2D) {
|
||||
if (component.props.initialized) {
|
||||
return
|
||||
}
|
||||
if (component.init) {
|
||||
await component?.init()
|
||||
}
|
||||
component.setProps({initialized: true})
|
||||
|
||||
if (component.childs) {
|
||||
for await (const child of component.childs) {
|
||||
child.parent = component
|
||||
await this.initComponent(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific component
|
||||
*
|
||||
* note: It first update the childs THEN the component
|
||||
*
|
||||
* @param component the component to update
|
||||
* @returns the list of components to exclude in collision check
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
private async updateComponent(component: Component2D): Promise<void> {
|
||||
|
||||
if (!component.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
// update childs first
|
||||
// const toExclude: Array<Component2D> = []
|
||||
// if (component.childs && component.childs.length > 0) {
|
||||
// for await (const child of component.childs) {
|
||||
// toExclude.push(...await this.updateComponent(child))
|
||||
// }
|
||||
// }
|
||||
|
||||
const state: Partial<ComponentState> = component.state
|
||||
|
||||
component.setProps({scene: this})
|
||||
|
||||
if (component.update) {
|
||||
return component.update(state as ComponentState)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
private async renderComponent(component: Component2D) {
|
||||
if (!component.enabled) {
|
||||
return
|
||||
}
|
||||
const debug = component.debug
|
||||
if (debug) {
|
||||
console.group('rendering: ', component.id)
|
||||
}
|
||||
if (component.renderer) {
|
||||
if (debug) {
|
||||
console.log('rendering!')
|
||||
}
|
||||
// console.log('is rendering new element')
|
||||
await component.renderer.render(this.ge, this.ge.ctx)
|
||||
} else if (debug) {
|
||||
console.log('component has no renderer')
|
||||
}
|
||||
|
||||
if (component.debug) {
|
||||
console.groupEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private destroyComponent(component: Component2D) {
|
||||
for (const child of component.childs) {
|
||||
this.destroyComponent(child)
|
||||
}
|
||||
component.destroy?.()
|
||||
component.setProps({initialized: false})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (component.hasMoved) check collisions
|
||||
update item
|
||||
if (in camera) render it
|
||||
*/
|
||||
|
@ -32,7 +32,9 @@ export default class Tileset {
|
||||
}
|
||||
// const {x, y} = this.getPosFromId(id)
|
||||
const cols = Math.trunc(this.declaration.fileSize.width / this.width(id))
|
||||
// eslint-disable-next-line id-length
|
||||
const x = id % cols
|
||||
// eslint-disable-next-line id-length
|
||||
const y = Math.trunc(id / cols)
|
||||
const sx = x * this.width(id) + x * (this.declaration.spacing ?? 0)
|
||||
const sy = y * this.height(id) + y * (this.declaration.spacing ?? 0)
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { objectMap, objectValues } from '@dzeio/object-util'
|
||||
import Vector2D from './2D/Vector2D'
|
||||
import Scene from './Scene'
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Animation Engine
|
||||
* Camera fined control
|
||||
* Collision
|
||||
*/
|
||||
export default class GameEngine {
|
||||
@ -12,16 +12,9 @@ export default class GameEngine {
|
||||
public ctx: CanvasRenderingContext2D
|
||||
public canvas: HTMLCanvasElement
|
||||
public caseSize: Vector2D = new Vector2D(1, 1)
|
||||
public cursor: {
|
||||
position: Vector2D
|
||||
isDown: boolean
|
||||
wasDown: boolean
|
||||
} = {
|
||||
position: new Vector2D(0, 0),
|
||||
isDown: false,
|
||||
wasDown: false
|
||||
}
|
||||
public currentScene?: Scene
|
||||
public componentId = 0
|
||||
|
||||
public currentScene?: Scene | null
|
||||
|
||||
// last frame timestamp
|
||||
public lastFrame = 0
|
||||
@ -33,13 +26,18 @@ export default class GameEngine {
|
||||
*/
|
||||
public frameTime = 0
|
||||
|
||||
private isRunning = false
|
||||
/**
|
||||
* indicate if the engine is running
|
||||
*/
|
||||
public isRunning = false
|
||||
|
||||
|
||||
// timer between frames
|
||||
private timer = 0
|
||||
|
||||
private loopId?: number
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public constructor(
|
||||
id: string,
|
||||
public options?: {
|
||||
@ -49,11 +47,15 @@ export default class GameEngine {
|
||||
/**
|
||||
* Maximum framerate you want to achieve
|
||||
*
|
||||
* note: -1 mean infinite
|
||||
* note: -1/undefined mean infinite
|
||||
*/
|
||||
goalFramerate?: number
|
||||
}
|
||||
) {
|
||||
console.log('Setting up GameEngine')
|
||||
if (GameEngine.ge) {
|
||||
throw new Error('GameEngine already init')
|
||||
}
|
||||
GameEngine.ge = this
|
||||
const canvas = document.querySelector<HTMLCanvasElement>(id)
|
||||
if (!canvas) {
|
||||
@ -62,10 +64,12 @@ export default class GameEngine {
|
||||
this.canvas = canvas
|
||||
if (this.options?.caseCount) {
|
||||
this.caseSize = new Vector2D(
|
||||
// @ts-expect-error idc
|
||||
this.canvas.width / ((typeof this.options.caseCount) !== 'number' ? this.options.caseCount[0] : this.options.caseCount ),
|
||||
// @ts-expect-error idc2 lol
|
||||
this.canvas.height / ((typeof this.options.caseCount) !== 'number' ? this.options.caseCount[1] : this.options.caseCount)
|
||||
this.canvas.width / (
|
||||
typeof this.options.caseCount !== 'number' ? this.options.caseCount[0] : this.options.caseCount
|
||||
),
|
||||
this.canvas.height / (
|
||||
typeof this.options.caseCount !== 'number' ? this.options.caseCount[1] : this.options.caseCount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -73,7 +77,8 @@ export default class GameEngine {
|
||||
if (!ctx) {
|
||||
throw new Error('Error, Context could not get found!')
|
||||
}
|
||||
ctx.imageSmoothingEnabled = false
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
this.ctx = ctx
|
||||
|
||||
if (options?.goalFramerate && options.goalFramerate >= 0) {
|
||||
@ -82,7 +87,10 @@ export default class GameEngine {
|
||||
}
|
||||
|
||||
public static getGameEngine(): GameEngine {
|
||||
return this.ge
|
||||
// if (!this.ge) {
|
||||
// throw new Error('Game Engine not initialized!')
|
||||
// }
|
||||
return this.ge as GameEngine
|
||||
}
|
||||
|
||||
public start() {
|
||||
@ -91,94 +99,139 @@ export default class GameEngine {
|
||||
return
|
||||
}
|
||||
this.isRunning = true
|
||||
this.currentScene?.init().then(() => this.update())
|
||||
document.addEventListener('mousemove', (ev) => {
|
||||
this.cursor.position = new Vector2D(
|
||||
(ev.clientX + window.scrollX) / this.caseSize.x - (this.currentScene?.camera?.topLeft?.x ?? 0),
|
||||
(ev.clientY + window.scrollY) / this.caseSize.y - (this.currentScene?.camera?.topLeft?.y ?? 0)
|
||||
)
|
||||
if (this.cursor.isDown) {
|
||||
this.cursor.wasDown = true
|
||||
}
|
||||
})
|
||||
document.addEventListener('mousedown', () => {
|
||||
console.log('cursor down')
|
||||
this.cursor.isDown = true
|
||||
})
|
||||
document.addEventListener('mouseup', () => {
|
||||
console.log('cursor up')
|
||||
this.cursor.isDown = false
|
||||
this.cursor.wasDown = false
|
||||
})
|
||||
this.currentScene!.init().then(() => this.update())
|
||||
}
|
||||
|
||||
public pause() {
|
||||
this.isRunning = false
|
||||
}
|
||||
|
||||
public debugPerformances() {
|
||||
setInterval(() => {
|
||||
console.log(
|
||||
'\n',
|
||||
...objectMap(this.currentScene?.updatePerformances ?? {}, (v, k) => [k, v.toFixed(2) + 'ms\n']).flat()
|
||||
)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.isRunning = false
|
||||
for (const scene of objectValues(Scene.scenes)) {
|
||||
scene.destroy()
|
||||
}
|
||||
if (GameEngine.ge) {
|
||||
// @ts-expect-error normal behavior
|
||||
delete GameEngine.ge as any
|
||||
}
|
||||
if (this.loopId) {
|
||||
clearInterval(this.loopId)
|
||||
}
|
||||
}
|
||||
|
||||
public getXCaseCount(): number {
|
||||
const caseCount = this.options?.caseCount
|
||||
if (caseCount) {
|
||||
if (typeof caseCount === 'number') {
|
||||
return caseCount
|
||||
} else {
|
||||
return caseCount[0]
|
||||
}
|
||||
}
|
||||
return this.canvas.offsetWidth
|
||||
}
|
||||
|
||||
|
||||
public getYCaseCount(): number {
|
||||
const caseCount = this.options?.caseCount
|
||||
if (caseCount) {
|
||||
if (typeof caseCount === 'number') {
|
||||
return caseCount
|
||||
} else {
|
||||
return caseCount[1]
|
||||
}
|
||||
}
|
||||
return this.canvas.offsetWidth
|
||||
}
|
||||
public async setScene(scene: Scene | string) {
|
||||
console.log('Setting scene', typeof scene === 'string' ? scene : scene.id)
|
||||
const wasRunning = this.isRunning
|
||||
if (wasRunning) {
|
||||
this.isRunning = false
|
||||
}
|
||||
await this.currentScene?.destroy()
|
||||
await this.currentScene?.init()
|
||||
this.currentScene?.destroy()
|
||||
if (wasRunning) {
|
||||
this.isRunning = true
|
||||
}
|
||||
this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene
|
||||
const res = typeof scene === 'string' ? Scene.scenes[scene] : scene
|
||||
if (!res) {
|
||||
throw new Error('Scene not found!')
|
||||
}
|
||||
this.currentScene = res
|
||||
await this.currentScene?.init()
|
||||
this.currentScene.setGameEngine(this)
|
||||
}
|
||||
|
||||
private async update() {
|
||||
// console.log('update')
|
||||
let frameFinished = true
|
||||
setInterval((it) => {
|
||||
// get current time
|
||||
const now = window.performance.now()
|
||||
private update() {
|
||||
if (this.loopId) {
|
||||
console.error('Already initialized')
|
||||
return
|
||||
}
|
||||
|
||||
// game is not runnig, wait a frame
|
||||
if (!this.isRunning || !frameFinished) {
|
||||
// console.log('skip frame')
|
||||
// setTimeout(() => {
|
||||
// this.update()
|
||||
// }, this.timer)
|
||||
return
|
||||
}
|
||||
|
||||
// game is running too fast, wait until necessary
|
||||
if (this.lastFrame + this.timer > now ) {
|
||||
// console.log('skip frame')
|
||||
// setTimeout(() => {
|
||||
// this.update()
|
||||
// }, (this.lastFrame + this.timer) - now)
|
||||
return
|
||||
}
|
||||
// console.log('new frame')
|
||||
frameFinished = false
|
||||
|
||||
// if a background need to be drawn
|
||||
if (this.options?.background) {
|
||||
this.ctx.fillStyle = this.options.background
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
} else {
|
||||
// clear the previous frame
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
|
||||
// update scene
|
||||
this.currentScene?.update()
|
||||
|
||||
// calculate for next frame
|
||||
this.lastFrame = window.performance.now()
|
||||
this.frameTime = window.performance.now() - now
|
||||
frameFinished = true
|
||||
// this.update()
|
||||
// requestAnimationFrame(() => {
|
||||
// })
|
||||
// indicate if the main loop has started
|
||||
let run = false
|
||||
this.loopId = requestAnimationFrame(() => {
|
||||
run = true
|
||||
this.loop()
|
||||
})
|
||||
// sometime the main loop do not start so we need to try again
|
||||
setTimeout(() => {
|
||||
if (!run) {
|
||||
clearInterval(this.loopId)
|
||||
delete this.loopId
|
||||
this.update()
|
||||
}
|
||||
}, 20)
|
||||
}
|
||||
|
||||
private async loop() {
|
||||
// get current time
|
||||
const now = window.performance.now()
|
||||
|
||||
// game is not runnig, wait a frame
|
||||
if (!this.isRunning) {
|
||||
// console.log('skip frame 1')
|
||||
this.loopId = requestAnimationFrame(() => this.loop())
|
||||
return
|
||||
}
|
||||
|
||||
// game is running too fast, wait until necessary
|
||||
if (this.lastFrame + this.timer > now) {
|
||||
// console.log('skip frame 2')
|
||||
this.loopId = requestAnimationFrame(() => this.loop())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// console.log('new frame')
|
||||
|
||||
// if a background need to be drawn
|
||||
if (this.options?.background) {
|
||||
this.ctx.fillStyle = this.options.background
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
} else {
|
||||
// clear the previous frame
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
|
||||
await this.currentScene?.update()
|
||||
// calculate for next frame
|
||||
this.lastFrame = window.performance.now()
|
||||
this.frameTime = window.performance.now() - now
|
||||
|
||||
this.loopId = requestAnimationFrame(() => this.loop())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface GameState<UserState = any> {
|
||||
|
11
src/GameEngine/libs/CodeUtils.ts
Normal file
11
src/GameEngine/libs/CodeUtils.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Allow to quickly apply elements to item through fn
|
||||
*
|
||||
* @param item the variable to apply childs
|
||||
* @param fn the function to run
|
||||
* @returns item with elements modified from fn
|
||||
*/
|
||||
export function apply<T>(item: T, fn: (this: T, item: T) => void): T {
|
||||
fn.call(item, item)
|
||||
return item
|
||||
}
|
48
src/GameEngine/libs/MathUtils.ts
Normal file
48
src/GameEngine/libs/MathUtils.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export default class MathUtils {
|
||||
/**
|
||||
* round the value to the nearest value
|
||||
*
|
||||
* ex: [88, 45] round to 90, while [50, 45] round to 45
|
||||
* @param value the value to round
|
||||
* @param near the multiplier to near to
|
||||
* @returns the value closest to the nearest [near]
|
||||
*/
|
||||
public static roundToNearest(value: number, near: number): number {
|
||||
// get the remainder of the division
|
||||
const remainder = value % near
|
||||
// if remainder is 0 then no need to round
|
||||
if (remainder === 0 || near <= 0) {
|
||||
return value
|
||||
}
|
||||
// round down if value is less than near / 2
|
||||
if (remainder < near / 2) {
|
||||
return value - remainder
|
||||
}
|
||||
// round up
|
||||
return value - remainder + near
|
||||
}
|
||||
|
||||
/**
|
||||
* clamp the specified value between two other values
|
||||
* @param value the value to clamp
|
||||
* @param min the minimum value
|
||||
* @param max the maxmimum
|
||||
* @returns the value clamped between [min] and [max]
|
||||
*/
|
||||
public static clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
/**
|
||||
* transform degrees to radians
|
||||
* @param deg the value in degrees
|
||||
* @returns the value in radians
|
||||
*/
|
||||
public static toRadians(deg: number): number {
|
||||
deg = deg % 360
|
||||
if (deg < 0) {
|
||||
deg = 360 + deg
|
||||
}
|
||||
return deg * (Math.PI / 180)
|
||||
}
|
||||
}
|
7
src/assets/README.md
Normal file
7
src/assets/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Assets
|
||||
|
||||
Contains images that can be imported directly into the application
|
||||
|
||||
# Folder Architecture
|
||||
|
||||
- /assets/[path to element from src]/[folder named as the element]/[assets of the element].[ext]
|
BIN
src/assets/layouts/Base/favicon.png
Normal file
BIN
src/assets/layouts/Base/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
9
src/assets/layouts/Base/favicon.svg
Normal file
9
src/assets/layouts/Base/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 749 B |
4
src/client/styl/stylus.d.ts
vendored
4
src/client/styl/stylus.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
declare module '*.styl' {
|
||||
const content: any
|
||||
export = content
|
||||
}
|
24
src/components/Favicon/Favicon.astro
Normal file
24
src/components/Favicon/Favicon.astro
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
import { getImage } from 'astro:assets'
|
||||
|
||||
export interface Props {
|
||||
svg: ImageMetadata
|
||||
png: ImageMetadata
|
||||
icoPath?: string
|
||||
}
|
||||
|
||||
if (Astro.props.icoPath !== '/favicon.ico') {
|
||||
console.warn('It is recommanded that the ICO file should be located at /favicon.ico')
|
||||
|
||||
}
|
||||
|
||||
const appleTouch = await getImage({src: Astro.props.png, width: 180, height: 180})
|
||||
---
|
||||
|
||||
<>
|
||||
<link rel="icon" href={Astro.props.icoPath ?? "/favicon.ico"} sizes="any">
|
||||
<link rel="icon" href={Astro.props.svg.src} type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href={appleTouch.src} />
|
||||
<!-- Currently not integrated until I find a way to. -->
|
||||
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
|
||||
</>
|
36
src/components/Favicon/Manifest.ts
Normal file
36
src/components/Favicon/Manifest.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { getImage } from 'astro:assets'
|
||||
|
||||
export default class Manifest {
|
||||
static async create(baseImage: ImageMetadata, options: {
|
||||
name: string
|
||||
color?: string
|
||||
images?: Array<number>
|
||||
}) {
|
||||
const [
|
||||
i192,
|
||||
i512
|
||||
] = await Promise.all([
|
||||
getImage({src: baseImage, format: 'png', width: 192, height: 192}),
|
||||
getImage({src: baseImage, format: 'png', width: 512, height: 512})
|
||||
])
|
||||
return JSON.stringify({
|
||||
name: options.name,
|
||||
short_name: options.name,
|
||||
icons: [
|
||||
{
|
||||
src: i192.src,
|
||||
sizes: "192x192",
|
||||
type: "image/png"
|
||||
},
|
||||
{
|
||||
src: i512.src,
|
||||
sizes: "512x512",
|
||||
type: "image/png"
|
||||
}
|
||||
],
|
||||
theme_color: options.color ?? "#fff",
|
||||
background_color: options.color ?? "#fff",
|
||||
display: "standalone"
|
||||
})
|
||||
}
|
||||
}
|
4
src/components/Passthrough/Passthrough.astro
Normal file
4
src/components/Passthrough/Passthrough.astro
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
const json = JSON.stringify(Astro.props)
|
||||
---
|
||||
<script id="ASTRO_DATA" is:inline type="application/json" set:html={json}></script>
|
10
src/components/Passthrough/utils.ts
Normal file
10
src/components/Passthrough/utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* note: you MUST only pass simple items that can go in JSON format natively
|
||||
*/
|
||||
export function load<T extends {} = {}>(): T {
|
||||
const tag = document.querySelector<HTMLScriptElement>('#ASTRO_DATA')
|
||||
if (!tag) {
|
||||
throw new Error('could not load client variables, tag not found')
|
||||
}
|
||||
return JSON.parse(tag.innerText)
|
||||
}
|
42
src/components/Picture.astro.tmp
Normal file
42
src/components/Picture.astro.tmp
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
import { LocalImageProps, RemoteImageProps, getImage } from 'astro:assets'
|
||||
import AstroUtils from '../libs/AstroUtils'
|
||||
type ImageProps = LocalImageProps | RemoteImageProps
|
||||
export type Props = ImageProps
|
||||
|
||||
const res = await AstroUtils.wrap(async () => {
|
||||
const image = Astro.props.src
|
||||
const ext = typeof image === 'string' ? image.substring(image.lastIndexOf('.')) : image.format
|
||||
if (ext === 'svg') {
|
||||
return {
|
||||
format: 'raw',
|
||||
props: {
|
||||
...Astro.props,
|
||||
src: typeof image === 'string' ? image : image.src
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avif = await getImage({src: Astro.props.src, format: 'avif'})
|
||||
const webp = await getImage({src: Astro.props.src, format: 'webp'})
|
||||
const orig = await getImage({src: Astro.props.src, format: ext})
|
||||
|
||||
return {
|
||||
format: 'new',
|
||||
avif,
|
||||
webp,
|
||||
orig
|
||||
}
|
||||
})
|
||||
|
||||
---
|
||||
|
||||
{res.format === 'new' && (
|
||||
<picture class:list={[res.orig!.attributes.class, Astro.props.class]}>
|
||||
<source srcset={res.avif!.src} type="image/avif" />
|
||||
<source srcset={res.webp!.src} type="image/webp" />
|
||||
<img src={res.orig!.src} class="" {...res.orig!.attributes} />
|
||||
</picture>
|
||||
) || (
|
||||
<img {...res.props} />
|
||||
)}
|
3
src/components/README.md
Normal file
3
src/components/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Components
|
||||
|
||||
Contains big elements that can be reused by themselve
|
5
src/content/README.md
Normal file
5
src/content/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Content
|
||||
|
||||
Contains raw content for pages.
|
||||
|
||||
Mostly some static pages or blog posts.
|
15
src/content/config.ts
Normal file
15
src/content/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// 1. Import utilities from `astro:content`
|
||||
// import { defineCollection, z } from 'astro:content'
|
||||
|
||||
// 2. Define your collection(s)
|
||||
// const docsCollection = defineCollection({
|
||||
// type: 'content',
|
||||
// schema: z.object({
|
||||
// title: z.string()
|
||||
// })
|
||||
// })
|
||||
// 3. Export a single `collections` object to register your collection(s)
|
||||
// This key should match your collection directory name in "src/content"
|
||||
// export const collections = {
|
||||
// 'docs': docsCollection,
|
||||
// };
|
23
src/env.d.ts
vendored
Normal file
23
src/env.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client-image" />
|
||||
/// <reference path="./libs/ResponseBuilder" />
|
||||
|
||||
/**
|
||||
* Environment variables declaration
|
||||
*/
|
||||
interface ImportMetaEnv {
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
|
||||
declare namespace App {
|
||||
/**
|
||||
* Middlewares variables
|
||||
*/
|
||||
interface Locals {
|
||||
responseBuilder: ResponseBuilder
|
||||
}
|
||||
}
|
304
src/games/PokemonShuffle/index.ts
Normal file
304
src/games/PokemonShuffle/index.ts
Normal file
@ -0,0 +1,304 @@
|
||||
import States from '../../libs/States'
|
||||
|
||||
export interface Cell {
|
||||
// cell pokemon ID
|
||||
id: number
|
||||
// is cell part of an horizontal combo
|
||||
horizontalCombo?: boolean
|
||||
// is cell part of a vertical combo
|
||||
verticalCombo?: boolean
|
||||
// has cell just spawned?
|
||||
justSpawned?: boolean
|
||||
// is cell falling
|
||||
isFalling?: boolean
|
||||
}
|
||||
|
||||
export default class PokemonShuffle {
|
||||
public gameState: States<{
|
||||
cells: Array<Array<Cell | null>>
|
||||
// boss damages
|
||||
damage: number
|
||||
}> = new States({
|
||||
cells: [[]] as any,
|
||||
damage: 0
|
||||
})
|
||||
|
||||
private comboMax: number = 0
|
||||
public constructor(
|
||||
private boardSize: number = 6,
|
||||
private pokemonCount: number = 5,
|
||||
public readonly boss: {
|
||||
health: number
|
||||
id: number
|
||||
}
|
||||
) {
|
||||
this.gameState.setProps({cells: this.generate()})
|
||||
}
|
||||
|
||||
public async moveCell(from: [number, number], to: [number, number]) {
|
||||
|
||||
// move cells
|
||||
const cellFrom = this.gameState.props.cells[from[0]]![from[1]] ?? null
|
||||
const cellTo = this.gameState.props.cells[to[0]]![to[1]] ?? null
|
||||
|
||||
this.gameState.props.cells[from[0]]![from[1]] = cellTo
|
||||
this.gameState.props.cells[to[0]]![to[1]] = cellFrom
|
||||
|
||||
|
||||
// run the main loop
|
||||
let newScore = 0
|
||||
let runCheckLoop = true
|
||||
while (runCheckLoop) {
|
||||
const scoreMade = this.calculateGame()
|
||||
runCheckLoop = scoreMade > 0
|
||||
if (scoreMade > 0) {
|
||||
await this.updateGameState()
|
||||
}
|
||||
newScore += scoreMade
|
||||
}
|
||||
|
||||
// revert if no combo were made
|
||||
if (newScore === 0) {
|
||||
this.gameState.props.cells[from[0]]![from[1]] = cellFrom
|
||||
this.gameState.props.cells[to[0]]![to[1]] = cellTo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* generate a new game
|
||||
*/
|
||||
private generate(): Array<Array<Cell | null>> {
|
||||
return Array
|
||||
.from(Array(this.boardSize))
|
||||
.map(
|
||||
() => Array.from(Array(this.boardSize))
|
||||
.map(() => ({ id: this.randomPokemon(0, this.pokemonCount) }))
|
||||
)
|
||||
}
|
||||
|
||||
private randomPokemon(min = 0, max = 100): number {
|
||||
let id = -1
|
||||
do {
|
||||
id = Math.floor(Math.random() * (max - min) + min)
|
||||
// dont return 1 as it is the `?`
|
||||
} while (id < 0 || id === 1)
|
||||
return id
|
||||
}
|
||||
|
||||
private async updateGameState() {
|
||||
// remove combos
|
||||
const cells = this.gameState.props.cells.map((row) => row.map((cell) => {
|
||||
if (!cell) {
|
||||
return null
|
||||
}
|
||||
return cell
|
||||
}))
|
||||
|
||||
let needContinue = true
|
||||
let hadTurn = false
|
||||
while (needContinue) {
|
||||
console.log('running calculate')
|
||||
// Make items fall
|
||||
needContinue = false
|
||||
for (let rowIdx = (cells.length - 1); rowIdx >= 0; rowIdx--) {
|
||||
const row = cells[rowIdx] ?? []
|
||||
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
||||
const cell = row[colIdx]
|
||||
if (cell) {
|
||||
cell.justSpawned = false
|
||||
cell.isFalling = false
|
||||
}
|
||||
if (cell && rowIdx+1 < row.length && !cells[rowIdx+1]?.[colIdx]) {
|
||||
cell.isFalling = true
|
||||
needContinue = true
|
||||
console.log('making', rowIdx, colIdx, 'fall')
|
||||
// Move cell down
|
||||
cells![rowIdx+1]![colIdx] = cell
|
||||
cells![rowIdx]![colIdx] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the top lane
|
||||
for (let col = 0; col < cells[0]!.length; col++) {
|
||||
const cell = cells![0]![col]
|
||||
if (!cell) {
|
||||
console.log('filling to lane at', col)
|
||||
needContinue = true
|
||||
cells[0]![col] = {id: this.randomPokemon(0, this.pokemonCount), justSpawned: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to wait for the falling animation
|
||||
if (needContinue) {
|
||||
this.gameState.setProps({
|
||||
cells: cells,
|
||||
// hitBoss: false
|
||||
}, {force: true})
|
||||
await this.wait(300)
|
||||
}
|
||||
|
||||
// Clear items
|
||||
let hasCleared = false
|
||||
for (const row of cells) {
|
||||
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
||||
const cell = row[colIdx]
|
||||
if (!cell || (!cell.horizontalCombo && !cell.verticalCombo)) {continue}
|
||||
row[colIdx] = null
|
||||
hasCleared = true
|
||||
needContinue = true
|
||||
console.log('clearing', colIdx)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCleared/* && !initial*/) {
|
||||
await this.wait(500)
|
||||
}
|
||||
hadTurn = true
|
||||
}
|
||||
return hadTurn
|
||||
}
|
||||
|
||||
private wait(time: number): Promise<void> {
|
||||
return new Promise((res) => setTimeout(() => res(), time))
|
||||
}
|
||||
|
||||
private calculateGame() {
|
||||
const items = this.gameState.props.cells
|
||||
|
||||
// the number of matches found
|
||||
let matches = 0
|
||||
|
||||
// the score the calculation made
|
||||
let newPoints = 0
|
||||
|
||||
// the current combo
|
||||
let combo = 0
|
||||
|
||||
// loop through each rows
|
||||
for (let rowIdx = 0; rowIdx < items.length; rowIdx++) {
|
||||
const row = items[rowIdx] ?? []
|
||||
|
||||
// loop through each cols
|
||||
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
||||
const cell = row[colIdx]
|
||||
|
||||
// skip if there is no cell
|
||||
if (!cell) {
|
||||
continue
|
||||
}
|
||||
|
||||
// get the pokemon ID
|
||||
const id = cell.id
|
||||
|
||||
// Checkup combos horizontally
|
||||
if (!cell.horizontalCombo && !(cell.isFalling || cell.justSpawned)) {
|
||||
// the number of time it is seen
|
||||
let count = 0
|
||||
while((colIdx + ++count) < items.length) {
|
||||
// check the card below
|
||||
const tmp = row[colIdx + count]
|
||||
if (!tmp || tmp.id !== id || tmp.isFalling || tmp.justSpawned) {break}
|
||||
}
|
||||
if (count >= 3) {
|
||||
matches += 1
|
||||
for (let i = colIdx; i < (colIdx + count); i++) {
|
||||
const tmp = items[rowIdx]![i]
|
||||
if (!tmp) {continue}
|
||||
tmp.horizontalCombo = true
|
||||
}
|
||||
newPoints += this.calculateMatch(count, combo)
|
||||
}
|
||||
}
|
||||
// Checkup combos Vertical
|
||||
if (!cell.verticalCombo && !(cell.isFalling || cell.justSpawned)) {
|
||||
let count = 0
|
||||
while((rowIdx + ++count) < items.length) {
|
||||
const tmp = items[rowIdx + count]![colIdx]
|
||||
if (!tmp || tmp.id !== id || tmp.isFalling || tmp.justSpawned) {break}
|
||||
}
|
||||
if (count >= 3) {
|
||||
matches += 1
|
||||
for (let i = rowIdx; i < (rowIdx + count); i++) {
|
||||
const tmp = items[i]![colIdx]
|
||||
if (!tmp) {continue}
|
||||
tmp.verticalCombo = true
|
||||
}
|
||||
newPoints += this.calculateMatch(count, combo)
|
||||
// console.log(colIdx, rowIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If combos were found
|
||||
if (matches) {
|
||||
combo += matches
|
||||
const comboMax = Math.max(this.comboMax, combo)
|
||||
// if (comboMax === combo && !initial) {
|
||||
// window.localStorage.setItem('pokemon-shuffle/comboMax', comboMax.toString())
|
||||
// }
|
||||
this.gameState.setProps({
|
||||
cells: items
|
||||
// damage: state.props.damage ?? 0 + newPoints,
|
||||
// combo,
|
||||
// comboMax,
|
||||
// hitBoss: true
|
||||
}, { force: true })
|
||||
}
|
||||
return newPoints
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a score for a specific match
|
||||
*
|
||||
* @param len the length of the match
|
||||
* @param combo the current combo count
|
||||
* @returns the score for the specific match
|
||||
*/
|
||||
private calculateMatch(len: number, combo: number) {
|
||||
let score = (len - 2) * 40 // currently the damage
|
||||
if (len > 3) {
|
||||
switch (len) {
|
||||
case 4:
|
||||
score *= 1.5
|
||||
break
|
||||
case 5:
|
||||
score *= 2
|
||||
break
|
||||
case 6:
|
||||
score *= 3
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if (combo > 1) {
|
||||
if (combo >= 2 && combo <= 4) {
|
||||
score *= 1.1
|
||||
}
|
||||
if (combo >= 5 && combo <= 9) {
|
||||
score *= 1.15
|
||||
}
|
||||
if (combo >= 10 && combo <= 24) {
|
||||
score *= 1.2
|
||||
}
|
||||
if (combo >= 25 && combo <= 49) {
|
||||
score *= 1.3
|
||||
}
|
||||
if (combo >= 50 && combo <= 74) {
|
||||
score *= 1.4
|
||||
}
|
||||
if (combo >= 75 && combo <= 99) {
|
||||
score *= 1.5
|
||||
}
|
||||
if (combo >= 100 && combo <= 199) {
|
||||
score *= 2
|
||||
}
|
||||
if (combo >= 200) {
|
||||
score *= 2.5
|
||||
}
|
||||
}
|
||||
return score
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { stat } from 'fs'
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
|
||||
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import Renderer from 'GameEngine/Renderer'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import { globalState } from '.'
|
||||
import Cursor from './Cursor'
|
||||
import Space from './Space'
|
||||
import TextComponent from './TextComponent'
|
||||
|
||||
export default class Button extends Component2D {
|
||||
private static isOnCursor = false
|
||||
|
||||
public size = {
|
||||
width: 3, height: 1
|
||||
}
|
||||
|
||||
public renderer: RectRenderer = new RectRenderer(this)
|
||||
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
|
||||
|
||||
public position: Vector2D = new Vector2D(10, 5)
|
||||
|
||||
public scale: Vector2D = new Vector2D(20, 10)
|
||||
|
||||
public constructor() {
|
||||
super()
|
||||
|
||||
this.childs = [
|
||||
new TextComponent(this, 'remèttre à zéro', 'bold', 10, 'white')
|
||||
]
|
||||
this.renderer.material = 'black'
|
||||
}
|
||||
|
||||
public update(state: ComponentState): void | Promise<void> {
|
||||
if (state.isColliding === 'click' || state.isColliding === 'down') {
|
||||
Space.shouldReset = true
|
||||
globalState.x10Moved = 0
|
||||
globalState.x20Moved = 0
|
||||
} else {
|
||||
Space.shouldReset = false
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import { stat } from 'fs'
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import Renderer from 'GameEngine/Renderer'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
|
||||
export default class Cursor extends Component2D {
|
||||
|
||||
public renderer: RectRenderer = new RectRenderer(this)
|
||||
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
|
||||
|
||||
public position: Vector2D = new Vector2D(0, 0)
|
||||
|
||||
public scale: Vector2D = new Vector2D(1, 1)
|
||||
|
||||
// public origin: Vector2D = new Vector2D(0.5, 0.5)
|
||||
|
||||
public update(state: ComponentState): void | Promise<void> {
|
||||
// state.mouseHovering
|
||||
|
||||
this.renderer.material = 'blue'
|
||||
const cursor = GameEngine.getGameEngine().cursor
|
||||
this.position = cursor.position
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import { globalState } from '.'
|
||||
import Cursor from './Cursor'
|
||||
import TextComponent from './TextComponent'
|
||||
|
||||
export default class Space extends Component2D {
|
||||
public static shouldReset = false
|
||||
|
||||
public size = {
|
||||
width: 3, height: 1
|
||||
}
|
||||
|
||||
public renderer: RectRenderer = new RectRenderer(this)
|
||||
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
|
||||
|
||||
public position: Vector2D = new Vector2D(0, 0)
|
||||
|
||||
public scale: Vector2D = new Vector2D(30, 10)
|
||||
|
||||
private posBeforeCursor: Vector2D | null = null
|
||||
private basePosition: Vector2D
|
||||
|
||||
private hasMoved = false
|
||||
|
||||
public constructor(
|
||||
position: Vector2D,
|
||||
size: Vector2D,
|
||||
private cursor: Cursor,
|
||||
private placeableRects: Array<[Vector2D, Vector2D]>,
|
||||
private log: boolean = false
|
||||
) {
|
||||
super()
|
||||
this.position = position
|
||||
this.basePosition = position
|
||||
// this.debug = true
|
||||
this.scale = size
|
||||
|
||||
const text = `${size.x}x${size.y}`
|
||||
this.childs = [
|
||||
new TextComponent(this, text, 'bold', 8, 'blue')
|
||||
]
|
||||
}
|
||||
|
||||
public update(state: ComponentState): void | Promise<void> {
|
||||
// state.mouseHovering
|
||||
const point = this.scale.div(2).sub(this.position)
|
||||
|
||||
this.renderer.material = 'white'
|
||||
this.renderer.stroke = {color: 'black', width: 3}
|
||||
const cursor = GameEngine.getGameEngine().cursor
|
||||
if (this.log) console.log(Space.shouldReset)
|
||||
if (state.isColliding === 'click' || state.isColliding === 'down') {
|
||||
if (!this.hasMoved) {
|
||||
this.hasMoved = true
|
||||
if (this.scale.x === 10) {
|
||||
globalState.x10Moved ++
|
||||
} else {
|
||||
globalState.x20Moved ++
|
||||
}
|
||||
}
|
||||
this.renderer.stroke = {color: 'green', width: 3}
|
||||
if (!this.posBeforeCursor) {
|
||||
this.posBeforeCursor = this.position
|
||||
}
|
||||
// console.log('follow cursor', cursor.position, this.position)
|
||||
this.position = cursor.position
|
||||
let canPlace = false
|
||||
for (const placeableRect of this.placeableRects) {
|
||||
if (point.isIn(placeableRect[0], this.scale.sub(placeableRect[1]))) {
|
||||
canPlace = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!canPlace) {
|
||||
this.renderer.stroke = {color: 'red', width: 3}
|
||||
}
|
||||
else {
|
||||
this.posBeforeCursor = this.position
|
||||
}
|
||||
} else if (this.posBeforeCursor) {
|
||||
const futurePosition = this.position.decimalCount(0)
|
||||
let canPlace = false
|
||||
for (const placeableRect of this.placeableRects) {
|
||||
if (point.isIn(placeableRect[0], this.scale.sub(placeableRect[1]))) {
|
||||
canPlace = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!canPlace) {
|
||||
this.position = this.posBeforeCursor.decimalCount(0)
|
||||
} else {
|
||||
this.position = futurePosition
|
||||
}
|
||||
this.posBeforeCursor = null
|
||||
}
|
||||
|
||||
if (Space.shouldReset) {
|
||||
this.position = this.basePosition
|
||||
this.hasMoved = false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import { stat } from 'fs'
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
|
||||
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import Renderer from 'GameEngine/Renderer'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
|
||||
import { globalState } from '.'
|
||||
import Cursor from './Cursor'
|
||||
|
||||
export default class Text extends Component2D {
|
||||
|
||||
|
||||
public renderer: TextRenderer = new TextRenderer(this)
|
||||
|
||||
// public position: Vector2D = new Vector2D(0, 0)
|
||||
|
||||
public constructor(private type: keyof typeof globalState, weight?: 'bold', size?: number, color?: string) {
|
||||
super()
|
||||
switch (type) {
|
||||
case 'x10Moved':
|
||||
this.position = new Vector2D(10, 40)
|
||||
break
|
||||
case 'x20Moved':
|
||||
this.position = new Vector2D(10, 43)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
this.renderer.text = 'nique'
|
||||
this.renderer.weight = weight
|
||||
this.renderer.size = size
|
||||
this.renderer.color = color
|
||||
}
|
||||
|
||||
public update(state: ComponentState): void | Promise<void> {
|
||||
switch (this.type) {
|
||||
case 'x10Moved':
|
||||
this.renderer.text = '10x20: ' + (5 - globalState.x10Moved)
|
||||
break
|
||||
case 'x20Moved':
|
||||
this.renderer.text = '10x20: ' + (5 - globalState.x20Moved)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import { stat } from 'fs'
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
|
||||
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import Renderer from 'GameEngine/Renderer'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
|
||||
import Cursor from './Cursor'
|
||||
|
||||
export default class TextComponent extends Component2D {
|
||||
|
||||
|
||||
public renderer: TextRenderer = new TextRenderer(this)
|
||||
|
||||
// public position: Vector2D = new Vector2D(0, 0)
|
||||
|
||||
public constructor(private parent: Component2D, text: string, weight?: 'bold', size?: number, color?: string) {
|
||||
super()
|
||||
this.renderer.text = text
|
||||
this.renderer.weight = weight
|
||||
this.renderer.size = size
|
||||
this.renderer.color = color
|
||||
}
|
||||
|
||||
public update(state: ComponentState): void | Promise<void> {
|
||||
this.position = this.parent.position
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export const globalState: {
|
||||
x20Moved: number
|
||||
x10Moved: number
|
||||
} = {
|
||||
x10Moved: 0,
|
||||
x20Moved: 0
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Asset from 'GameEngine/Asset'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import TileRenderer from 'GameEngine/Renderer/TileRenderer'
|
||||
import SoundManager from 'GameEngine/SoundManager'
|
||||
import Tileset from 'GameEngine/Tileset'
|
||||
import type Vector2D from '../../GameEngine/2D/Vector2D'
|
||||
import Asset from '../../GameEngine/Asset'
|
||||
import Component2D from '../../GameEngine/Component2D'
|
||||
import TileRenderer from '../../GameEngine/Renderer/TileRenderer'
|
||||
import SoundManager from '../../GameEngine/SoundManager'
|
||||
import Tileset from '../../GameEngine/Tileset'
|
||||
|
||||
export class Explosion extends Component2D {
|
||||
|
||||
@ -12,6 +12,8 @@ export class Explosion extends Component2D {
|
||||
height: .9
|
||||
}
|
||||
|
||||
public ended = true
|
||||
|
||||
// public origin = new Vector2D(-.5, -.5)
|
||||
|
||||
private explosionTileset = new Tileset(Asset.init('/assets/tictactoe/explosion.png'), {
|
||||
@ -22,7 +24,7 @@ export class Explosion extends Component2D {
|
||||
private n = 0
|
||||
|
||||
|
||||
public update() {
|
||||
public override update() {
|
||||
if (this.animationNumber !== -1 && this.n++ >= 12) {
|
||||
this.renderer = new TileRenderer(this, {
|
||||
id: this.animationNumber++,
|
||||
@ -31,7 +33,8 @@ export class Explosion extends Component2D {
|
||||
this.n = 0
|
||||
if (this.animationNumber > 5) {
|
||||
this.animationNumber = -1
|
||||
this.renderer = undefined
|
||||
this.renderer = null
|
||||
this.ended = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,5 +43,6 @@ export class Explosion extends Component2D {
|
||||
new SoundManager('/assets/tictactoe/explosion.wav').play()
|
||||
this.position = pos
|
||||
this.animationNumber = 0
|
||||
this.ended = false
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
|
||||
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Asset from 'GameEngine/Asset'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import TileRenderer from 'GameEngine/Renderer/TileRenderer'
|
||||
import SoundManager from 'GameEngine/SoundManager'
|
||||
import Tileset from 'GameEngine/Tileset'
|
||||
import { globalState } from '.'
|
||||
import GameEngine from '../../GameEngine'
|
||||
import BoxCollider2D from '../../GameEngine/2D/Collider/BoxCollider2D'
|
||||
import ColliderDebugger from '../../GameEngine/2D/Debug/ColliderDebugger'
|
||||
import PointDebugger from '../../GameEngine/2D/Debug/PointDebugger'
|
||||
import Vector2D from '../../GameEngine/2D/Vector2D'
|
||||
import Asset from '../../GameEngine/Asset'
|
||||
import Component2D, { type ComponentState } from '../../GameEngine/Component2D'
|
||||
import Cursor from '../../GameEngine/Components/Cursor'
|
||||
import TileRenderer from '../../GameEngine/Renderer/TileRenderer'
|
||||
import SoundManager from '../../GameEngine/SoundManager'
|
||||
import Tileset from '../../GameEngine/Tileset'
|
||||
import { Explosion } from './Explosion'
|
||||
|
||||
export default class Item extends Component2D {
|
||||
|
||||
public static explosion = new Explosion()
|
||||
|
||||
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
|
||||
public override collider: BoxCollider2D = new BoxCollider2D(this, {
|
||||
tags: 'cursor'
|
||||
})
|
||||
|
||||
private x: number
|
||||
private y: number
|
||||
@ -32,53 +34,58 @@ export default class Item extends Component2D {
|
||||
this.x = Math.trunc(index % 3)
|
||||
this.y = Math.trunc(index / 3)
|
||||
this.position = new Vector2D(
|
||||
this.x,
|
||||
this.y
|
||||
this.x + 0.025 * this.x,
|
||||
this.y + 0.025 * this.y
|
||||
)
|
||||
this.scale = new Vector2D(
|
||||
.9, .9
|
||||
.95, .95
|
||||
)
|
||||
// this.renderer = new RectRenderer(this, {material: 'green'})
|
||||
this.renderer = new TileRenderer(this, {tileset: this.tileset, id: -1})
|
||||
// console.log(this.tileset.getSourceData(0), this.tileset.width(0), this.tileset.height(0))
|
||||
// console.log(this.tileset.getSourceData(1))
|
||||
this.childs = [
|
||||
new ColliderDebugger(this, this.collider),
|
||||
new PointDebugger(this.collider.pos()[0]),
|
||||
new PointDebugger(this.collider.pos()[1])
|
||||
new ColliderDebugger(),
|
||||
new PointDebugger({
|
||||
point: this.collider.pos()[0],
|
||||
size: .1
|
||||
}),
|
||||
new PointDebugger({
|
||||
point: this.collider.pos()[1],
|
||||
color: 'green',
|
||||
size: .1
|
||||
})
|
||||
]
|
||||
// console.log(index)
|
||||
}
|
||||
|
||||
public init() {
|
||||
console.log('item initialized')
|
||||
}
|
||||
|
||||
public async update(state: ComponentState) {
|
||||
public override async update(state: ComponentState) {
|
||||
if (!globalState.isPlaying) {
|
||||
return
|
||||
}
|
||||
// console.log(state)
|
||||
const value: '' | 'X' | 'O' = globalState.gameState[this.x][this.y] as '' | 'X' | 'O'
|
||||
if (state.isColliding === 'click' && value === '') {
|
||||
const cursor = state.collisions?.find((it) => it.component instanceof Cursor)?.component as Cursor | undefined
|
||||
const value: '' | 'X' | 'O' = globalState.gameState?.[this.x]?.[this.y] as '' | 'X' | 'O'
|
||||
if (!cursor?.isDown && cursor?.wasDown) {
|
||||
// console.log('hovering')
|
||||
await this.onClick()
|
||||
}
|
||||
|
||||
if (value === 'X') {
|
||||
(this.renderer as TileRenderer).id = 1
|
||||
this.renderer?.setProps({id: 1})
|
||||
// this.renderer = new ImageRenderer(this, Asset.init('/assets/tictactoe/X.png'))
|
||||
} else if(value === 'O') {
|
||||
(this.renderer as TileRenderer).id = 0
|
||||
this.renderer?.setProps({id: 0})
|
||||
} else {
|
||||
(this.renderer as TileRenderer).id = -1
|
||||
this.renderer?.setProps({id: -1})
|
||||
}
|
||||
}
|
||||
|
||||
private async onClick() {
|
||||
console.log(this.position)
|
||||
const clickSound = new SoundManager('/assets/tictactoe/bip.wav')
|
||||
clickSound.play()
|
||||
globalState.gameState[this.x][this.y] = globalState.playerTurn
|
||||
globalState.gameState![this.x]![this.y] = globalState.playerTurn
|
||||
console.log(this.checkVictory())
|
||||
if (this.checkVictory()) {
|
||||
Item.explosion.run(this.position)
|
||||
|
@ -1,18 +1,15 @@
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import Vector2D from '../../GameEngine/2D/Vector2D'
|
||||
import Component2D from '../../GameEngine/Component2D'
|
||||
import RectRenderer from '../../GameEngine/Renderer/RectRenderer'
|
||||
|
||||
export default class Line extends Component2D {
|
||||
|
||||
// public debug = true
|
||||
|
||||
public constructor(direction: number, index: number) {
|
||||
super()
|
||||
this.renderer = new RectRenderer(this, {material: 'orange'})
|
||||
|
||||
this.position = new Vector2D(
|
||||
direction ? index ? 1.5 : 0.5 : 1,
|
||||
direction ? 1 : index ? 0.5 : 1.5
|
||||
direction ? index ? 1.95 : 0.95 : 0,
|
||||
direction ? 0 : index ? 1.95 : 0.95
|
||||
)
|
||||
|
||||
this.scale = new Vector2D(
|
||||
@ -27,5 +24,4 @@ export default class Line extends Component2D {
|
||||
|
||||
// this.childs = [new ComponentDebug(this)]
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,28 +1,30 @@
|
||||
import GameEngine from 'GameEngine'
|
||||
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
|
||||
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Component2D, { ComponentState } from 'GameEngine/Component2D'
|
||||
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
|
||||
import { globalState } from '..'
|
||||
import GameEngine from '../../../GameEngine'
|
||||
import BoxCollider2D from '../../../GameEngine/2D/Collider/BoxCollider2D'
|
||||
import ColliderDebugger from '../../../GameEngine/2D/Debug/ColliderDebugger'
|
||||
import Vector2D from '../../../GameEngine/2D/Vector2D'
|
||||
import Component2D, { type ComponentState } from '../../../GameEngine/Component2D'
|
||||
import Cursor from '../../../GameEngine/Components/Cursor'
|
||||
import RectRenderer from '../../../GameEngine/Renderer/RectRenderer'
|
||||
|
||||
export default class Start extends Component2D {
|
||||
public renderer: RectRenderer = new RectRenderer(this, {material: 'yellow'})
|
||||
public position: Vector2D = new Vector2D(1, 1)
|
||||
public scale: Vector2D = new Vector2D(2, 1)
|
||||
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
|
||||
public childs: Array<Component2D> = [new ColliderDebugger(this, this.collider)]
|
||||
public override renderer: RectRenderer = new RectRenderer(this, {material: 'yellow'})
|
||||
public override position: Vector2D = new Vector2D(0.5, 1)
|
||||
public override scale: Vector2D = new Vector2D(2, 1)
|
||||
public override collider: BoxCollider2D = new BoxCollider2D(this, {
|
||||
tags: 'cursor'
|
||||
})
|
||||
public override childs: Array<Component2D> = [new ColliderDebugger()]
|
||||
|
||||
private hasCollided = false
|
||||
|
||||
public async update(state: ComponentState) {
|
||||
if (state.isColliding === 'click') {
|
||||
this.hasCollided = true
|
||||
} else if (this.hasCollided) {
|
||||
public override async update(state: ComponentState) {
|
||||
const cursor = (state.collisions?.find((it) => it.component instanceof Cursor))?.component as Cursor | undefined
|
||||
if (!cursor) {
|
||||
return
|
||||
}
|
||||
if (cursor.leftBtn.wasDown && !cursor.leftBtn.isDown) {
|
||||
console.log('Start Game !')
|
||||
await GameEngine.getGameEngine().setScene('TicTacToe')
|
||||
globalState.isPlaying = true
|
||||
this.hasCollided = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
src/layouts/Base.astro
Normal file
28
src/layouts/Base.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string
|
||||
}
|
||||
|
||||
import Favicon from '../components/Favicon/Favicon.astro'
|
||||
import IconSVG from '../assets/layouts/Base/favicon.svg'
|
||||
import IconPNG from '../assets/layouts/Base/favicon.png'
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="Astro description">
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<Favicon svg={IconSVG} png={IconPNG} icoPath="/favicon.ico" />
|
||||
<title>{title}</title>
|
||||
|
||||
<!-- Plausible -->
|
||||
<script defer data-domain="lucide-astro.dzeio.com" src="/js/script.js" />
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
11
src/layouts/Layout.astro
Normal file
11
src/layouts/Layout.astro
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
import Base, { type Props as BaseProps } from './Base.astro'
|
||||
|
||||
export interface Props extends BaseProps {}
|
||||
---
|
||||
|
||||
<Base {...Astro.props}>
|
||||
<main class="container">
|
||||
<slot />
|
||||
</main>
|
||||
</Base>
|
7
src/layouts/README.md
Normal file
7
src/layouts/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Layouts
|
||||
|
||||
Application different layouts they should extends `Base.astro` if added and also pass the parameters of Base.astro to the page
|
||||
|
||||
## Base.astro
|
||||
|
||||
This is the base file for each path of the application, executed for each paths
|
5
src/libs/AstroUtils.ts
Normal file
5
src/libs/AstroUtils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default class AstroUtils {
|
||||
public static async wrap<T = void>(fn: () => T | Promise<T>) {
|
||||
return await fn()
|
||||
}
|
||||
}
|
289
src/libs/HTTP/StatusCode.ts
Normal file
289
src/libs/HTTP/StatusCode.ts
Normal file
@ -0,0 +1,289 @@
|
||||
/**
|
||||
* HTTP Status code
|
||||
*
|
||||
* Following https://developer.mozilla.org/en-US/docs/Web/HTTP/Status an extension of the RFC9110
|
||||
*/
|
||||
enum StatusCode {
|
||||
|
||||
/****************
|
||||
* 1xx Requests *
|
||||
****************/
|
||||
/**
|
||||
* This interim response indicates that the client should continue the request or ignore the response if the request is already finished.
|
||||
*/
|
||||
CONTINUE = 100,
|
||||
/**
|
||||
* This code is sent in response to an Upgrade request header from the client and indicates the protocol the server is switching to.
|
||||
*/
|
||||
SWITCHING_PROTOCOLS,
|
||||
/**
|
||||
* This code indicates that the server has received and is processing the request, but no response is available yet.
|
||||
*/
|
||||
PROCESSING,
|
||||
/**
|
||||
* This status code is primarily intended to be used with the Link header, letting the user agent start preloading resources while the server prepares a response.
|
||||
*/
|
||||
EARLY_HINTS,
|
||||
|
||||
/****************
|
||||
* 2xx Requests *
|
||||
****************/
|
||||
|
||||
/**
|
||||
* The request succeeded. The result meaning of "success" depends on the HTTP method:
|
||||
* - `GET`: The resource has been fetched and transmitted in the message body.
|
||||
* - `HEAD`: The representation headers are included in the response without any message body.
|
||||
* - `PUT` or `POST`: The resource describing the result of the action is transmitted in the message body.
|
||||
* - `TRACE`: The message body contains the request message as received by the server.
|
||||
*/
|
||||
OK = 200,
|
||||
/**
|
||||
* The request succeeded, and a new resource was created as a result. This is typically the response sent after `POST` requests, or some `PUT` requests.
|
||||
*/
|
||||
CREATED,
|
||||
/**
|
||||
* The request has been received but not yet acted upon. It is noncommittal, since there is no way in HTTP to later send an asynchronous response indicating the outcome of the request. It is intended for cases where another process or server handles the request, or for batch processing.
|
||||
*/
|
||||
ACCEPTED,
|
||||
/**
|
||||
* This response code means the returned metadata is not exactly the same as is available from the origin server, but is collected from a local or a third-party copy. This is mostly used for mirrors or backups of another resource. Except for that specific case, the `200 OK` response is preferred to this status.
|
||||
*/
|
||||
NON_AUTHORITATIVE_INFORMATION,
|
||||
/**
|
||||
* There is no content to send for this request, but the headers may be useful. The user agent may update its cached headers for this resource with the new ones.
|
||||
*/
|
||||
NO_CONTENT,
|
||||
/**
|
||||
* Tells the user agent to reset the document which sent this request.
|
||||
*/
|
||||
RESET_CONTENT,
|
||||
/**
|
||||
* This response code is used when the Range header is sent from the client to request only part of a resource.
|
||||
*/
|
||||
PARTIAL_CONTENT,
|
||||
/**
|
||||
* Conveys information about multiple resources, for situations where multiple status codes might be appropriate.
|
||||
*/
|
||||
MULTI_STATUS,
|
||||
/**
|
||||
* Used inside a `<dav:propstat>` response element to avoid repeatedly enumerating the internal members of multiple bindings to the same collection.
|
||||
*/
|
||||
ALREADY_REPORTED,
|
||||
/**
|
||||
* The server has fulfilled a `GET` request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.
|
||||
*/
|
||||
IM_USED = 226,
|
||||
|
||||
/****************
|
||||
* 3xx Requests *
|
||||
****************/
|
||||
|
||||
/**
|
||||
* The request has more than one possible response. The user agent or user should choose one of them. (There is no standardized way of choosing one of the responses, but HTML links to the possibilities are recommended so the user can pick.)
|
||||
*/
|
||||
MULTIPLE_CHOICES = 300,
|
||||
/**
|
||||
* The URL of the requested resource has been changed permanently. The new URL is given in the response.
|
||||
*/
|
||||
MOVED_PERMANENTLY,
|
||||
/**
|
||||
* This response code means that the URI of requested resource has been changed temporarily. Further changes in the URI might be made in the future. Therefore, this same URI should be used by the client in future requests.
|
||||
*/
|
||||
FOUND,
|
||||
/**
|
||||
* The server sent this response to direct the client to get the requested resource at another URI with a GET request.
|
||||
*/
|
||||
SEE_OTHER,
|
||||
/**
|
||||
* This is used for caching purposes. It tells the client that the response has not been modified, so the client can continue to use the same cached version of the response.
|
||||
*/
|
||||
NOT_MODIFIED,
|
||||
/**
|
||||
* Defined in a previous version of the HTTP specification to indicate that a requested response must be accessed by a proxy. It has been deprecated due to security concerns regarding in-band configuration of a proxy.
|
||||
*/
|
||||
USE_PROXY,
|
||||
/**
|
||||
* This response code is no longer used; it is just reserved. It was used in a previous version of the HTTP/1.1 specification.
|
||||
*/
|
||||
// UNUSED
|
||||
/**
|
||||
* The server sends this response to direct the client to get the requested resource at another URI with the same method that was used in the prior request. This has the same semantics as the `302 Found` HTTP response code, with the exception that the user agent must not change the HTTP method used: if a `POST` was used in the first request, a `POST` must be used in the second request.
|
||||
*/
|
||||
TEMPORARY_REDIRECT = 307,
|
||||
/**
|
||||
* This means that the resource is now permanently located at another URI, specified by the `Location:` HTTP Response header. This has the same semantics as the `301 Moved Permanently` HTTP response code, with the exception that the user agent must not change the HTTP method used: if a `POST` was used in the first request, a `POST` must be used in the second request.
|
||||
*/
|
||||
PERMANENT_REDIRECT,
|
||||
|
||||
/****************
|
||||
* 4xx Requests *
|
||||
****************/
|
||||
|
||||
/**
|
||||
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
|
||||
*/
|
||||
BAD_REQUEST = 400,
|
||||
/**
|
||||
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
|
||||
*/
|
||||
UNAUTHORIZED,
|
||||
/**
|
||||
* This response code is reserved for future use. The initial aim for creating this code was using it for digital payment systems, however this status code is used very rarely and no standard convention exists.
|
||||
*/
|
||||
PAYMENT_REQUIRED,
|
||||
/**
|
||||
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike `401 Unauthorized`, the client's identity is known to the server.
|
||||
*/
|
||||
FORBIDDEN,
|
||||
/**
|
||||
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of `403 Forbidden` to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
|
||||
*/
|
||||
NOT_FOUND,
|
||||
/**
|
||||
* The request method is known by the server but is not supported by the target resource. For example, an API may not allow calling `DELETE` to remove a resource.
|
||||
*/
|
||||
METHOD_NOT_ALLOWED,
|
||||
/**
|
||||
* This response is sent when the web server, after performing server-driven content negotiation, doesn't find any content that conforms to the criteria given by the user agent.
|
||||
*/
|
||||
NOT_ACCEPTABLE,
|
||||
/**
|
||||
* This is similar to `401 Unauthorized` but authentication is needed to be done by a proxy.
|
||||
*/
|
||||
PROXY_AUTHENTIFICATION_REQUIRED,
|
||||
/**
|
||||
* This response is sent on an idle connection by some servers, even without any previous request by the client. It means that the server would like to shut down this unused connection. This response is used much more since some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also note that some servers merely shut down the connection without sending this message.
|
||||
*/
|
||||
REQUEST_TIMEOUT,
|
||||
/**
|
||||
* This response is sent when a request conflicts with the current state of the server.
|
||||
*/
|
||||
CONFLICT,
|
||||
/**
|
||||
* This response is sent when the requested content has been permanently deleted from server, with no forwarding address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources that have been deleted with this status code.
|
||||
*/
|
||||
GONE,
|
||||
/**
|
||||
* Server rejected the request because the `Content-Length` header field is not defined and the server requires it.
|
||||
*/
|
||||
LENGTH_REQUIRED,
|
||||
/**
|
||||
* The client has indicated preconditions in its headers which the server does not meet.
|
||||
*/
|
||||
PRECONDITION_FAILED,
|
||||
/**
|
||||
* Request entity is larger than limits defined by server. The server might close the connection or return an `Retry-After` header field.
|
||||
*/
|
||||
PAYLOAD_TOO_LARGE,
|
||||
/**
|
||||
* The URI requested by the client is longer than the server is willing to interpret.
|
||||
*/
|
||||
URI_TOO_LONG,
|
||||
/**
|
||||
* The media format of the requested data is not supported by the server, so the server is rejecting the request.
|
||||
*/
|
||||
UNSUPPORTED_MEDIA_TYPE,
|
||||
/**
|
||||
* The range specified by the `Range` header field in the request cannot be fulfilled. It's possible that the range is outside the size of the target URI's data.
|
||||
*/
|
||||
RANGE_NOT_SATISFIABLE,
|
||||
/**
|
||||
* This response code means the expectation indicated by the `Expect` request header field cannot be met by the server.
|
||||
*/
|
||||
EXPECTATION_FAILED,
|
||||
/**
|
||||
* The server refuses the attempt to brew coffee with a teapot.
|
||||
*/
|
||||
IM_A_TEAPOT,
|
||||
/**
|
||||
* The request was directed at a server that is not able to produce a response. This can be sent by a server that is not configured to produce responses for the combination of scheme and authority that are included in the request URI.
|
||||
*/
|
||||
MIDIRECTED_REQUEST = 421,
|
||||
/**
|
||||
* The request was well-formed but was unable to be followed due to semantic errors.
|
||||
*/
|
||||
UNPROCESSABLE_CONTENT,
|
||||
/**
|
||||
* The resource that is being accessed is locked.
|
||||
*/
|
||||
LOCKED,
|
||||
/**
|
||||
* The request failed due to failure of a previous request.
|
||||
*/
|
||||
FAILED_DEPENDENCY,
|
||||
/**
|
||||
* Indicates that the server is unwilling to risk processing a request that might be replayed.
|
||||
*/
|
||||
TOO_EARLY,
|
||||
/**
|
||||
* The server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol. The server sends an `Upgrade` header in a 426 response to indicate the required protocol(s).
|
||||
*/
|
||||
UPGRADE_REQUIRED,
|
||||
/**
|
||||
* The origin server requires the request to be conditional. This response is intended to prevent the 'lost update' problem, where a client `GET`s a resource's state, modifies it and `PUT`s it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.
|
||||
*/
|
||||
PRECONDITION_REQUIRED = 428,
|
||||
/**
|
||||
* The user has sent too many requests in a given amount of time ("rate limiting").
|
||||
*/
|
||||
TOO_MANY_REQUESTS,
|
||||
/**
|
||||
* The server is unwilling to process the request because its header fields are too large. The request may be resubmitted after reducing the size of the request header fields.
|
||||
*/
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
|
||||
/**
|
||||
* The user agent requested a resource that cannot legally be provided, such as a web page censored by a government.
|
||||
*/
|
||||
UNAVAILABLE_OR_LEGAL_REASONS = 451,
|
||||
|
||||
/****************
|
||||
* 5xx Requests *
|
||||
****************/
|
||||
|
||||
/**
|
||||
* The server has encountered a situation it does not know how to handle.
|
||||
*/
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
/**
|
||||
* The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are `GET` and `HEAD`.
|
||||
*/
|
||||
NOT_IMPLEMENTED,
|
||||
/**
|
||||
* This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.
|
||||
*/
|
||||
BAD_GATEWAY,
|
||||
/**
|
||||
* The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the `Retry-After` HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached.
|
||||
*/
|
||||
SERVICE_UNAVAILABLE,
|
||||
/**
|
||||
* This error response is given when the server is acting as a gateway and cannot get a response in time.
|
||||
*/
|
||||
GATEWAY_TIMEOUT,
|
||||
/**
|
||||
* The HTTP version used in the request is not supported by the server.
|
||||
*/
|
||||
HTTP_VERSION_NOT_SUPPORTED,
|
||||
/**
|
||||
* The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
|
||||
*/
|
||||
VARIANT_ALSO_NEGOTIATES,
|
||||
/**
|
||||
* The method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request.
|
||||
*/
|
||||
INSUFFICIENT_STORAGE,
|
||||
/**
|
||||
* The server detected an infinite loop while processing the request.
|
||||
*/
|
||||
LOOP_DETECTED,
|
||||
/**
|
||||
* Further extensions to the request are required for the server to fulfill it.
|
||||
*/
|
||||
NOT_EXTENDED = 510,
|
||||
/**
|
||||
* Indicates that the client needs to authenticate to gain network access.
|
||||
*/
|
||||
NETWORK_AUTHENTIFICATION_REQUIRED,
|
||||
}
|
||||
|
||||
export default StatusCode
|
3
src/libs/README.md
Normal file
3
src/libs/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Libs
|
||||
|
||||
Globally independent objects/classes/functions that SHOULD be unit testable by themselve
|
66
src/libs/RFCs/RFC7807.ts
Normal file
66
src/libs/RFCs/RFC7807.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import ResponseBuilder from '../ResponseBuilder'
|
||||
|
||||
/**
|
||||
* Add headers:
|
||||
* Content-Type: application/problem+json
|
||||
*
|
||||
* following https://www.rfc-editor.org/rfc/rfc7807.html
|
||||
*/
|
||||
export default interface RFC7807 {
|
||||
/**
|
||||
* A URI reference [RFC3986] that identifies the
|
||||
* problem type.
|
||||
*
|
||||
* This specification encourages that, when
|
||||
* dereferenced, it provide human-readable documentation for the
|
||||
* problem type (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||
*
|
||||
* When
|
||||
* this member is not present, its value is assumed to be
|
||||
* "about:blank"
|
||||
*/
|
||||
type?: string
|
||||
|
||||
/**
|
||||
* A short, human-readable summary of the problem
|
||||
* type.
|
||||
*
|
||||
* It SHOULD NOT change from occurrence to occurrence of the
|
||||
* problem, except for purposes of localization (e.g., using
|
||||
* proactive content negotiation; see [RFC7231], Section 3.4).
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* The HTTP status code ([RFC7231], Section 6)
|
||||
* generated by the origin server for this occurrence of the problem.
|
||||
*/
|
||||
status?: number
|
||||
|
||||
/**
|
||||
* A human-readable explanation specific to this
|
||||
* occurrence of the problem.
|
||||
*/
|
||||
details?: string
|
||||
|
||||
/**
|
||||
* A URI reference that identifies the specific
|
||||
* occurrence of the problem.
|
||||
*
|
||||
* It may or may not yield further
|
||||
* information if dereferenced.
|
||||
*/
|
||||
instance?: string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error the error (base items are type, status, title details and instance)
|
||||
* @returns
|
||||
*/
|
||||
export function buildRFC7807(error: RFC7807 & Record<string, any>, response: ResponseBuilder = new ResponseBuilder()): Response {
|
||||
response.addHeader('Content-Type', 'application/problem+json')
|
||||
.body(JSON.stringify(error))
|
||||
.status(error.status ?? 500)
|
||||
return response.build()
|
||||
}
|
59
src/libs/ResponseBuilder.ts
Normal file
59
src/libs/ResponseBuilder.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { objectLoop } from '@dzeio/object-util'
|
||||
|
||||
/**
|
||||
* Simple builde to create a new Response object
|
||||
*/
|
||||
export default class ResponseBuilder {
|
||||
|
||||
private _body: BodyInit | null | undefined
|
||||
public body(body: string | Buffer | object | null | undefined) {
|
||||
if (typeof body === 'object' && !(body instanceof Buffer)) {
|
||||
this._body = JSON.stringify(body)
|
||||
this.addHeader('Content-Type', 'application/json')
|
||||
} else if (body instanceof Buffer) {
|
||||
this._body = body.toString()
|
||||
} else {
|
||||
this._body = body
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private _headers: Record<string, string> = {}
|
||||
public headers(headers: Record<string, string>) {
|
||||
this._headers = headers
|
||||
return this
|
||||
}
|
||||
|
||||
public addHeader(key: string, value: string) {
|
||||
this._headers[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
public addHeaders(headers: Record<string, string>) {
|
||||
objectLoop(headers, (value, key) => {
|
||||
this.addHeader(key, value)
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
public removeHeader(key: string) {
|
||||
delete this._headers[key]
|
||||
return this
|
||||
}
|
||||
|
||||
private _status?: number
|
||||
public status(status: number) {
|
||||
this._status = status
|
||||
return this
|
||||
}
|
||||
|
||||
public build(): Response {
|
||||
const init: ResponseInit = {
|
||||
headers: this._headers
|
||||
}
|
||||
if (this._status) {
|
||||
init.status = this._status
|
||||
}
|
||||
return new Response(this._body, init)
|
||||
}
|
||||
}
|
71
src/libs/States.ts
Normal file
71
src/libs/States.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { isObject, objectClone, objectEqual } from '@dzeio/object-util'
|
||||
|
||||
type ChangeFn<T> = (props: T, oldProps: T) => void | Promise<void>
|
||||
|
||||
export default class States<T extends Record<string, any>> {
|
||||
private observers: Partial<Record<keyof T, Array<ChangeFn<Partial<T>>>>> = {}
|
||||
|
||||
public props: T = {} as T
|
||||
|
||||
public setProps(items: Partial<T>, options?: {
|
||||
force: boolean
|
||||
}) {
|
||||
const old = objectClone(this.props)
|
||||
let updated = false
|
||||
for (const item in items) {
|
||||
if (
|
||||
!options?.force &&
|
||||
((isObject(this.props[item]) && isObject(items[item]) && objectEqual(this.props[item] as object, items[item] as object)) ||
|
||||
this.props[item] === items[item])
|
||||
) {
|
||||
console.warn(item, 'does not need to be updated')
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
this.props[item] = items[item] as T[typeof item]
|
||||
const obs = this.observers[item]
|
||||
if (obs) {
|
||||
for (const observer of obs) {
|
||||
observer(this.props, old)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
const obs = this.observers['']
|
||||
if (obs) {
|
||||
for (const observer of obs) {
|
||||
observer(this.props, old)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
props: T
|
||||
) {
|
||||
this.setProps(props)
|
||||
}
|
||||
|
||||
public forceUpdate(key: string) {
|
||||
const obs = this.observers[key]
|
||||
if (obs) {
|
||||
for (const observer of obs) {
|
||||
observer(this.props, this.props)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdate(fn: ChangeFn<Partial<T>>) {
|
||||
return this.listen('', fn)
|
||||
}
|
||||
|
||||
public listen(key: keyof T | '', fn: ChangeFn<Partial<T>>) {
|
||||
if (!this.observers[key]) {
|
||||
this.observers[key] = []
|
||||
}
|
||||
this.observers[key]?.push(fn)
|
||||
if (typeof this.props[key] !== 'undefined') {
|
||||
fn(this.props, this.props)
|
||||
}
|
||||
}
|
||||
}
|
103
src/models/Dao.ts
Normal file
103
src/models/Dao.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* the Dao is the object that connect the Database or source to the application layer
|
||||
*
|
||||
* you MUST call it through the `DaoFactory` file
|
||||
*/
|
||||
export default abstract class Dao<Object extends { id: any } = { id: any }> {
|
||||
|
||||
/**
|
||||
* insert a new object into the source
|
||||
*
|
||||
* @param obj the object to create
|
||||
* @returns the object with it's id filled if create or null otherwise
|
||||
*/
|
||||
abstract create(obj: Omit<Object, 'id'>): Promise<Object | null>
|
||||
|
||||
/**
|
||||
* insert a new object into the source
|
||||
*
|
||||
* @param obj the object to create
|
||||
* @returns the object with it's id filled if create or null otherwise
|
||||
*/
|
||||
public insert: Dao<Object>['create'] = (obj: Parameters<Dao<Object>['create']>[0]) => this.create(obj)
|
||||
|
||||
/**
|
||||
* find the list of objects having elements from the query
|
||||
*
|
||||
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||
* @returns an array containing the list of elements that match with the query
|
||||
*/
|
||||
abstract findAll(query?: Partial<Object>): Promise<Array<Object>>
|
||||
|
||||
/**
|
||||
* find the list of objects having elements from the query
|
||||
*
|
||||
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||
* @returns an array containing the list of elements that match with the query
|
||||
*/
|
||||
public find: Dao<Object>['findAll'] = (query: Parameters<Dao<Object>['findAll']>[0]) => this.findAll(query)
|
||||
|
||||
/**
|
||||
* find an object by it's id
|
||||
*
|
||||
* (shortcut to findOne({id: id}))
|
||||
*
|
||||
* @param id the id of the object
|
||||
* @returns
|
||||
*/
|
||||
public findById(id: Object['id']): Promise<Object | null> {
|
||||
return this.findOne({id: id} as Partial<Object>)
|
||||
}
|
||||
|
||||
/**
|
||||
* find an object by it's id
|
||||
*
|
||||
* (shortcut to findOne({id: id}))
|
||||
*
|
||||
* @param id the id of the object
|
||||
* @returns
|
||||
*/
|
||||
public get(id: Object['id']) {
|
||||
return this.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* find the first element that match `query`
|
||||
*
|
||||
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
|
||||
* @returns the first element matching with the query or null otherwise
|
||||
*/
|
||||
public async findOne(query?: Partial<Object>): Promise<Object | null> {
|
||||
return (await this.findAll(query))[0] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* update the remote reference of the object
|
||||
*
|
||||
* note: it will not try to insert an item (use `upsert` to handle this)
|
||||
*
|
||||
* @param obj the object to update
|
||||
* @returns an object if it was able to update or null otherwise
|
||||
*/
|
||||
abstract update(obj: Object): Promise<Object | null>
|
||||
|
||||
/**
|
||||
* update the remote reference of the object or create it if not found
|
||||
* @param obj the object to update/insert
|
||||
* @returns the object is updated/inserted or null otherwise
|
||||
*/
|
||||
public async upsert(object: Object | Omit<Object, 'id'>): Promise<Object | null> {
|
||||
if ('id' in object) {
|
||||
return this.update(object)
|
||||
}
|
||||
return this.insert(object)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the object
|
||||
* @param obj the object to delete
|
||||
*
|
||||
* @returns if the object was deleted or not (if object is not in db it will return true)
|
||||
*/
|
||||
abstract delete(obj: Object): Promise<boolean>
|
||||
}
|
54
src/models/DaoFactory.ts
Normal file
54
src/models/DaoFactory.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* TODO:
|
||||
* Add to `DaoItem` your model name
|
||||
* Add to the function `initDao` the Dao
|
||||
*/
|
||||
|
||||
/**
|
||||
* the different Daos that can be initialized
|
||||
*
|
||||
* Touch this interface to define which key is linked to which Dao
|
||||
*/
|
||||
interface DaoItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to get any DAO
|
||||
*/
|
||||
export default class DaoFactory {
|
||||
/**
|
||||
* reference of the different Daos for a correct singleton implementation
|
||||
*/
|
||||
private static daos: Partial<DaoItem> = {}
|
||||
|
||||
/**
|
||||
* Get a a dao by its key
|
||||
*
|
||||
* it will throw an error if no Dao exists linked to the item key
|
||||
*
|
||||
* @param key the dao key to get
|
||||
* @returns the Dao you want as a singleton
|
||||
*/
|
||||
public static get<Key extends keyof DaoItem>(key: Key): DaoItem[Key] {
|
||||
if (!(key in this.daos)) {
|
||||
const dao = this.initDao(key)
|
||||
if (!dao) {
|
||||
throw new Error(`${key} has no valid Dao`)
|
||||
}
|
||||
this.daos[key] = dao as DaoItem[Key]
|
||||
}
|
||||
return this.daos[key] as DaoItem[Key]
|
||||
}
|
||||
|
||||
/**
|
||||
* init a dao by its key, it does not care if it exists or not
|
||||
*
|
||||
* @param item the element to init
|
||||
* @returns a new initialized dao or undefined if no dao is linked
|
||||
*/
|
||||
private static initDao(item: keyof DaoItem): any | undefined {
|
||||
switch (item) {
|
||||
default: return undefined
|
||||
}
|
||||
}
|
||||
}
|
26
src/models/README.md
Normal file
26
src/models/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Models
|
||||
|
||||
this folder contains the Application data layer
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Add a {model}/index.ts contianing your `interface`
|
||||
2. Add a {model}/{model}Dao.ts containing your DAO that `extends` from `Dao.ts`
|
||||
3. Add your Dao to the `DaoFactory.ts` file
|
||||
|
||||
|
||||
## **/index.ts
|
||||
|
||||
file containing the definition of the model
|
||||
|
||||
## **/\*Dao.ts
|
||||
|
||||
File containing the implementation of the Dao
|
||||
|
||||
## Dao.ts
|
||||
|
||||
the Dao.ts is the file each `*Dao.ts` extends from allowing to have a simple, quick and easy to comprehend connectivity
|
||||
|
||||
## DaoFactory.ts
|
||||
|
||||
The DaoFactory file is the file in which you will have the only direct reference to each `*Dao` files and will be sent from there to the rest of the applicaiton layer
|
5
src/pages/README.md
Normal file
5
src/pages/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Content
|
||||
|
||||
Contains raw content for pages.
|
||||
|
||||
Mostly some static pages or blog posts.
|
@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
import App from 'next/app'
|
||||
|
||||
import PlausibleProvider from 'next-plausible'
|
||||
|
||||
export default class CApp extends App {
|
||||
|
||||
public render() {
|
||||
const { Component, pageProps } = this.props
|
||||
|
||||
return (
|
||||
<PlausibleProvider
|
||||
enabled
|
||||
customDomain="https://proxy.dzeio.com"
|
||||
domain="games.avior.me"
|
||||
integrity="sha256-R6vN8jmBq9SIpnfJRnw9eNUfLbC2yO3GPQAKR5ZS7zQ="
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</PlausibleProvider>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
class MyDocument extends Document {
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
22
src/pages/api/event.ts
Normal file
22
src/pages/api/event.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
/**
|
||||
* Plausible proxy
|
||||
*/
|
||||
export const post: APIRoute = async ({ request, clientAddress }) => {
|
||||
// const body = await request.json()
|
||||
// console.log(body, clientAddress)
|
||||
const res = await fetch('https://plausible.io/api/event', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': request.headers.get('User-Agent') as string,
|
||||
'X-Forwarded-For': clientAddress,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: await request.text()
|
||||
})
|
||||
return {
|
||||
status: res.status,
|
||||
body: await res.text()
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import React from 'react'
|
||||
import GameEngine from 'GameEngine'
|
||||
import Scene from 'GameEngine/Scene'
|
||||
import Space from 'games/city/Space'
|
||||
import Vector2D from 'GameEngine/2D/Vector2D'
|
||||
import Cursor from 'games/city/Cursor'
|
||||
import Button from 'games/city/Button'
|
||||
import Text from 'games/city/Text'
|
||||
import FPSCounter from 'GameEngine/Components/FPSCounter'
|
||||
|
||||
export default class Snake extends React.PureComponent {
|
||||
|
||||
public async componentDidMount() {
|
||||
const ge = new GameEngine('#test', {
|
||||
caseCount: [208,104],
|
||||
debugColliders: false,
|
||||
goalFramerate: 60
|
||||
})
|
||||
const mainScene = new Scene('Menu')
|
||||
mainScene.camera.topLeft.x = 0
|
||||
mainScene.camera.topLeft.y = 0
|
||||
const cursor = new Cursor()
|
||||
|
||||
// for (let ch = 0; ch < 104; ch++) {
|
||||
// for (let cw = 0; cw < 208; cw++) {
|
||||
// mainScene.addComponent(
|
||||
// new Tile(cw, ch)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
const placeableRects: Array<[Vector2D, Vector2D]> = [
|
||||
// top rect
|
||||
[new Vector2D(0, 0), new Vector2D(208, 18)],
|
||||
|
||||
// left recet
|
||||
[new Vector2D(20, 28), new Vector2D(167, 79)],
|
||||
|
||||
// right rect
|
||||
[new Vector2D(178,28), new Vector2D(208, 79)],
|
||||
// bottom rect
|
||||
[new Vector2D(0, 89), new Vector2D(208, 104)],
|
||||
|
||||
|
||||
]
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const width = i % 2 === 0 ? 20 : 10
|
||||
const height = i % 2 === 0 ? 10 : 20
|
||||
const baseX = width / 2 + 1
|
||||
const baseY = i % 2 === 0 ? 50 : 70
|
||||
|
||||
const it = new Space(new Vector2D(baseX, baseY), new Vector2D(width, height), cursor, placeableRects)
|
||||
// it.debug = i === 0
|
||||
mainScene.addComponent(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
// mainScene.addComponent(
|
||||
// // cursor,
|
||||
// new Space(new Vector2D(20, 10), cursor, placeableRects, true)
|
||||
// )
|
||||
|
||||
mainScene.addComponent(
|
||||
new Button(),
|
||||
new Text('x10Moved', undefined, 16),
|
||||
new Text('x20Moved', undefined, 16),
|
||||
new FPSCounter({textColor: 'white', size: 32})
|
||||
)
|
||||
|
||||
await ge.setScene(mainScene)
|
||||
ge.start()
|
||||
}
|
||||
|
||||
|
||||
public render = () => (
|
||||
<>
|
||||
<canvas id="test" width="2080" height="1040" style={{backgroundImage: 'url(\'/assets/city/background.png\')'}}></canvas>
|
||||
</>
|
||||
)
|
||||
|
||||
private randomInt(min: number, max: number): number {
|
||||
min = Math.ceil(min)
|
||||
max = Math.floor(max)
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
}
|
8
src/pages/index.astro
Normal file
8
src/pages/index.astro
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<main>
|
||||
</main>
|
||||
</Layout>
|
@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Index extends React.Component {
|
||||
|
||||
public render = () => (
|
||||
<main>
|
||||
</main>
|
||||
)
|
||||
}
|
12
src/pages/js/script.js.ts
Normal file
12
src/pages/js/script.js.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
/**
|
||||
* Plausible proxy
|
||||
*/
|
||||
export const get: APIRoute = async () => {
|
||||
const res = await fetch('https://plausible.io/js/script.file-downloads.outbound-links.js')
|
||||
return {
|
||||
status: 200,
|
||||
body: await res.text()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user