This commit is contained in:
Florian Bouillon 2022-09-08 18:41:47 +02:00
parent 2143f9887e
commit 4bb1f17467
16 changed files with 350 additions and 5281 deletions

4755
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,6 @@
"test": "jest --config jext.config.js"
},
"dependencies": {
"@dzeio/components": "^0.10.1",
"@dzeio/object-util": "^1.2.0",
"critters": "^0.0.10",
"easy-sitemap": "^1.0.0",

View File

@ -45,4 +45,9 @@ export default class Vector2D {
parseFloat(this.y.toFixed(nDecimal))
)
}
public set(x: number, y: number) {
this.x = x
this.y = y
}
}

View File

@ -10,10 +10,21 @@ export interface ComponentState {
isColliding?: string
}
export type StaticComponent<
// eslint-disable-next-line @typescript-eslint/ban-types
T extends {} | void = {} | void
> =
new (params: T | undefined) => Component2D<T>
/**
* 2D Component
*/
export default abstract class Component2D {
export default abstract class Component2D<
// eslint-disable-next-line @typescript-eslint/ban-types
T extends {} | void = {} | void
> {
public params: T = {} as T
/**
* Indicate how the component is rendered
@ -75,6 +86,12 @@ export default abstract class Component2D {
*/
public debug?: boolean
public constructor(it: T | void) {
if (it) {
this.params = it
}
}
/**
* Function run when the component is initialized
*/

View File

@ -1,25 +1,57 @@
import GameEngine from 'GameEngine'
import ComponentDebug from 'GameEngine/2D/Debug/ComponentDebug'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D from 'GameEngine/Component2D'
import Renderer from 'GameEngine/Renderer'
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
export default class FPSCounter extends Component2D {
export default class FPSCounter extends Component2D<{textColor?: string, size?: number}> {
public position: Vector2D = new Vector2D(0,0)
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: 'pouet'})
public renderer: TextRenderer = new TextRenderer(this, {text: 'loading...'})
private lastUpdate: number = new Date().getTime()
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
}
if (this.params.size) {
this.renderer.size = this.params.size
}
}
public update() {
const now = new Date().getTime()
this.renderer.text = (1000 / (now - this.lastUpdate)).toFixed(2)
this.lastUpdate = now
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)
}
}
}

View File

@ -29,17 +29,14 @@ export default class Scene {
}
public async init() {
this.components.forEach((v) => {
if (v.init) {
v.init()
}
})
for await (const component of this.components) {
await component.init?.()
}
}
public async update() {
for (let index = 0; index < this.components.length; index++) {
const component = this.components[index];
await this.updateComponent(component, index)
await this.updateComponent(this.components[index], index)
}
}
@ -99,8 +96,8 @@ export default class Scene {
if (debug) {
console.log('Processing childs', v)
}
for await (const child of v.childs) {
await this.updateComponent(child)
for (let cIndex = 0; cIndex < v.childs.length; cIndex++) {
await this.updateComponent(v.childs[cIndex], cIndex)
}
}
}

View File

@ -22,8 +22,23 @@ export default class GameEngine {
wasDown: false
}
public currentScene?: Scene
// last frame timestamp
public lastFrame = 0
/**
* last frame execution time in milliseconds
*
* @memberof GameEngine
*/
public frameTime = 0
private isRunning = false
private timer = 16.6
// timer between frames
private timer = 0
public constructor(
id: string,
@ -61,12 +76,8 @@ export default class GameEngine {
ctx.imageSmoothingEnabled = false
this.ctx = ctx
if (options?.goalFramerate) {
if (options.goalFramerate === -1) {
this.timer = 0
} else {
this.timer = 1000 / options.goalFramerate
}
if (options?.goalFramerate && options.goalFramerate >= 0) {
this.timer = 1000 / options.goalFramerate
}
}
@ -80,13 +91,11 @@ export default class GameEngine {
return
}
this.isRunning = true
requestAnimationFrame(() => {
this.update()
})
this.currentScene?.init().then(() => this.update())
document.addEventListener('mousemove', (ev) => {
this.cursor.position = new Vector2D(
ev.clientX / this.caseSize.x - (this.currentScene?.camera?.topLeft?.x ?? 0),
ev.clientY / this.caseSize.y - (this.currentScene?.camera?.topLeft?.y ?? 0)
(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
@ -109,35 +118,66 @@ export default class GameEngine {
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()
if (wasRunning) {
this.isRunning = true
}
this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene
this.currentScene.setGameEngine(this)
}
private update() {
const now = new Date().getTime()
if (!this.isRunning) {
return
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
if (this.options?.background) {
this.ctx.fillStyle = this.options.background
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
}
this.currentScene?.update()
const diff = new Date().getTime() - now
if (diff > this.timer) {
requestAnimationFrame(() => {
this.update()
})
} else {
setTimeout(() => {
// this.update()
requestAnimationFrame(() => {
this.update()
})
}, this.timer - diff)
}
private async update() {
// console.log('update')
let frameFinished = true
setInterval((it) => {
// get current time
const now = window.performance.now()
// 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(() => {
// })
})
}
}

48
src/games/city/Button.ts Normal file
View File

@ -0,0 +1,48 @@
import { stat } from 'fs'
import GameEngine from 'GameEngine'
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D, { ComponentState } from 'GameEngine/Component2D'
import Renderer from 'GameEngine/Renderer'
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import { globalState } from '.'
import Cursor from './Cursor'
import Space from './Space'
import TextComponent from './TextComponent'
export default class Button extends Component2D {
private static isOnCursor = false
public size = {
width: 3, height: 1
}
public renderer: RectRenderer = new RectRenderer(this)
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
public position: Vector2D = new Vector2D(10, 5)
public scale: Vector2D = new Vector2D(20, 10)
public constructor() {
super()
this.childs = [
new TextComponent(this, 'remèttre à zéro', 'bold', 10, 'white')
]
this.renderer.material = 'black'
}
public update(state: ComponentState): void | Promise<void> {
if (state.isColliding === 'click' || state.isColliding === 'down') {
Space.shouldReset = true
globalState.x10Moved = 0
globalState.x20Moved = 0
} else {
Space.shouldReset = false
}
}
}

View File

@ -1,24 +1,19 @@
import { stat } from 'fs'
import GameEngine from 'GameEngine'
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D, { ComponentState } from 'GameEngine/Component2D'
import Renderer from 'GameEngine/Renderer'
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import { globalState } from '.'
import Cursor from './Cursor'
import TextComponent from './TextComponent'
export default class Space extends Component2D {
private static isOnCursor = false
public static shouldReset = false
public size = {
width: 3, height: 1
}
public renderer: RectRenderer = new RectRenderer(this)
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
@ -27,19 +22,26 @@ export default class Space extends Component2D {
public scale: Vector2D = new Vector2D(30, 10)
private posBeforeCursor: Vector2D | null = null
private basePosition: Vector2D
private hasMoved = false
public constructor(
position: Vector2D,
size: Vector2D,
private cursor: Cursor,
private placeableRects: Array<[Vector2D, Vector2D]>,
private log: boolean = false
) {
super()
this.position = position
this.basePosition = position
// this.debug = true
this.scale = size
const text = `${size.x}x${size.y}`
this.childs = [
new TextComponent(this, text, 'bold', 16, 'blue')
new TextComponent(this, text, 'bold', 8, 'blue')
]
}
@ -50,25 +52,22 @@ export default class Space extends Component2D {
this.renderer.material = 'white'
this.renderer.stroke = {color: 'black', width: 3}
const cursor = GameEngine.getGameEngine().cursor
// if (this.log) console.log(point)
if (this.log) console.log(Space.shouldReset)
if (state.isColliding === 'click' || state.isColliding === 'down') {
if (!this.hasMoved) {
this.hasMoved = true
if (this.scale.x === 10) {
globalState.x10Moved ++
} else {
globalState.x20Moved ++
}
}
this.renderer.stroke = {color: 'green', width: 3}
if (!this.posBeforeCursor) {
this.posBeforeCursor = this.position
}
Space.isOnCursor = true
// console.log('follow cursor', cursor.position, this.position)
this.position = cursor.position.decimalCount(0)
let canPlace = false
for (const placeableRect of this.placeableRects) {
if (point.isIn(placeableRect[0], this.scale.sub(placeableRect[1]))) {
canPlace = true
break
}
}
if (!canPlace) this.renderer.stroke = {color: 'red', width: 3}
else this.posBeforeCursor = this.position
} else if (this.posBeforeCursor) {
this.position = cursor.position
let canPlace = false
for (const placeableRect of this.placeableRects) {
if (point.isIn(placeableRect[0], this.scale.sub(placeableRect[1]))) {
@ -77,10 +76,31 @@ export default class Space extends Component2D {
}
}
if (!canPlace) {
this.position = this.posBeforeCursor
this.renderer.stroke = {color: 'red', width: 3}
}
else {
this.posBeforeCursor = this.position
}
} else if (this.posBeforeCursor) {
const futurePosition = this.position.decimalCount(0)
let canPlace = false
for (const placeableRect of this.placeableRects) {
if (point.isIn(placeableRect[0], this.scale.sub(placeableRect[1]))) {
canPlace = true
break
}
}
if (!canPlace) {
this.position = this.posBeforeCursor.decimalCount(0)
} else {
this.position = futurePosition
}
Space.isOnCursor = false
this.posBeforeCursor = null
}
if (Space.shouldReset) {
this.position = this.basePosition
this.hasMoved = false
}
}
}

51
src/games/city/Text.ts Normal file
View File

@ -0,0 +1,51 @@
import { stat } from 'fs'
import GameEngine from 'GameEngine'
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
import ColliderDebugger from 'GameEngine/2D/Debug/ColliderDebugger'
import PointDebugger from 'GameEngine/2D/Debug/PointDebugger'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D, { ComponentState } from 'GameEngine/Component2D'
import Renderer from 'GameEngine/Renderer'
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
import { globalState } from '.'
import Cursor from './Cursor'
export default class Text extends Component2D {
public renderer: TextRenderer = new TextRenderer(this)
// public position: Vector2D = new Vector2D(0, 0)
public constructor(private type: keyof typeof globalState, weight?: 'bold', size?: number, color?: string) {
super()
switch (type) {
case 'x10Moved':
this.position = new Vector2D(10, 40)
break
case 'x20Moved':
this.position = new Vector2D(10, 43)
break
default:
break
}
this.renderer.text = 'nique'
this.renderer.weight = weight
this.renderer.size = size
this.renderer.color = color
}
public update(state: ComponentState): void | Promise<void> {
switch (this.type) {
case 'x10Moved':
this.renderer.text = '10x20: ' + (5 - globalState.x10Moved)
break
case 'x20Moved':
this.renderer.text = '10x20: ' + (5 - globalState.x20Moved)
break
default:
break
}
}
}

7
src/games/city/index.ts Normal file
View File

@ -0,0 +1,7 @@
export const globalState: {
x20Moved: number
x10Moved: number
} = {
x10Moved: 0,
x20Moved: 0
}

View File

@ -2,7 +2,6 @@ import React from 'react'
import App from 'next/app'
import PlausibleProvider from 'next-plausible'
import '@dzeio/components/style.css'
export default class CApp extends App {

View File

@ -1,24 +1,20 @@
/* eslint-disable max-classes-per-file */
import React from 'react'
import { Text, Link } from '@dzeio/components'
import GameEngine from 'GameEngine'
import Scene from 'GameEngine/Scene'
import Item from 'games/tictactoe/Item'
import Line from 'games/tictactoe/Line'
import Start from 'games/tictactoe/Menu/Start'
import FPSCounter from 'GameEngine/Components/FPSCounter'
import ComponentDebug from 'GameEngine/2D/Debug/ComponentDebug'
import Tile from 'games/city/Tile'
import Space from 'games/city/Space'
import Vector2D from 'GameEngine/2D/Vector2D'
import Cursor from 'games/city/Cursor'
import Button from 'games/city/Button'
import Text from 'games/city/Text'
import FPSCounter from 'GameEngine/Components/FPSCounter'
export default class Snake extends React.PureComponent {
public async componentDidMount() {
const ge = new GameEngine('#test', {
caseCount: [208,104],
debugColliders: true,
debugColliders: false,
goalFramerate: 60
})
const mainScene = new Scene('Menu')
@ -55,9 +51,7 @@ export default class Snake extends React.PureComponent {
const baseX = width / 2 + 1
const baseY = i % 2 === 0 ? 50 : 70
const it = new Space(new Vector2D(width, height), cursor, placeableRects)
it.position.x = baseX
it.position.y = baseY
const it = new Space(new Vector2D(baseX, baseY), new Vector2D(width, height), cursor, placeableRects)
// it.debug = i === 0
mainScene.addComponent(
it
@ -69,6 +63,13 @@ export default class Snake extends React.PureComponent {
// new Space(new Vector2D(20, 10), cursor, placeableRects, true)
// )
mainScene.addComponent(
new Button(),
new Text('x10Moved', undefined, 16),
new Text('x20Moved', undefined, 16),
new FPSCounter({textColor: 'white', size: 32})
)
await ge.setScene(mainScene)
ge.start()
}
@ -77,9 +78,6 @@ export default class Snake extends React.PureComponent {
public render = () => (
<>
<canvas id="test" width="2080" height="1040" style={{backgroundImage: 'url(\'/assets/city/background.png\')'}}></canvas>
<Text>
<span id="debug"></span>
</Text>
</>
)

View File

@ -1,17 +1,9 @@
import React from 'react'
import { Link, Text } from '@dzeio/components'
export default class Index extends React.Component {
public render = () => (
<main>
<Text>
<Link href="/pokemon-shuffle">Pokémon Shuffle</Link>
</Text>
<Text>
<Link href="/tictactoe">TicTacToe mais ou il y a eu beaucoup trop de temps de passé sur le jeux</Link>
</Text>
</main>
)
}

View File

@ -1,459 +0,0 @@
import { Button, Text, Util, NotificationManager, Col, Row, Input } from '@dzeio/components'
import { GetServerSideProps } from 'next'
import React, { MouseEvent as ReactMouseEvent } from 'react'
import css from './pokemon-shuffle.module.styl'
interface Props {
itemCount: number
boardSize: number
}
interface Cell {
id: number
id2: number
horizontalCombo?: true
verticalCombo?: true
justSpawned?: true
isFalling?: true
}
interface States {
items: Array<Array<Cell | undefined>>
loading?: true
movingItem?: {x: number, y: number, cell: Cell}
damage: number
turn: number
combo: number
comboMax: number
cursorPos: {x: number, y: number}
hitBoss: boolean
boss: {
hp: number
id: number
}
}
export default class PokemonShuffle extends React.Component<Props, States> {
public state: States = {
items: [[]],
damage: 0,
turn: 0,
combo: 0,
comboMax: 0,
cursorPos: {x: 0, y: 0},
hitBoss: false,
boss: {
hp: 10e4,
id: 2
}
}
private n = this.props.boardSize
public async componentDidMount() {
await this.start()
const boss = document.querySelector<HTMLElement>(`.${css.boss}`)
console.log(boss)
this.setState({
boss: Object.assign(this.state.boss, {pos: [boss?.offsetTop ?? 0, boss?.offsetLeft ?? 0]}),
comboMax: parseInt(window.localStorage.getItem('pokemon-shuffle/comboMax') ?? '0', 10)
})
}
public render = () => (
<main>
<form method="GET">
<Input name="boardSize" type="number" placeholder="Nombre de lignes" defaultValue={this.props.boardSize} max={10} />
<Input name="itemCount" type="number" placeholder="Nombre de pokémon différents" defaultValue={this.props.itemCount - 1} max={810} />
<Button>Changer</Button>
</form>
<ul>
<li><Text>Tour: {this.state.turn}</Text></li>
<li><Text>Combo: {this.state.combo}, Max: {this.state.comboMax}</Text></li>
<li><Text>Points: {this.state.damage}</Text></li>
</ul>
<Row align="center" >
<Col>
<Row direction="column" justify="center" align="center" >
<Col nogrow>
<Text className={Util.buildClassName(
css[`icon-${this.state.boss.id}`],
css.cell,
css.noAnimation,
css.boss,
[css.loading, this.state.hitBoss]
)}>
</Text>
</Col>
<Col nogrow>
<div className={css.bossBar}>
<div>
<div style={{width: `${Math.max(0, 100 - (100 * this.state.damage / this.state.boss.hp))}%`}}></div>
</div>
</div>
</Col>
</Row>
</Col>
<Col>
<table style={{margin: 'auto'}} className={css.table}>
<tbody className={Util.buildClassName(css.table, [css.loading, this.state.loading])}>
{this.state.items.map((row, y) => (
<tr key={y}>
{row.map((cell, x) => (
<td
key={cell?.id2 ?? x}
onClick={this.onCellClick(x, y)}
className={css.cellParent}
>
{/* <Text>{JSON.stringify(cell)}</Text> */}
{cell && (
<Text className={Util.buildClassName(
css[`icon-${cell.id}`],
css.cell,
[css.isFalling, cell.isFalling],
[css.justSpawned, cell.justSpawned],
[css.explode, cell.horizontalCombo || cell.verticalCombo]
)}>
</Text>
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</Col>
</Row>
<Button onClick={this.start}>Start!</Button>
{this.state.movingItem && (
<div className={css.hoverItem} style={{
left: this.state.cursorPos.x,
top: this.state.cursorPos.y,
// transform: 'scale(2)'
}}>
<Text className={Util.buildClassName(css[`icon-${this.state.movingItem.cell?.id}`], css.cell)}>
<div></div>
</Text>
</div>
)}
<Text>
TODO list:
</Text>
<ul>
<li><Text>Faire que les clear ce fasse de manière Async</Text></li>
<li><Text>Utiliser le système de damages de Pokémon Shuffle https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_Shuffle#Damage</Text></li>
<li><Text>Mode VS (Voir si on fait en local et/ou en ligne avec le Websocket)</Text></li>
<li><Text>Système de classement en ligne (maybe avec un compte pour eviter lees hackers lol)</Text></li>
<li><Text>Combat de boss a la Pokémon Shuffle lol</Text></li>
</ul>
<NotificationManager />
</main>
)
private mouveMove = (ev: MouseEvent) => {
this.setState({cursorPos: {
x: ev.clientX,
y: ev.clientY
}})
}
private start = async () => {
if (this.state.loading) {return}
await this.asyncSetState({
loading: true,
// generate datas
items: Array
.from(Array(this.props.boardSize))
.map(
() => Array.from(Array(this.props.boardSize))
.map(() => ({id: random(0, this.props.itemCount), id2: this.n++}))
)
})
// Quickly calculate everythings to make it look like it was perfecly generated
await this.calculate(true)
this.setState({turn: 1, damage: 0, comboMax: 0, combo: 0})
}
private onCellClick = (x: number, y: number) => async (ev: ReactMouseEvent) => {
if (this.state.loading) {
return NotificationManager.addNotification('Cant play while Calculating')
}
if (!this.state.movingItem) {
const cell = this.state.items[y][x]
if (!cell) {
return NotificationManager.addNotification('Cant move nothing')
}
document.addEventListener('mousemove', this.mouveMove)
this.setState({movingItem: {x,y,cell}})
this.state.items[y][x] = undefined
this.mouveMove(ev.nativeEvent)
return
} else {
document.removeEventListener('mousemove', this.mouveMove)
const items = this.state.items
const temp = items[y][x]
console.log(temp, this.state.movingItem)
items[y][x] = this.state.movingItem.cell
const tmpX = this.state.movingItem.x
const tmpY = this.state.movingItem.y
if (temp) {
items[tmpY][tmpX] = temp
}
this.setState({
movingItem: undefined,
loading: true,
items
}, async () => {
const revert = !await this.calculate()
if (revert) {
const movingItem = items[y][x]
items[y][x] = temp
items[tmpY][tmpX] = movingItem
this.setState({
items,
turn: this.state.turn - 1
})
}
})
}
}
private asyncSetState = (states: Partial<States>) => new Promise<void>(
(res) => this.setState(states as States, () => res())
)
/**
* Check if items has combos
* @returns if items were changed
*/
private async checkup(initial: boolean): Promise<boolean> {
const items = this.state.items
let checkupCount = 0
let newPoints = 0
for (let y = 0; y < items.length; y++) {
const row = items[y]
for (let x = 0; x < row.length; x++) {
const cell = row[x]
if (!cell) {continue}
const id = cell.id
// Checkup horizontal
if (!cell.horizontalCombo && !(cell.isFalling || cell.justSpawned)) {
let sameCount = 0
while((x + ++sameCount) < items.length) {
// console.log(y + sameCount, x)
const tmp = row[x + sameCount]
if (!tmp || tmp.id !== id || tmp.isFalling || tmp.justSpawned) {break}
}
if (sameCount >= 3) {
checkupCount += 1
let len = 0
for (let i = x; i < (x + sameCount); i++) {
const tmp = items[y][i]
if (!tmp) {continue}
tmp.horizontalCombo = true
len++
}
newPoints += calculateScore(len, this.state.combo)
}
}
// Checkup Vertical
if (!cell.verticalCombo && !(cell.isFalling || cell.justSpawned)) {
let sameCount = 0
while((y + ++sameCount) < items.length) {
// console.log(y + sameCount, x)
const tmp = items[y + sameCount][x]
if (!tmp || tmp.id !== id || tmp.isFalling || tmp.justSpawned) {break}
}
if (sameCount >= 3) {
checkupCount += 1
let len = 0
for (let i = y; i < (y + sameCount); i++) {
const tmp = items[i][x]
if (!tmp) {continue}
tmp.verticalCombo = true
len++
}
newPoints += calculateScore(len, this.state.combo)
// console.log(x, y)
}
}
}
}
// If combos were found
if (checkupCount) {
const combo = this.state.combo + checkupCount
const comboMax = Math.max(this.state.comboMax, combo)
if (comboMax === combo && !initial) {
window.localStorage.setItem('pokemon-shuffle/comboMax', comboMax.toString())
}
await this.asyncSetState({
items,
damage: this.state.damage + newPoints,
combo,
comboMax,
hitBoss: true
})
}
return !!checkupCount
}
private async endTurn(state?: Partial<States>) {
await this.asyncSetState({...state, loading: undefined, turn: this.state.turn + 1, combo: 0})
}
private async calculate(initial = false) {
// remove combos
const items = this.state.items.map((r) => r.map((c) => {
if (!c) {
return c
}
delete c.horizontalCombo
delete c.verticalCombo
return c
}))
let needContinue = false
let hadTurn = false
do {
// Make items fall
needContinue = false
for (let y = (items.length - 1); y >= 0; y--) {
const row = items[y]
for (let x = 0; x < row.length; x++) {
const cell = row[x]
if (cell) {
cell.justSpawned = undefined
cell.isFalling = undefined
}
if (cell && y+1 < row.length && !items[y+1][x]) {
cell.isFalling = true
needContinue = true
// Move cell down
items[y+1][x] = cell
items[y][x] = undefined
}
}
}
// Fill the top lane
for (let x = 0; x < items[0].length; x++) {
const cell = items[0][x]
if (!cell) {
needContinue = true
items[0][x] = {id: random(0, this.props.itemCount), id2: this.n++, justSpawned: true}
}
}
// Need to wait for the falling animation
if (needContinue) {
await this.asyncSetState({items, hitBoss: false})
if (!initial) {
await wait(300)
}
}
// Checkup if there is combos
const checkup = await this.checkup(initial)
if (!checkup && !needContinue) {
await this.endTurn({items})
break
}
// Clear items
let hasCleared = false
for (const row of items) {
for (let x = 0; x < row.length; x++) {
const cell = row[x]
if (!cell || (!cell.horizontalCombo && !cell.verticalCombo)) {continue}
row[x] = undefined
hasCleared = true
needContinue = true
}
}
if (hasCleared && !initial) {
await wait(500)
}
hadTurn = true
} while (needContinue)
return hadTurn
}
}
function calculateScore(len: number, combo: number) {
let score = (len - 2) * 40 // currently the damage
if (len > 3) {
switch (len) {
case 4:
score *= 1.5
break
case 5:
score *= 2
break
case 6:
score *= 3
break
default:
break
}
}
if (combo > 1) {
if (combo >= 2 && combo <= 4) {
score *= 1.1
}
if (combo >= 5 && combo <= 9) {
score *= 1.15
}
if (combo >= 10 && combo <= 24) {
score *= 1.2
}
if (combo >= 25 && combo <= 49) {
score *= 1.3
}
if (combo >= 50 && combo <= 74) {
score *= 1.4
}
if (combo >= 75 && combo <= 99) {
score *= 1.5
}
if (combo >= 100 && combo <= 199) {
score *= 2
}
if (combo >= 200) {
score *= 2.5
}
}
return score
}
function random(min = 0, max = 100): number {
const r = Math.floor(Math.random() * (max - min) + min)
// dont return 1 as it is the `?`
if (r === 1) {
return random(min, max)
}
return r
}
function wait(time: number): Promise<void> {
return new Promise((res) => setTimeout(() => res(), time))
}
export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
const { boardSize, itemCount } = ctx.query as Record<string, string>
return {
props: {
// add 1 to suppress the `?`
itemCount: itemCount ? parseInt(itemCount, 10) + 1 : 7,
boardSize: boardSize ? parseInt(boardSize, 10) : 7
}
}
}

View File

@ -1,6 +1,5 @@
/* eslint-disable max-classes-per-file */
import React from 'react'
import { Text, Link } from '@dzeio/components'
import GameEngine from 'GameEngine'
import Scene from 'GameEngine/Scene'
import Item from 'games/tictactoe/Item'
@ -40,15 +39,6 @@ export default class Snake extends React.PureComponent {
public render = () => (
<>
<canvas id="test" width="300" height="300"></canvas>
<Text>
<span id="debug"></span>
</Text>
<Text>Explositon animation from <Link href="https://opengameart.org/content/explosion-animations">https://opengameart.org/content/explosion-animations</Link></Text>
<Text>
Bienvenue sur le TicTacToe le plus Overengineered du monde xd<br />
Avec un moteur de jeux complètement fait maison et bien plus encore en préparation ! (vieille version en 75 lignes, nouvelle en plus de 200 lol)
</Text>
<Text>Version faites il y a 4 ans encore disponible sur Github lol <Link href="https://github.com/Aviortheking/TicTacToe">https://github.com/Aviortheking/TicTacToe</Link></Text>
</>
)