diff --git a/public/assets/tictactoe/O.png b/public/assets/tictactoe/O.png new file mode 100644 index 0000000..d527231 Binary files /dev/null and b/public/assets/tictactoe/O.png differ diff --git a/public/assets/tictactoe/X.png b/public/assets/tictactoe/X.png new file mode 100644 index 0000000..4fc3e71 Binary files /dev/null and b/public/assets/tictactoe/X.png differ diff --git a/public/assets/tictactoe/bip.wav b/public/assets/tictactoe/bip.wav new file mode 100644 index 0000000..e3d11be Binary files /dev/null and b/public/assets/tictactoe/bip.wav differ diff --git a/public/assets/tictactoe/victory.wav b/public/assets/tictactoe/victory.wav new file mode 100644 index 0000000..0baeebf Binary files /dev/null and b/public/assets/tictactoe/victory.wav differ diff --git a/src/GameEngine.ts b/src/GameEngine.ts new file mode 100644 index 0000000..5b7776e --- /dev/null +++ b/src/GameEngine.ts @@ -0,0 +1,245 @@ +/* eslint-disable max-classes-per-file */ +export default class GameEngine { + public ctx: CanvasRenderingContext2D + public canvas: HTMLCanvasElement + public caseSize: [number, number] = [1, 1] + public cursor: { + x: number + y: number + isDown: boolean + wasDown: boolean + } = { + x: 0, + y: 0, + isDown: false, + wasDown: false + } + private currentScene?: Scene + private isRunning = false + + public constructor( + private id: string, + private options?: { + caseCount?: number | [number, number] + background?: string + } + ) { + const canvas = document.querySelector(id) + if (!canvas) { + throw new Error('Error, canvas not found!') + } + this.canvas = canvas + if (this.options?.caseCount) { + this.caseSize = [ + // @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) + ] + } + + const ctx = canvas.getContext('2d') + if (!ctx) { + throw new Error('Error, Context could not get found!') + } + ctx.imageSmoothingEnabled = false + this.ctx = ctx + } + + public start() { + if (this.isRunning) { + console.warn('Game is already running') + return + } + this.isRunning = true + requestAnimationFrame(() => { + this.update() + }) + document.addEventListener('mousemove', (ev) => { + this.cursor.x = ev.clientX + this.cursor.y = ev.clientY + if (this.cursor.isDown) { + this.cursor.wasDown = true + } + }) + document.addEventListener('mousedown', () => { + this.cursor.isDown = true + }) + document.addEventListener('mouseup', () => { + this.cursor.isDown = false + this.cursor.wasDown = false + }) + } + + public pause() { + this.isRunning = false + } + + public setScene(scene: Scene | string) { + this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene + this.currentScene.setGameEngine(this) + } + + + private update() { + 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() + setTimeout(() => { + this.update() + }, 0) + } +} + + +export class SoundManager { + private audio: HTMLAudioElement + public constructor(path: string) { + this.audio = new Audio(path) + this.audio.load() + this.audio.volume = .8 + } + + public play() { + this.audio.play() + } + + public end() { + this.audio.pause() + } +} + +export class AssetsManager { + + public static assets: Record = {} + + public isLoaded = false + + private image: HTMLImageElement + + private constructor( + private path: string + ) { + this.image = new Image() + } + + public static init(path: string) { + if (!this.assets[path]) { + this.assets[path] = new AssetsManager(path) + } + return this.assets[path] + } + + public async load() { + return new Promise((res, rej) => { + this.image.src = this.path + this.image.onload = () => { + this.isLoaded = true + res() + } + }) + } + + public async get() { + if (!this.isLoaded) { + await this.load() + } + return this.image + } +} + +export class Scene { + public static scenes: Record = {} + + public background?: string + + private components: Array = [] + private ge!: GameEngine + + public constructor(sceneId: string) { + Scene.scenes[sceneId] = this + } + + public addComponent(...cp: Array) { + return this.components.push(...cp) + } + + public setGameEngine(ge: GameEngine) { + this.ge = ge + } + + public async init() { + this.components.forEach((v) => { + if (v.init) { + v.init() + } + }) + } + + public async update() { + this.components.forEach(async (v) => { + const state: Partial = {} + const width = (v.size?.width ?? 1) * this.ge.caseSize[0] + const height = (v.size?.height ?? 1) * this.ge.caseSize[1] + 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.display && v.pos) { + const ax = v.pos.x * this.ge.caseSize[0] + const ay = v.pos.y * this.ge.caseSize[1] + if (v.display.type === 'color') { + this.ge.ctx.fillStyle = v.display.source + this.ge.ctx.fillRect( + ax, + ay, + width, + height + ) + } else if (v.display.type === 'image') { + this.ge.ctx.drawImage( + await AssetsManager.init(v.display.source).get(), + ax, + ay, + width, + height + ) + } + } + if (v.update) { + v.update(state as ComponentState) + } + }) + } +} + +export interface ComponentState { + mouseHovering: boolean + mouseClicking: boolean + mouseClicked: boolean +} + +export abstract class Component2D { + public id?: number + public display?: { + type: 'image' | 'color' + source: string + } + public pos?: {x: number, y: number, z?: number, rotation?: number} + public size?: {width: number, height: number} + + public init?(): Promise | void + + public update?(state: ComponentState): Promise | void +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index faad683..b38d696 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,6 +9,9 @@ export default class Index extends React.Component { Pokémon Shuffle + + TicTacToe mais ou il y a eu beaucoup trop de temps de passé sur le jeux + ) } diff --git a/src/pages/tictactoe/Game.ts b/src/pages/tictactoe/Game.ts new file mode 100644 index 0000000..21da913 --- /dev/null +++ b/src/pages/tictactoe/Game.ts @@ -0,0 +1,161 @@ +/* eslint-disable max-classes-per-file */ +import GameEngine, { Component2D, ComponentState, Scene, SoundManager } from 'GameEngine' + +const globalState: { + playerTurn: 'X' | 'O' + gameState: [ + [string, string, string], + [string, string, string], + [string, string, string], + ] +} = { + playerTurn: 'X', + gameState: [ + ['', '', ''], + ['', '', ''], + ['', '', ''] + ] +} + +class Item extends Component2D { + + public size = { + width: .9, + height: .9 + } + + private x: number + private y: number + + public constructor(index: number) { + super() + this.x = Math.trunc(index % 3) + this.y = Math.trunc(index / 3) + this.pos = { + x: this.x + this.x * .05, + y: this.y + this.y * .05 + } + console.log(this.pos, index) + } + + public init() { + console.log('item initialized') + } + + public update(state: ComponentState) { + // console.log(state) + const value: '' | 'X' | 'O' = globalState.gameState[this.x][this.y] as '' | 'X' | 'O' + if (state.mouseClicked && value === '') { + // console.log('hovering') + this.onClick() + } + + if (value === 'X') { + this.display = { + type: 'image', + source: '/assets/tictactoe/X.png' + } + } else if(value === 'O') { + this.display = { + type: 'image', + source: '/assets/tictactoe/O.png' + } + } + } + + private onClick() { + const clickSound = new SoundManager('/assets/tictactoe/bip.wav') + clickSound.play() + globalState.gameState[this.x][this.y] = globalState.playerTurn + console.log(this.checkVictory()) + if (this.checkVictory()) { + new SoundManager('/assets/tictactoe/victory.wav').play() + // globalState.gameState = [ + // ['', '', ''], + // ['', '', ''], + // ['', '', ''] + // ] + } + globalState.playerTurn = globalState.playerTurn === 'X' ? 'O' : 'X' + } + + // uh MAYBE refactor lol + private checkVictory() { + if(globalState.gameState[0][0] === globalState.playerTurn) { + if(globalState.gameState[1][0] === globalState.playerTurn) { + if(globalState.gameState[2][0] === globalState.playerTurn) { + return true + } + } + if(globalState.gameState[0][1] === globalState.playerTurn) { + if(globalState.gameState[0][2] === globalState.playerTurn) { + return true + } + } + if(globalState.gameState[1][1] === globalState.playerTurn) { + if(globalState.gameState[2][2] === globalState.playerTurn) { + return true + } + } + } + if(globalState.gameState[2][2] === globalState.playerTurn) { + if(globalState.gameState[2][1] === globalState.playerTurn) { + if(globalState.gameState[2][0] === globalState.playerTurn) { + return true + } + } + if(globalState.gameState[1][2] === globalState.playerTurn) { + if(globalState.gameState[0][2] === globalState.playerTurn) { + return true + } + } + } + if(globalState.gameState[1][1] === globalState.playerTurn) { + if(globalState.gameState[0][1] === globalState.playerTurn) { + if(globalState.gameState[2][1] === globalState.playerTurn) { + return true + } + } + if(globalState.gameState[1][0] === globalState.playerTurn) { + if(globalState.gameState[1][2] === globalState.playerTurn) { + return true + } + } + } + return false + } +} + +class Line extends Component2D { + + public constructor(direction: number, index: number) { + super() + this.display = { + type: 'color', + source: 'orange' + } + this.pos = { + x: direction ? index ? 1.95 : .9 : 0, + y: direction ? 0 : index ? .9 : 1.95 + } + this.size = { + width: direction ? .15 : 3, + height: direction ? 3 : .15 + } + } + +} + +const ge = new GameEngine('#test', { + caseCount: 3, + background: 'blue' +}) +const scene = new Scene('TicTacToe') +scene.addComponent( + ...Array.from(new Array(2)).map((_, index) => new Line(0, index)), + ...Array.from(new Array(2)).map((_, index) => new Line(1, index)), + ...Array.from(new Array(9)).map((_, index) => new Item(index)), +) + +ge.start() +ge.setScene(scene) diff --git a/src/pages/tictactoe/index.tsx b/src/pages/tictactoe/index.tsx new file mode 100644 index 0000000..4883fde --- /dev/null +++ b/src/pages/tictactoe/index.tsx @@ -0,0 +1,15 @@ +import { Text, Link } from '@dzeio/components' +import React from 'react' +export default class Snake extends React.PureComponent { + public async componentDidMount() { + await import('./Game') + + } + public render = () => ( + <> + + Bienvenue sur le TicTacToe le plus Overengineered du monde xd
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)
+ Version faites il y a 4 ans encore disponible sur Github lol https://github.com/Aviortheking/TicTacToe + + ) +}