This commit is contained in:
wfz
2026-04-15 20:58:16 +08:00
commit 8ab9a9fa70
33 changed files with 6140 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
export const GRID_SIZE = 32
export const CELL_SIZE = 1
export const PLAYER_SIZE = 0.8
export const ZOMBIE_SIZE = 0.8
export const WALL_SIZE = 1
export const SPAWN_SIZE = 1
export const TICK_RATE = 30
export const TICK_INTERVAL = 1000 / TICK_RATE
export const WEAPONS = {
PISTOL: 'pistol',
MACHINE_GUN: 'machine_gun',
SHOTGUN: 'shotgun',
GRENADE: 'grenade'
}
export const WEAPON_CONFIG = {
[WEAPONS.PISTOL]: {
name: 'Pistol',
damage: 25,
fireRate: 400,
ammo: Infinity,
maxAmmo: Infinity,
speed: 20,
spread: 0,
pellets: 1,
range: 30,
auto: false,
chargeable: false
},
[WEAPONS.MACHINE_GUN]: {
name: 'Machine Gun',
damage: 15,
fireRate: 100,
ammo: 100,
maxAmmo: 100,
speed: 25,
spread: 0.05,
pellets: 1,
range: 25,
auto: true,
chargeable: false
},
[WEAPONS.SHOTGUN]: {
name: 'Shotgun',
damage: 20,
fireRate: 800,
ammo: 20,
maxAmmo: 20,
speed: 18,
spread: 0.15,
pellets: 6,
range: 12,
auto: false,
chargeable: false
},
[WEAPONS.GRENADE]: {
name: 'Grenade',
damage: 100,
fireRate: 1500,
ammo: 10,
maxAmmo: 10,
speed: 12,
spread: 0,
pellets: 1,
range: 15,
auto: false,
chargeable: true,
maxCharge: 2000,
explosionRadius: 3
}
}
export const ZOMBIE_CONFIG = {
BASE_HEALTH: 100,
BASE_SPEED: 2,
DAMAGE: 10,
ATTACK_RATE: 1000,
SPAWN_INTERVAL_BASE: 3000,
SPAWN_INTERVAL_MIN: 800,
DIFFICULTY_INCREASE_INTERVAL: 30000,
HEALTH_INCREASE_PER_WAVE: 20,
SPEED_INCREASE_PER_WAVE: 0.1,
LOOT_DROP_CHANCE: 0.3
}
export const PLAYER_CONFIG = {
MAX_HEALTH: 100,
SPEED: 5,
INVULNERABLE_TIME: 500
}
export const MSG_TYPE = {
CREATE_ROOM: 'create_room',
JOIN_ROOM: 'join_room',
LEAVE_ROOM: 'leave_room',
ROOM_LIST: 'room_list',
ROOM_STATE: 'room_state',
READY: 'ready',
START_GAME: 'start_game',
GAME_STARTED: 'game_started',
PLAYER_INPUT: 'player_input',
GAME_STATE: 'game_state',
PLAYER_JOIN: 'player_join',
PLAYER_LEAVE: 'player_leave',
ERROR: 'error',
PING: 'ping',
PONG: 'pong'
}

250
frontend/src/utils/grid.js Normal file
View File

@@ -0,0 +1,250 @@
import { GRID_SIZE, CELL_SIZE, WALL_SIZE } from './constants.js'
export class Grid {
constructor(mapData) {
this.width = GRID_SIZE
this.height = GRID_SIZE
this.cells = []
this.parseMap(mapData)
}
parseMap(mapData) {
this.cells = []
for (let y = 0; y < this.height; y++) {
this.cells[y] = []
for (let x = 0; x < this.width; x++) {
this.cells[y][x] = mapData[y] ? mapData[y][x] || 0 : 0
}
}
}
isWall(gridX, gridY) {
if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) return true
return this.cells[gridY][gridX] === 1
}
isSpawnPoint(gridX, gridY) {
if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) return false
return this.cells[gridY][gridX] === 2
}
worldToGrid(wx, wy) {
return {
x: Math.floor(wx / CELL_SIZE),
y: Math.floor(wy / CELL_SIZE)
}
}
gridToWorld(gx, gy) {
return {
x: gx * CELL_SIZE + CELL_SIZE / 2,
y: gy * CELL_SIZE + CELL_SIZE / 2
}
}
isWalkable(wx, wy, size) {
const half = size / 2
const corners = [
{ x: wx - half, y: wy - half },
{ x: wx + half, y: wy - half },
{ x: wx - half, y: wy + half },
{ x: wx + half, y: wy + half }
]
for (const corner of corners) {
const g = this.worldToGrid(corner.x, corner.y)
if (this.isWall(g.x, g.y)) return false
}
return true
}
getSpawnPoints() {
const points = []
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
if (this.cells[y][x] === 2) {
points.push({ x, y })
}
}
}
return points
}
findPath(startX, startY, endX, endY) {
const sg = this.worldToGrid(startX, startY)
const eg = this.worldToGrid(endX, endY)
if (this.isWall(eg.x, eg.y)) return null
const openSet = []
const closedSet = new Set()
const cameFrom = new Map()
const heuristic = (ax, ay, bx, by) => Math.abs(ax - bx) + Math.abs(ay - by)
const startKey = `${sg.x},${sg.y}`
openSet.push({
x: sg.x,
y: sg.y,
g: 0,
h: heuristic(sg.x, sg.y, eg.x, eg.y),
f: heuristic(sg.x, sg.y, eg.x, eg.y)
})
const directions = [
{ dx: 0, dy: -1 },
{ dx: 0, dy: 1 },
{ dx: -1, dy: 0 },
{ dx: 1, dy: 0 },
{ dx: -1, dy: -1 },
{ dx: 1, dy: -1 },
{ dx: -1, dy: 1 },
{ dx: 1, dy: 1 }
]
let iterations = 0
const maxIterations = 2000
while (openSet.length > 0 && iterations < maxIterations) {
iterations++
openSet.sort((a, b) => a.f - b.f)
const current = openSet.shift()
const currentKey = `${current.x},${current.y}`
if (current.x === eg.x && current.y === eg.y) {
const path = []
let key = currentKey
while (cameFrom.has(key)) {
const [cx, cy] = key.split(',').map(Number)
const wp = this.gridToWorld(cx, cy)
path.unshift({ x: wp.x, y: wp.y })
key = cameFrom.get(key)
}
return path
}
closedSet.add(currentKey)
for (const dir of directions) {
const nx = current.x + dir.dx
const ny = current.y + dir.dy
const nKey = `${nx},${ny}`
if (closedSet.has(nKey)) continue
if (this.isWall(nx, ny)) continue
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) continue
if (dir.dx !== 0 && dir.dy !== 0) {
if (this.isWall(current.x + dir.dx, current.y) || this.isWall(current.x, current.y + dir.dy)) {
continue
}
}
const isDiagonal = dir.dx !== 0 && dir.dy !== 0
const moveCost = isDiagonal ? 1.414 : 1
const g = current.g + moveCost
const existing = openSet.find(n => n.x === nx && n.y === ny)
if (existing) {
if (g < existing.g) {
existing.g = g
existing.f = g + existing.h
cameFrom.set(nKey, currentKey)
}
} else {
const h = heuristic(nx, ny, eg.x, eg.y)
openSet.push({ x: nx, y: ny, g, h, f: g + h })
cameFrom.set(nKey, currentKey)
}
}
}
return null
}
}
export function generateDefaultMap() {
const map = []
for (let y = 0; y < GRID_SIZE; y++) {
map[y] = []
for (let x = 0; x < GRID_SIZE; x++) {
if (x === 0 || x === GRID_SIZE - 1 || y === 0 || y === GRID_SIZE - 1) {
map[y][x] = 1
} else {
map[y][x] = 0
}
}
}
const wallSegments = [
{ x1: 5, y1: 5, x2: 5, y2: 10 },
{ x1: 10, y1: 3, x2: 15, y2: 3 },
{ x1: 20, y1: 5, x2: 20, y2: 12 },
{ x1: 8, y1: 15, x2: 14, y2: 15 },
{ x1: 25, y1: 10, x2: 25, y2: 18 },
{ x1: 3, y1: 20, x2: 8, y2: 20 },
{ x1: 15, y1: 20, x2: 15, y2: 26 },
{ x1: 22, y1: 22, x2: 28, y2: 22 },
{ x1: 5, y1: 25, x2: 10, y2: 25 },
{ x1: 12, y1: 8, x2: 12, y2: 12 },
{ x1: 18, y1: 16, x2: 22, y2: 16 },
{ x1: 27, y1: 5, x2: 27, y2: 9 },
{ x1: 8, y1: 27, x2: 13, y2: 27 },
{ x1: 18, y1: 26, x2: 18, y2: 30 }
]
for (const seg of wallSegments) {
if (seg.x1 === seg.x2) {
for (let y = seg.y1; y <= seg.y2; y++) {
if (seg.x1 > 0 && seg.x1 < GRID_SIZE - 1 && y > 0 && y < GRID_SIZE - 1) {
map[y][seg.x1] = 1
}
}
} else {
for (let x = seg.x1; x <= seg.x2; x++) {
if (x > 0 && x < GRID_SIZE - 1 && seg.y1 > 0 && seg.y1 < GRID_SIZE - 1) {
map[seg.y1][x] = 1
}
}
}
}
const spawnPoints = [
{ x: 2, y: 2 },
{ x: 29, y: 2 },
{ x: 2, y: 29 },
{ x: 29, y: 29 }
]
for (const sp of spawnPoints) {
map[sp.y][sp.x] = 2
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const ny = sp.y + dy
const nx = sp.x + dx
if (ny > 0 && ny < GRID_SIZE - 1 && nx > 0 && nx < GRID_SIZE - 1) {
if (map[ny][nx] === 1) map[ny][nx] = 0
}
}
}
}
const zombieSpawns = [
{ x: 16, y: 2 },
{ x: 2, y: 16 },
{ x: 29, y: 16 },
{ x: 16, y: 29 },
{ x: 16, y: 16 }
]
for (const sp of zombieSpawns) {
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const ny = sp.y + dy
const nx = sp.x + dx
if (ny > 0 && ny < GRID_SIZE - 1 && nx > 0 && nx < GRID_SIZE - 1) {
if (map[ny][nx] === 1) map[ny][nx] = 0
}
}
}
}
return map
}

106
frontend/src/utils/input.js Normal file
View File

@@ -0,0 +1,106 @@
export class InputManager {
constructor() {
this.keys = {}
this.mouse = { x: 0, y: 0, left: false, right: false }
this.keyBindings = {
moveUp: 'KeyW',
moveDown: 'KeyS',
moveLeft: 'KeyA',
moveRight: 'KeyD',
weapon1: 'Digit1',
weapon2: 'Digit2',
weapon3: 'Digit3',
weapon4: 'Digit4'
}
this.pendingActions = []
this.sequenceNumber = 0
this.onKeyDown = null
this.onKeyUp = null
this._onKeyDown = (e) => {
this.keys[e.code] = true
if (this.onKeyDown) this.onKeyDown(e)
}
this._onKeyUp = (e) => {
this.keys[e.code] = false
if (this.onKeyUp) this.onKeyUp(e)
}
this._onMouseMove = (e) => {
this.mouse.x = e.clientX
this.mouse.y = e.clientY
}
this._onMouseDown = (e) => {
if (e.button === 0) this.mouse.left = true
if (e.button === 2) this.mouse.right = true
}
this._onMouseUp = (e) => {
if (e.button === 0) this.mouse.left = false
if (e.button === 2) this.mouse.right = false
}
this._onContextMenu = (e) => e.preventDefault()
}
attach() {
window.addEventListener('keydown', this._onKeyDown)
window.addEventListener('keyup', this._onKeyUp)
window.addEventListener('mousemove', this._onMouseMove)
window.addEventListener('mousedown', this._onMouseDown)
window.addEventListener('mouseup', this._onMouseUp)
window.addEventListener('contextmenu', this._onContextMenu)
}
detach() {
window.removeEventListener('keydown', this._onKeyDown)
window.removeEventListener('keyup', this._onKeyUp)
window.removeEventListener('mousemove', this._onMouseMove)
window.removeEventListener('mousedown', this._onMouseDown)
window.removeEventListener('mouseup', this._onMouseUp)
window.removeEventListener('contextmenu', this._onContextMenu)
}
getMovement() {
let dx = 0, dy = 0
if (this.keys[this.keyBindings.moveUp] || this.keys['ArrowUp']) dy -= 1
if (this.keys[this.keyBindings.moveDown] || this.keys['ArrowDown']) dy += 1
if (this.keys[this.keyBindings.moveLeft] || this.keys['ArrowLeft']) dx -= 1
if (this.keys[this.keyBindings.moveRight] || this.keys['ArrowRight']) dx += 1
if (dx !== 0 && dy !== 0) {
dx *= 0.7071
dy *= 0.7071
}
return { dx, dy }
}
getSelectedWeapon() {
if (this.keys[this.keyBindings.weapon1]) return 0
if (this.keys[this.keyBindings.weapon2]) return 1
if (this.keys[this.keyBindings.weapon3]) return 2
if (this.keys[this.keyBindings.weapon4]) return 3
return -1
}
buildInputState(mouseGroundPos) {
const movement = this.getMovement()
const weaponIdx = this.getSelectedWeapon()
this.sequenceNumber++
return {
seq: this.sequenceNumber,
dx: movement.dx,
dy: movement.dy,
aimX: mouseGroundPos.x,
aimY: mouseGroundPos.y,
firing: this.mouse.left,
weaponIndex: weaponIdx
}
}
updateKeyBinding(action, keyCode) {
if (this.keyBindings.hasOwnProperty(action)) {
this.keyBindings[action] = keyCode
}
}
getKeyBindings() {
return { ...this.keyBindings }
}
}