This repository has been archived on 2026-05-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
instructions-clear/public/client.js
T
2025-01-25 00:06:48 +09:00

728 lines
26 KiB
JavaScript

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { Tween } from '@tweenjs/tween.js'
import * as THREE from 'three'
import Stats from 'stats.js'
const global = (0,eval)('this')
const scene = new THREE.Scene()
const raycaster = new THREE.Raycaster()
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
const clock = new THREE.Clock()
const renderer = new THREE.WebGLRenderer()
const backgroundColor = new THREE.Color().setHex(0x112233)
scene.background = backgroundColor
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(render)
const cameraOffsetX = 0
const cameraOffsetY = -13.5
const cameraOffsetZ = 20
camera.position.set(cameraOffsetX, cameraOffsetY, cameraOffsetZ)
camera.rotation.set((34 / 180) * Math.PI, 0, 0)
camera.zoom += 0.2
camera.updateProjectionMatrix()
camera.layers.enable(1)
camera.layers.enable(2)
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xcccccc })
const terrainMaterial = new THREE.MeshToonMaterial({ color: 0x5c4033 })
const passableTerrainMaterial = new THREE.MeshToonMaterial({ color: 0x228822, transparent: true, opacity: 0.65 })
const opacity = 0.3
const teamMaterials = {
blue: new THREE.MeshToonMaterial({ color: 0x4444ff }),
blueTransparent: new THREE.MeshToonMaterial({ color: 0x4444ff, transparent: true, opacity }),
neutral: new THREE.MeshToonMaterial({ color: 0xcccccc }),
neutralTransparent: new THREE.MeshToonMaterial({ color: 0xcccccc, transparent: true, opacity }),
red: new THREE.MeshToonMaterial({ color: 0xff4444 }),
redTransparent: new THREE.MeshToonMaterial({ color: 0xff4444, transparent: true, opacity }),
projectile: new THREE.MeshToonMaterial({ color: 0xff00ff, transparent: true, opacity }),
range: new THREE.MeshToonMaterial({ color: 0x00ffff, transparent: true, opacity: opacity / 2 }),
visionRange: new THREE.MeshToonMaterial({ color: 0x226022 }),
// visionRange: new THREE.MeshToonMaterial({ color: 0x00ffff, transparent: true, opacity: opacity / 6 }),
}
// TODO: draw lines of path for minimap camera
const minimapCameraZ = 10
const minimapCamera = new THREE.OrthographicCamera(-10, 10, 10, -10)
const minimapRenderer = new THREE.WebGLRenderer()
minimapRenderer.setSize(300, 300)
minimapRenderer.setAnimationLoop(minimapRender)
minimapCamera.position.set(10, 10, 10)
const animationActions = {}
const entities = {}
const gltf = {}
const mixers = {}
const positionTweens = {}
const projectiles = {}
const rotationTweens = {}
const terrains = {}
var state = { abilities: [], entities: [], terrains: [], projectiles: [] }
global.animationActions = animationActions
global.entities = entities
global.gltf = gltf
global.mixers = mixers
global.projectiles = projectiles
global.state = state
global.terrains = terrains
const gltfLoader = new GLTFLoader()
const preloadGLTF = function loadTemplate(path) {
gltfLoader.load(path, (loadedGLTF) => gltf[path] = loadedGLTF)
}
const addGLTF = function addGLTF(scene, path, id, additionalSteps = function noAdditionalSteps() {}) {
if (gltf[path] == null) {
setTimeout(() => addGLTF(scene, path, id, additionalSteps), 200)
return
}
const scale = 2
const model = gltf[path].scene.clone()
const mixer = new THREE.AnimationMixer(model)
mixers[id] = mixer
animationActions[id] = {}
gltf[path].animations.forEach((it) => {
const animation = mixer.clipAction(it)
animationActions[id][it.name] = animation
})
model.scale.set(scale, scale, scale)
additionalSteps(model)
scene.add(model)
}
const geometry = new THREE.PlaneGeometry(0, 0)
const material = new THREE.MeshToonMaterial({ color: 0x115011 })
const ground = new THREE.Mesh(geometry, material)
scene.add(ground)
const ambientLight = new THREE.AmbientLight(0x404040, 10)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5)
directionalLight.position.set(-0.5, -0.05, 1)
directionalLight.power = 3000
scene.add(directionalLight)
global.THREE = THREE
global.renderer = renderer
global.camera = camera
global.scene = scene
var tweenDuration = 1
const keysDown = {}
const mouse = {}
var stats = new Stats()
stats.showPanel(0)
var delta = 0
function render() {
stats.begin()
delta = clock.getDelta()
cameraMovement()
Object.values(positionTweens).forEach((tween) => tween.update())
Object.values(rotationTweens).forEach((tween) => tween.update())
Object.values(mixers).forEach((mixer) => mixer.update(delta))
renderer.render(scene, camera)
stats.end()
}
function minimapRender() {
minimapRenderer.render(scene, minimapCamera)
}
const lockedCameraSpeedMultiplier = 3
var cameraLocked = true
function followCamera() {
const entity = entities[playerId]
if (entity == null) { return }
const cameraSpeed = lockedCameraSpeedMultiplier * delta
const distanceX = Math.abs((entity.position.x + cameraOffsetX) - camera.position.x)
const distanceY = Math.abs((entity.position.y + cameraOffsetY) - camera.position.y)
camera.position.z = cameraOffsetZ
if (distanceX > 0.01) {
if (entity.position.x + cameraOffsetX > camera.position.x) {
camera.position.x += cameraSpeed * distanceX
}
if (entity.position.x + cameraOffsetX < camera.position.x) {
camera.position.x -= cameraSpeed * distanceX
}
}
else if (distanceX != 0) {
camera.position.x = entity.position.x + cameraOffsetX
}
if (distanceY > 0.01) {
if (entity.position.y + cameraOffsetY > camera.position.y) {
camera.position.y += cameraSpeed * distanceY
}
if (entity.position.y + cameraOffsetY < camera.position.y) {
camera.position.y -= cameraSpeed * distanceY
}
}
else if (distanceY != 0) {
camera.position.y = entity.position.y + cameraOffsetY
}
}
const cameraSpeedMultiplier = 10
function cameraMovement() {
if (cameraLocked) {
followCamera()
return
}
const cameraSpeed = cameraSpeedMultiplier * delta
if (keysDown.ArrowLeft) { camera.position.x -= cameraSpeed }
else if (keysDown.ArrowRight) { camera.position.x += cameraSpeed }
if (keysDown.ArrowUp) { camera.position.y += cameraSpeed }
else if (keysDown.ArrowDown) { camera.position.y -= cameraSpeed }
if (keysDown.Space) {
camera.position.set(entities[playerId].position.x + cameraOffsetX, entities[playerId].position.y + cameraOffsetY, cameraOffsetZ)
}
}
function raycastToGround() {
const canvas = renderer.domElement
raycaster.setFromCamera(new THREE.Vector2((mouse.x / canvas.clientWidth) * 2 - 1, (mouse.y / canvas.clientHeight) * -2 + 1), camera)
const intersect = raycaster.intersectObject(ground).at(0)?.point
if (intersect != null) {
return {
x: Math.round(intersect.x * 100),
y: Math.round(intersect.y * 100),
}
}
return null
}
var websocket = null
global.websocket = null
var timerId = null
var playerId = null
var playerTeam = null
function connectWebSocket() {
websocket = new WebSocket(`ws://${window.location.hostname}:1280/ws`)
global.websocket = websocket
websocket.onerror = () => websocket.close()
websocket.onopen = () => {
document.getElementById('connection').innerHTML = 'open'
clearInterval(timerId)
websocket.send(JSON.stringify({ action: 'join', id: playerId }))
}
websocket.onclose = () => {
websocket = null
document.getElementById('connection').innerHTML = 'closed'
timerId = setInterval(() => {
if (websocket == null) {
connectWebSocket()
}
}, 2000)
}
websocket.onmessage = (event) => {
state.byteSize = new Blob([event.data]).size
const stateUpdates = JSON.parse(event.data)
if (stateUpdates.tickRate != null) {
tweenDuration = 1000 / stateUpdates.tickRate
}
if (stateUpdates.width != null && stateUpdates.height != null && stateUpdates.width != (state.width ?? -1) && stateUpdates.height != (state.height ?? -1) ) {
ground.geometry = new THREE.PlaneGeometry(stateUpdates.width / 100, stateUpdates.height / 100)
ground.position.set(stateUpdates.width / 200, stateUpdates.height / 200, 0)
}
if (stateUpdates.width != null && stateUpdates.height != null) {
state.width = stateUpdates.width
state.height = stateUpdates.height
minimapCamera.top = state.height / 200
minimapCamera.right = state.width / 200
minimapCamera.bottom = -state.height / 200
minimapCamera.left = -state.width / 200
minimapCamera.updateProjectionMatrix()
minimapCamera.position.set(state.width / 200, state.height / 200, minimapCameraZ)
const size = 300
const wide = state.width > state.height
minimapRenderer.setSize(
wide ? size : (state.width / state.height) * size,
wide ? (state.height / state.width) * size : size,
)
}
for (const [key, value] of Object.entries(stateUpdates)) {
if (!['abilities', 'terrains', 'entities', 'projectiles', 'width', 'height'].includes(key)) {
state[key] = value
}
}
if (stateUpdates.abilities != null) {
const ids = stateUpdates.abilities.map((it) => it.id)
state.abilities = state.abilities.filter((it) => ids.includes(it.id))
for (const ability of stateUpdates.abilities ?? []) {
const index = state?.abilities?.findIndex((it) => it.id == ability.id)
if (index > -1) {
state.abilities[index] = {...state.abilities[index], ...ability}
}
else {
state.abilities.push(ability)
}
}
}
if (stateUpdates.entities != null) {
const ids = stateUpdates.entities.map((it) => it.id)
state.entities = state.entities.filter((it) => ids.includes(it.id))
for (const entity of stateUpdates.entities ?? []) {
const index = state?.entities?.findIndex((it) => it.id == entity.id)
if (index > -1) {
state.entities[index] = {...state.entities[index], ...entity}
}
else {
state.entities.push(entity)
}
}
}
if (stateUpdates.terrains != null) {
const ids = stateUpdates.terrains.map((it) => it.id)
state.terrains = state.terrains.filter((it) => ids.includes(it.id))
for (const terrain of stateUpdates.terrains ?? []) {
const index = state?.terrains?.findIndex((it) => it.id == terrain.id)
if (index > -1) {
state.terrains[index] = {...state.terrains[index], ...terrain}
}
else {
state.terrains.push(terrain)
}
}
}
if (stateUpdates.projectiles != null) {
const ids = stateUpdates.projectiles.map((it) => it.id)
state.projectiles = state.projectiles.filter((it) => ids.includes(it.id))
for (const projectile of stateUpdates.projectiles) {
const index = state?.projectiles?.findIndex((it) => it.id == projectile.id)
if (index > -1) {
state.projectiles[index] = {...state.projectiles[index], ...projectile}
}
else {
state.projectiles.push(projectile)
}
}
}
for (const e of Object.values(entities)) {
e.userData.flaggedForRemoval = true
}
for (const e of state.entities ?? []) {
let entity
let created = false
if (e.id == playerId && playerTeam != e.team) {
playerTeam = e.team
}
if (e.id in entities) {
entity = entities[e.id]
}
else {
// const entityMaterial = teamMaterials[e.team]
// entity = new THREE.Mesh(new THREE.CylinderGeometry(e.visualRadius / 100, e.visualRadius / 100, e.height / 50), entityMaterial)
// TODO: change entity material
created = true
entity = new THREE.Group()
entity.rotation.x = Math.PI / 2
entity.scale.set(e.visualRadius / 100, e.visualRadius / 100, e.visualRadius / 100)
entity.userData.type = 'entity'
entity.userData.id = e.id
entity.position.set(e.position.x / 100, e.position.y / 100, 0)
scene.add(entity)
const hpMargin = 0.5
const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 }))
maxHp.position.set(0, 0, 0)
maxHp.scale.set(1.5, 0.2, 1)
maxHp.layers.set(1)
entity.add(maxHp)
const hp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0x77ff77 }))
hp.position.set(0, 0, 0)
hp.scale.set(1, 1, 1)
hp.layers.set(1)
maxHp.add(hp)
const teamMaterial = teamMaterials[`${e.team}Transparent`]
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry(1, 0.00001, 1), teamMaterial)
teamMarker.position.y = -0.493
entity.add(teamMarker)
const buffMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, transparent: true, opacity: 0.4 })
const buffMarker = new THREE.Mesh(new THREE.TorusGeometry(0.95, 0.15), buffMaterial)
buffMarker.rotation.x = Math.PI / 2
buffMarker.layers.set(1)
buffMarker.visible = false
entity.add(buffMarker)
const rangeMaterial = teamMaterials['range']
const rangeSize = (state.abilities.find((it) => it.id == e.abilities?.a)?.range ?? 0) + e.radius
const rangeMarker = new THREE.Mesh(new THREE.CylinderGeometry(rangeSize / e.visualRadius, rangeSize / e.visualRadius, 0.001), rangeMaterial)
rangeMarker.position.y = 0.004
rangeMarker.layers.set(1)
rangeMarker.visible = false
entity.add(rangeMarker)
const modelRotationBase = new THREE.Object3D()
modelRotationBase.rotation.y = e.rotation - (Math.PI / 2)
modelRotationBase.layers.set(1)
entity.add(modelRotationBase)
const visionRangeMaterial = teamMaterials['visionRange']
const visionRangeSize = e.visionRange ?? 0
const visionRangeMarker = new THREE.Mesh(new THREE.CylinderGeometry(visionRangeSize / e.visualRadius, visionRangeSize / e.visualRadius, 0.001), visionRangeMaterial)
visionRangeMarker.position.y = 0.002
visionRangeMarker.layers.set(1)
visionRangeMarker.visible = false
entity.add(visionRangeMarker)
if (e.model != null) {
addGLTF(modelRotationBase, e.model, e.id, function(model) {
const box = new THREE.Box3().setFromObject(model)
const size = box.getSize(new THREE.Vector3())
maxHp.position.set(0, size.y + hpMargin, 0)
buffMarker.position.y = size.y / 2
buffMarker.scale.z = size.y / 10
})
}
entities[e.id] = entity
}
entity.children.at(0).visible = !e.dead
entity.children.at(1).visible = !e.dead
entity.children.at(2).visible = !e.dead && e.buffs.some((it) => it.id == 'exposed') // TODO: only works for Exposed now
const animations = animationActions[e.id] ?? {}
const fadeIn = created ? 0 : 0.15
if (e.dead) {
if (!animations.dead?.isRunning()) {
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
animations.dead?.reset().fadeIn(fadeIn).play()
}
}
else if (e.casting != null) {
if (!animations.cast?.isRunning()) {
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
animations.cast?.reset().fadeIn(fadeIn).play()
}
}
else {
if (!animations.default?.isRunning()) {
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
animations.default?.reset().fadeIn(fadeIn).play()
}
}
entity.userData.flaggedForRemoval = false
const oldRotationY = entity.children.at(4).rotation.y
const newRotationY = e.rotation - (Math.PI / 2)
if (Math.abs((oldRotationY - (2 * Math.PI)) - newRotationY) < Math.abs(oldRotationY - newRotationY)) {
entity.children.at(4).rotation.y = oldRotationY - (2 * Math.PI)
}
if (Math.abs((oldRotationY + (2 * Math.PI)) - newRotationY) < Math.abs(oldRotationY - newRotationY)) {
entity.children.at(4).rotation.y = oldRotationY + (2 * Math.PI)
}
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z: 0 }, tweenDuration).start()
rotationTweens[entity.id] = new Tween(entity.children.at(4).rotation).to({ x: 0, y: newRotationY, z: 0 }, tweenDuration).start()
const hp = entity.children.at(0).children.at(0)
const percentageHp = e.health / e.maxHealth
hp.scale.x = percentageHp
hp.position.x = -(1 - percentageHp) / 2
entity.children.at(3).visible = !e.dead && e.id == playerId
// entity.children.at(5).visible = !e.dead && e.team == playerTeam
}
for (const e of Object.values(entities)) {
if (e.userData.flaggedForRemoval) {
scene.remove(e)
delete animationActions[e.userData.id]
delete entities[e.userData.id]
delete mixers[e.userData.id]
delete positionTweens[e.userData.id]
delete rotationTweens[e.userData.id]
}
}
for (const p of Object.values(projectiles)) {
p.userData.flaggedForRemoval = true
}
for (const p of state.projectiles ?? []) {
let projectile
if (p.id in projectiles) {
projectile = projectiles[p.id]
}
else {
projectile = new THREE.Mesh(new THREE.SphereGeometry(p.visualRadius / 100), projectileMaterial)
projectile.userData.type = 'projectile'
projectile.userData.id = p.id
projectile.position.set(p.position.x / 100, p.position.y / 100, p.height / 100)
projectile.layers.set(2)
scene.add(projectile)
projectile.rotation.x = Math.PI / 2 // needed for the team marker...
const teamMaterial = teamMaterials[`${p.team}Transparent`] ?? teamMaterials['projectile']
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((p.radius) / 100, (p.radius) / 100, 1), teamMaterial)
const teamMarkerSize = 4000
teamMarker.scale.y = p.height / teamMarkerSize
teamMarker.position.y = (p.height / (teamMarkerSize * 2)) - (p.height / 100)
teamMarker.position.y += 0.01
teamMarker.layers.set(2)
projectile.add(teamMarker)
projectiles[p.id] = projectile
}
projectile.userData.flaggedForRemoval = false
positionTweens[projectile.id] = new Tween(projectile.position).to({ x: p.position.x / 100, y: p.position.y / 100, z: p.height / 100 }, tweenDuration).start()
}
for (const p of Object.values(projectiles)) {
if (p.userData.flaggedForRemoval) {
scene.remove(p)
delete projectiles[p.userData.id]
delete positionTweens[p.userData.id]
}
}
for (const t of state.terrains ?? []) {
let terrain
if (t.id in terrains) {
terrain = terrains[t.id]
}
else {
const vertices = t.relativeVertices
const shape = new THREE.Shape()
shape.moveTo(vertices.at(0).x / 100, vertices.at(0).y / 100)
vertices.slice(1).forEach((v) => shape.lineTo(v.x / 100, v.y / 100))
terrain = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { bevelEnabled: false, depth: t.collision ? 0.5 : 0.35 }), t.collision ? terrainMaterial : passableTerrainMaterial)
terrain.userData.type = 'terrain'
terrain.userData.id = t.id
scene.add(terrain)
terrains[t.id] = terrain
}
terrain.position.set(t.position.x / 100, t.position.y / 100, 0)
}
if (playerId != null) {
const player = state.entities.find((e) => e.id == playerId)
if (player != null) {
const playerAbilities = player.abilities
let abilitiesHTML = ''
let i = 0
for (const [abilityKey, _abilityId] of Object.entries(playerAbilities)) {
i++
const abilityKeyText = abilityKey.toUpperCase()
const abilityTemplate = `<div id="ability-${i}" class="ability">${abilityKeyText}<div id="ability-${i}-cooldown" class="cooldown"></div><div id="ability-${i}-cooldown-text" class="cooldown-text"></div></div>`
abilitiesHTML += abilityTemplate
}
if (document.getElementById(`abilities`).innerHTML != abilitiesHTML) {
document.getElementById(`abilities`).innerHTML = abilitiesHTML
}
let abilityIndex = 0
for (const [_abilityKey, abilityId] of Object.entries(playerAbilities)) {
abilityIndex++
const ability = state.abilities.find((it) => it.id == abilityId)
const lastCast = player.cooldowns[ability.id] ?? -Infinity
const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0
const remainingCooldown = (lastCast + cooldownDuration) - state.currentTick
let cssPercentage = '100%'
let text = ''
if (remainingCooldown > 0) {
const cooldownPercentage = 1 - (remainingCooldown / cooldownDuration)
cssPercentage = `${Math.round(100 * cooldownPercentage)}%`
if (remainingCooldown / state.tickRate <= 5) {
text = `${(Math.round(10 * remainingCooldown / state.tickRate) / 10).toFixed(1)}`
}
else {
text = `${Math.round(remainingCooldown / state.tickRate)}`
}
}
if (player.casting?.ability == ability.id) {
document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(50% 0%, 0% 100%, 100% 100%)` // triangle
}
else {
document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(0 ${cssPercentage}, 100% ${cssPercentage}, 100% 100%, 0 100%)`
}
document.getElementById(`ability-${abilityIndex}-cooldown-text`).innerHTML = text
}
let buffs = ``
player.buffs.forEach((b) => {
buffs += `<div class="buff"><div class="buff-body">${state.buffs.find((it) => it.id == b.id).name}</div></div>`
})
if (document.getElementById('buffs').innerHTML != buffs) {
document.getElementById('buffs').innerHTML = buffs
}
let castIndicatorDisplay = 'none'
if (player.casting != null) {
castIndicatorDisplay = 'block'
const ability = state.abilities.find((it) => it.id == player.casting.ability)
if (ability != null) {
const castDuration = (ability.castTime * state.tickRate) ?? 0
const remainingCastTime = (player.casting.timestamp + castDuration) - state.currentTick
let cssPercentage = '100%'
if (remainingCastTime > 0) {
const castPercentage = 1 - (remainingCastTime / castDuration)
cssPercentage = `${Math.round(100 * castPercentage)}%`
}
document.getElementById('cast_indicator_progress').style.clipPath = `polygon(0 0, ${cssPercentage} 0, ${cssPercentage} 100%, 0% 100%)`
document.getElementById('cast_indicator_name').innerHTML = ability.name ?? ''
}
}
document.getElementById('cast_indicator').style.display = castIndicatorDisplay
}
}
document.getElementById('state').innerHTML = JSON.stringify(stateUpdates, null, 2)
}
}
window.addEventListener('load', () => {
preloadGLTF('models/generic-bam-placeholder.gltf')
preloadGLTF('models/generic-player-placeholder.gltf')
preloadGLTF('models/generic-player-placeholder-red.gltf')
const params = Object.fromEntries(new URLSearchParams(window.location.search).entries())
playerId = params.id
if (playerId == null) {
playerId = prompt('Player ID:')
if (playerId == '') {
window.location.href = '/menu/'
return
}
}
connectWebSocket()
const canvas = renderer.domElement
canvas.classList.add('canvas')
window.addEventListener('mousedown', (event) => {
const intersect = raycastToGround()
if (intersect != null) {
const { x, y } = intersect
if (event.button == 0) {
websocket.send(JSON.stringify({ action: 'attack', id: playerId, x, y }))
}
if (event.button == 2) {
websocket.send(JSON.stringify({ action: 'move', id: playerId, x, y }))
}
}
})
window.addEventListener('keydown', (event) => {
const intersect = raycastToGround()
if (intersect != null) {
const { x, y } = intersect
if (event.code == 'KeyA') {
websocket.send(JSON.stringify({ action: 'attack', id: playerId, x, y }))
}
if (event.code == 'KeyX') {
websocket.send(JSON.stringify({ action: 'cast', slot: 'a', id: playerId, x, y }))
}
if (event.code == 'KeyS') {
websocket.send(JSON.stringify({ action: 'stop', id: playerId }))
}
if (event.code == 'KeyH') {
websocket.send(JSON.stringify({ action: 'halt', id: playerId }))
}
const alreadyBound = ['A', 'X', 'S', 'H']
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach((letter) => {
if (alreadyBound.includes(letter)) { return }
if (event.code == `Key${letter}`) {
websocket.send(JSON.stringify({ action: 'cast', slot: letter.toLowerCase(), id: playerId, x, y }))
}
})
}
})
window.addEventListener('wheel', (event) => {
if (event.deltaY < 0) {
camera.zoom += 0.2
if (camera.zoom > 3) {
camera.zoom = 3
}
camera.updateProjectionMatrix()
}
if (event.deltaY > 0) {
camera.zoom -= 0.2
if (camera.zoom < 1) {
camera.zoom = 1
}
camera.updateProjectionMatrix()
}
})
window.addEventListener('resize', (event) => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
window.addEventListener('contextmenu', (event) => event.preventDefault())
window.addEventListener('keydown', (event) => keysDown[event.code] = true)
window.addEventListener('keyup', (event) => keysDown[event.code] = false)
window.addEventListener('keydown', (event) => {
if (event.code == 'Space') {
cameraLocked = !cameraLocked
}
})
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX
mouse.y = event.clientY
})
document.body.appendChild(canvas)
const minimap = minimapRenderer.domElement
minimap.classList.add('minimap')
document.body.appendChild(minimap)
document.body.appendChild(stats.dom)
})