Florian Bouillon 26df50c4a8
feat: Upgraded Engine
Signed-off-by: Avior <f.bouillon@aptatio.com>
2023-01-18 16:05:07 +01:00

461 lines
12 KiB
TypeScript

import { Button, Col, Input, NotificationManager, Row, Text, Util } 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)
const items = this.state.items
items[y][x] = undefined
this.setState({movingItem: {x,y,cell}, items})
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
}
}
}