import {EventProcessor} from "./EventProcessor.js"; import {Player} from "./Player.js"; import {InventorySlot, SoundType} from "./Enums.js"; import {SoundRepository} from "./SoundRepository.js"; export class Game { #world #hud #stats #pointer #shouldRenderInsideTick #round = 1 #roundHalfTime = 2 #paused = false #started = false #options = null #readyCallback #endCallback #soundRepository #hudDebounceTicks = 1 #bombTimerId = null; #eventProcessor score = null bombDropPosition = null alivePlayers = [0, 0] buyList = [] players = [] playerMe = null playerSpectate = null #playerSlotsVisibleModels = [InventorySlot.SLOT_KNIFE, InventorySlot.SLOT_BOMB, InventorySlot.SLOT_PRIMARY, InventorySlot.SLOT_SECONDARY] constructor(world, hud, stats) { this.#world = world this.#hud = hud this.#stats = stats this.#eventProcessor = new EventProcessor(this) this.#soundRepository = new SoundRepository((...args) => world.playSound(...args)) } pause(msg, score, timeMs) { this.#paused = true console.log("Pause: " + msg + " for " + timeMs + "ms") clearInterval(this.#bombTimerId) this.#world.reset() const game = this this.players.forEach(function (player) { if (player.getId() === game.playerMe.getId()) { // reset spectate camera to our player const camera = game.#world.getCamera() camera.rotation.set(0, serverHorizontalRotationToThreeRadian(player.data.look.horizontal), 0) if (game.#pointer) { game.#pointer.reset() } player.get3DObject().getObjectByName('head').add(camera) game.playerSpectate = game.playerMe game.requestPointerLock() } else { player.get3DObject().getObjectByName('figure').visible = true } player.respawn() }) if (!this.#started) { this.#gameStartOrHalfTimeOrEnd() this.#started = true } if (this.#roundHalfTime === this.#round + 1) { this.#world.playSound('voice/blanka-last_round_of_half.mp3', null, true) } this.score = score this.#hud.pause(msg, timeMs) this.#hud.requestFullScoreBoardUpdate(this.score) } unpause() { this.#paused = false this.#hud.clearTopMessage() console.log("Game unpause") } end(msg) { console.log('Game ended') this.#gameStartOrHalfTimeOrEnd() if (this.#endCallback) { this.#endCallback(msg) } } roundStart(aliveAttackers, aliveDefenders) { console.log("Starting round " + this.#round) this.alivePlayers[0] = aliveDefenders this.alivePlayers[1] = aliveAttackers this.#hud.clearAlerts() this.#hud.roundStart(this.#options.setting.round_time_ms) } roundEnd(attackersWins, newRoundNumber, score) { let winner = attackersWins ? 'Attackers' : 'Defenders' console.log("Round " + this.#round + " ended. Round wins: " + winner) this.score = score; this.#round = newRoundNumber this.#hud.displayTopMessage(winner + ' wins') this.#hud.requestFullScoreBoardUpdate(this.score) } halfTime() { this.#gameStartOrHalfTimeOrEnd() } #gameStartOrHalfTimeOrEnd() { this.#world.playSound('538422__rosa-orenes256__referee-whistle-sound.wav', null, true) } processSound(data) { const spectatorId = this.playerSpectate.getId() if (data.type === SoundType.ITEM_ATTACK && data.player === spectatorId) { this.attackFeedback(data.item) } if (data.type === SoundType.ITEM_PICKUP) { this.#world.itemPickup(data.position, data.item, (spectatorId === data.player)) } if (data.type === SoundType.BULLET_HIT) { if (data.player) { this.#world.bulletPlayerHit(data.position, false) } else if (data.surface && (data.item.slot === InventorySlot.SLOT_PRIMARY || data.item.slot === InventorySlot.SLOT_SECONDARY)) { this.#world.bulletWallHit(data.position, data.surface, (data.item.slot === InventorySlot.SLOT_PRIMARY ? 1.2 : 0.8)) } } else if (data.type === SoundType.BULLET_HIT_HEADSHOT) { this.#world.bulletPlayerHit(data.position, true) } if (data.type === SoundType.ITEM_DROP) { this.#world.itemDrop(data.position, data.item) if (data.player === spectatorId) { this.dropFeedback(data.item) } if (data.item.slot === InventorySlot.SLOT_BOMB) { this.bombDropPosition = data.position } } if (data.type === SoundType.BOMB_DEFUSED) { clearInterval(this.#bombTimerId) } this.#soundRepository.play(data, spectatorId) } bombPlanted(timeMs, position) { const world = this.#world world.spawnBomb(position) this.bombDropPosition = position const bombSecCount = Math.round(timeMs / 1000) this.#hud.bombPlanted(bombSecCount) const tenSecWarningSecCount = Math.round(timeMs / 1000 - 10) let tickSecondsCount = 0; let bombTimerId = setInterval(function () { if (tickSecondsCount === bombSecCount) { clearInterval(bombTimerId) } if (tickSecondsCount === tenSecWarningSecCount) { world.playSound('88532__northern87__woosh-northern87.wav', null, true) } world.playSound('536422__rudmer-rotteveel__setting-electronic-timer-1-beep.wav', position, false) tickSecondsCount++; }, 1000) this.#bombTimerId = bombTimerId } isPaused() { return this.#paused } isPlaying() { return this.#started } onReady(callback) { this.#readyCallback = callback } onEnd(callback) { this.#endCallback = callback } gameStart(options) { this.#options = options this.#roundHalfTime = Math.floor(options.setting.max_rounds / 2) + 1 this.#hud.startWarmup(options.warmupSec * 1000) const playerId = options.playerId if (this.players[playerId]) { throw new Error("My Player is already set!") } this.playerMe = new Player(options.player, this.#world.createPlayerMe()) this.players[playerId] = this.playerMe; this.playerSpectate = this.playerMe if (this.#readyCallback) { this.#readyCallback(this.#options) } } playerKilled(playerIdDead, playerIdCulprit, wasHeadshot, killItemId) { const culpritPlayer = this.players[playerIdCulprit] const deadPlayer = this.players[playerIdDead] deadPlayer.died() this.alivePlayers[deadPlayer.getTeamIndex()]-- this.#hud.showKill( culpritPlayer.data, deadPlayer.data, wasHeadshot, this.playerMe.data, killItemId ) if (playerIdDead === this.playerSpectate.getId()) { this.requestPointerUnLock() this.spectatePlayer() } } spectatePlayer(directionNext = true) { if (this.playerMe.isAlive() || this.alivePlayers[this.playerMe.getTeamIndex()] === 0) { return } const myId = this.playerSpectate.getId() let aliveAvailableSpectateMates = this.getMyTeamPlayers().filter((player) => player.isAlive() && myId !== player.getId()) if (aliveAvailableSpectateMates.length === 0) { return; } let ids = aliveAvailableSpectateMates.map((player) => player.getId()).sort() if (!directionNext) { ids.reverse() } let playerId = ids.find((id) => myId > id) if (!playerId) { playerId = ids.shift() } const camera = this.#world.getCamera() camera.rotation.set(0, degreeToRadian(-90), 0) const player = this.players[playerId] player.get3DObject().getObjectByName('head').add(camera) player.get3DObject().getObjectByName('figure').visible = false if (this.playerSpectate.isAlive()) { this.playerSpectate.get3DObject().getObjectByName('figure').visible = true } this.playerSpectate = player this.equip(player.getEquippedSlotId()) } createPlayer(data) { const player = new Player(data, this.#world.spawnPlayer(data.color, this.playerMe.isAttacker() !== data.isAttacker)) if (this.players[data.id]) { throw new Error('Player already exist with id ' + data.id) } this.players[data.id] = player return player } attackFeedback(item) { if (this.playerSpectate.data.ammo > 0) { this.#hud.showShot(item) } } dropFeedback(item) { this.#hud.showDropAnimation(item) } equip(slotId) { if (!this.playerSpectate.data.slots[slotId]) { return false } this.playerSpectate.equip(slotId) const item = this.playerSpectate.data.slots[slotId] const povItems = this.#world.getCamera().getObjectByName('pov-item') povItems.children.forEach((mesh) => mesh.visible = false) let model = povItems.getObjectByName(`item-${item.id}`) if (!model) { model = this.#world.getModelForItem(item) povItems.add(model) } model.position.set(0, 0, 0) model.rotation.set(0, 0, 0) model.visible = true this.#hud.equip(slotId, this.playerSpectate.data.slots) return true } tick(state) { this.#stats.begin() const game = this if (this.#options !== null) { state.players.forEach(function (serverState) { let player = game.players[serverState.id] if (player === undefined) { player = game.createPlayer(serverState) } game.updatePlayerData(player, serverState) }) } state.events.forEach(function (event) { game.#eventProcessor.process(event) }) if (this.#shouldRenderInsideTick) { this.#render() } this.#stats.end() } updatePlayerData(player, serverState) { player.get3DObject().getObjectByName('head').position.y = serverState.heightSight player.get3DObject().position.set(serverState.position.x, serverState.position.y, -serverState.position.z) if (player.data.isAttacker === this.playerMe.data.isAttacker) { // if player on my team if (player.data.money !== serverState.money) { this.#hud.updateMyTeamPlayerMoney(player.data, serverState.money) } player.updateData(serverState) } else { player.data.item = serverState.item player.data.isAttacker = serverState.isAttacker } if (this.playerMe.getId() === serverState.id || this.playerSpectate.getId() === serverState.id) { if (this.playerSpectate.isInventoryChanged(serverState)) { this.equip(serverState.item.slot) } } if (this.playerMe.getId() !== serverState.id) { this.updateOtherPlayersModels(player, serverState) } } updateOtherPlayersModels(player, data) { const playerObject = player.get3DObject() playerObject.rotation.y = serverHorizontalRotationToThreeRadian(data.look.horizontal) const rotationVertical = this.playerMe.isAlive() ? Math.max(Math.min(data.look.vertical, 50), -50) : data.look.vertical // cap visual rotation if playerMe is alive to not see broken neck const rotationVerticalThree = serverVerticalRotationToThreeRadian(rotationVertical) playerObject.getObjectByName('head').rotation.x = rotationVerticalThree const hand = playerObject.getObjectByName('hand') if (hand.children.length) { hand.children[0].rotation.y = rotationVerticalThree } const body = playerObject.getObjectByName('body') if (body.position.y !== data.heightBody) { // update body height position if changed body.position.y = data.heightBody } if (player.isInventoryChanged(data)) { this.#otherPlayersInventoryChanged(player, data) player.equip(data.item.slot) } } #otherPlayersInventoryChanged(player, data) { const world = this.#world const hand = player.get3DObject().getObjectByName('hand') const belt = player.get3DObject().getObjectByName('belt'); if (hand.children.length === 1) { const lastHandItemModel = hand.children[0] belt.getObjectByName(`slot-${lastHandItemModel.userData.slot}`).add(lastHandItemModel) } else if (hand.children.length > 1) { throw new Error("Too many item in hands?") } this.#playerSlotsVisibleModels.forEach(function (slotId) { const item = data.slots[slotId] const beltSlot = belt.getObjectByName(`slot-${slotId}`) beltSlot.children.forEach((model) => model.visible = false) if (!item) { // do not have slotID filled return } let itemModel = beltSlot.getObjectByName(`item-${item.id}`) if (!itemModel) { itemModel = world.getModelForItem(item) beltSlot.add(itemModel) } itemModel.position.set(0, 0, 0) itemModel.rotation.set(0, 0, 0) itemModel.visible = true }) const modelInHand = belt.getObjectByName(`slot-${data.item.slot}`).getObjectByName(`item-${data.item.id}`) hand.add(modelInHand) modelInHand.position.set(0, 0, 0) modelInHand.rotation.set(0, 0, 0) modelInHand.visible = true modelInHand.userData.slot = data.item.slot } getMyTeamPlayers() { let meIsAttacker = this.playerMe.isAttacker() return this.players.filter((player) => player.isAttacker() === meIsAttacker) } meIsAlive() { return this.playerMe.isAlive() } meIsSpectating() { return (!this.meIsAlive()) } setDependency(pointer, renderWorldInsideTick) { this.#pointer = pointer this.#shouldRenderInsideTick = renderWorldInsideTick } getPlayerMeRotation() { return threeRotationToServer(this.#pointer.getObject().rotation) } getPlayerSpectateRotation() { if (this.playerSpectate.getId() === this.playerMe.getId()) { return this.getPlayerMeRotation() } return [this.playerSpectate.data.look.horizontal, this.playerSpectate.data.look.vertical] } requestPointerLock() { if (this.#pointer.isLocked || (this.playerMe && this.playerMe.getId() !== this.playerSpectate.getId())) { return } this.#pointer.lock() } requestPointerUnLock() { if (!this.#pointer.isLocked) { return } this.#pointer.unlock() } #render() { if (this.#started && --this.#hudDebounceTicks === 0) { this.#hudDebounceTicks = 4 this.#hud.updateHud(this.playerSpectate.data) } this.#world.render() } }