mirror of
https://github.com/Aviortheking/games.git
synced 2025-07-03 14:49:18 +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:
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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user