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