feat: move to Astro and mostly reworked the Pokémon Shuffle game

Signed-off-by: Avior <github@avior.me>
This commit is contained in:
2023-08-15 18:37:26 +02:00
parent 4bb1f17467
commit c42311eaae
111 changed files with 11290 additions and 10821 deletions

View 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),
]
}
}

View 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
}
}

View 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
}
}

View 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()
}
}

View 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
}
}
}

View File

@ -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
}
}

View File

@ -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()})
}
}

View File

@ -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)

View File

@ -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'})
}
}

View File

@ -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()

View File

@ -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))
)
}
}

View File

@ -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)
}
}

View File

@ -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) {}
}

View File

@ -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)
)
}
}

View 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
}
}
}

View 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))
}
}

View File

@ -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)})
}
}

View 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())
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

View 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)
}
}

View 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)
}
}

View 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)
}
}
}

View File

@ -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)
// }
}

View File

@ -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)
}
}

View File

@ -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
)

View File

@ -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
}
}

View File

@ -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
*/

View File

@ -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)

View File

@ -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> {

View 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
}

View 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)
}
}