mirror of
https://github.com/Aviortheking/games.git
synced 2025-04-25 04:12:08 +00:00
461 lines
12 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|