feat: 在 Act 场景中挂载 GameEngine 提供真实的模拟物理状态

将刚才提取出的引擎初始化进 `act-scene.ts`,配置一个空的怪物列表。
使得 Act 地图不再仅仅是个“游览器”,而是拥有血条、法力、体力、碰撞并进行 ticks 的真实服务器沙盒对象。
This commit is contained in:
troytt 2026-09-14 12:19:55 +00:00
parent 38d06cdb28
commit 1e7645d388
1 changed files with 67 additions and 24 deletions

View File

@ -31,6 +31,8 @@ import {
} from '../game/d2map.ts'
import type { CollisionGrid } from '../game/d2map.ts'
import { depthInsertIndex } from '../game/map.ts'
import { GameEngine, syncEngineState } from "../game/engine.ts"
import { DEMO_MONSTERS, DEMO_EXPERIENCE, DEMO_BASES, DEMO_AFFIXES, DEMO_SKILLS, DEMO_NPCS, DEMO_QUESTS } from "../game/demo-data.ts"
import { buildAtlas } from '../render/atlas.ts'
import type { AtlasFrame, SpriteAtlas } from '../render/atlas.ts'
import { SpriteRenderer } from '../render/renderer.ts'
@ -702,7 +704,6 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
const actSelect = document.querySelector<HTMLSelectElement>('#act')!
const variantSelect = document.querySelector<HTMLSelectElement>('#variant')!
const player = { x: runtime.spawn.x, y: runtime.spawn.y, facing: 0, walkFrame: 0, moving: false }
const input = new KeyboardInput()
input.attach()
let skippedDraws = 0
@ -711,34 +712,76 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
const camera = new ViewportCamera(canvas, runtime.widthPx, runtime.heightPx)
camera.attach()
let playerMoving = false
let playerWalkFrame = 0
const engine = new GameEngine(
{
widthPx: runtime.widthPx,
heightPx: runtime.heightPx,
overlap: (x, y) => (collides(runtime.grid, x, y) ? 1 : 0),
},
{
spawn: runtime.spawn,
stats: [],
xpTable: DEMO_EXPERIENCE,
itemBases: DEMO_BASES,
prefixAffixes: DEMO_AFFIXES,
suffixAffixes: DEMO_AFFIXES,
skills: DEMO_SKILLS,
npcDefs: DEMO_NPCS,
questDefs: DEMO_QUESTS,
combatOptions: {
playerSpeed: WALK_SPEED,
playerReach: 52,
playerCooldownTicks: 12,
playerDamage: 7,
playerManaPerAttack: 2,
respawnTicks: 75,
},
lootSeed: 0x5eed,
talkRadius: 80,
pickupRadius: 64,
inventoryCols: 10,
inventoryRows: 4,
}
)
const loop = new GameLoop({
tickRate: 25,
onTick: () => {
engine.tick({
movement: input.movement(),
attacking: input.attacking,
pickingUp: input.pickingUp,
talking: input.talking,
digits: input.digits,
saving: input.saving,
loading: input.loading,
})
const player = engine.world.player
const movement = input.movement()
player.moving = movement.x !== 0 || movement.y !== 0
if (player.moving) {
// Input is screen-space: the diamond lattice is rotated 45° from the
// screen axes, so "north" walks along a cell diagonal.
const length = Math.hypot(movement.x, movement.y)
const step = (WALK_SPEED / 25) / length
if (!collides(runtime.grid, player.x + movement.x * step, player.y)) player.x += movement.x * step
if (!collides(runtime.grid, player.x, player.y + movement.y * step)) player.y += movement.y * step
player.facing = facingOf(movement.x, movement.y)
player.walkFrame = (player.walkFrame + 1) % 24
let playerMoving = movement.x !== 0 || movement.y !== 0
if (playerMoving) {
playerWalkFrame = (playerWalkFrame + 1) % 24
} else {
player.walkFrame = 0
playerWalkFrame = 0
}
state.tick = loop.tick
state.tick = engine.world.tick
state.x = player.x
state.y = player.y
state.facing = player.facing
syncEngineState(engine, state)
},
onRender: () => {
if (state.pagesAtFirstFrame < 0) {
state.pagesAtFirstFrame = runtime.pages.filter(page => page !== null).length
state.firstFrameMs = Math.round(performance.now() - started)
}
renderer.begin({ x: player.x, y: player.y - 16, zoom: camera.zoom }, [0.03, 0.03, 0.04])
renderer.begin({ x: engine.world.player.x, y: engine.world.player.y - 16, zoom: camera.zoom }, [0.03, 0.03, 0.04])
skippedDraws = 0
const drawTiles = (list: readonly Drawable[], from: number, to: number): void => {
for (let index = from; index < to; index += 1) {
@ -749,7 +792,7 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
}
}
drawTiles(runtime.floors, 0, runtime.floors.length)
const cell = cellOf(runtime.grid, player.x, player.y)
const cell = cellOf(runtime.grid, engine.world.player.x, engine.world.player.y)
const playerDepth = cell.x + cell.y
let wallIdx = 0
@ -790,20 +833,20 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
drawUpToDepth(playerDepth)
if (character === null) {
// Placeholder until the character archive has been read.
renderer.drawSolid(player.x - MARKER_WIDTH / 2, player.y - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, [0.85, 0.75, 0.45, 1])
renderer.drawSolid(player.x - MARKER_WIDTH / 2, player.y - 6, MARKER_WIDTH, 6, [0.35, 0.25, 0.15, 1])
renderer.drawSolid(engine.world.player.x - MARKER_WIDTH / 2, engine.world.player.y - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, [0.85, 0.75, 0.45, 1])
renderer.drawSolid(engine.world.player.x - MARKER_WIDTH / 2, engine.world.player.y - 6, MARKER_WIDTH, 6, [0.35, 0.25, 0.15, 1])
} else {
const direction = facingToDirection(player.facing, character.directions)
const groupIndex = (player.moving ? character.walk : character.stand) + direction
const direction = facingToDirection(engine.world.player.facing, character.directions)
const groupIndex = (playerMoving ? character.walk : character.stand) + direction
const group = character.groups[groupIndex] ?? character.groups[character.walk + direction]
const frameIndex = player.moving
? Math.floor(player.walkFrame / 3) % Math.max(1, group?.length ?? 1)
const frameIndex = playerMoving
? Math.floor(playerWalkFrame / 3) % Math.max(1, group?.length ?? 1)
: 0
const frame = group?.[frameIndex]
if (frame !== undefined) {
// Feet at the player's position: the composited frame is anchored by its
// Feet at the engine.world.player's position: the composited frame is anchored by its
// bottom edge, which is where the COF puts the sprite's origin.
renderer.draw(frame, player.x - frame.width / 2, player.y - frame.height + FEET_HEIGHT / 2, { atlas: character.handle })
renderer.draw(frame, engine.world.player.x - frame.width / 2, engine.world.player.y - frame.height + FEET_HEIGHT / 2, { atlas: character.handle })
state.characterFrames += 1
state.characterGroup = groupIndex
}
@ -823,7 +866,7 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
hud.textContent = `${runtime.level} · ${runtime.quadrant} · ${runtime.source === 'pack' ? '资源包' : '读归档'}`
+ ` · ${String(runtime.cellsX)}×${String(runtime.cellsY)} 格 · ${String(runtime.floors.length)} 地面 / ${String(runtime.walls.length)} 墙`
+ ` · 页 ${String(state.pagesLoaded)}/${String(state.pagesTotal)}`
+ ` · 对象 ${String(runtime.objects)} · ${loop.tickRate.toFixed(1)} tps · pos ${player.x.toFixed(0)},${player.y.toFixed(0)}`
+ ` · 对象 ${String(runtime.objects)} · ${loop.tickRate.toFixed(1)} tps · pos ${engine.world.player.x.toFixed(0)},${engine.world.player.y.toFixed(0)}`
+ (skippedDraws > 0 ? ` · 待页 ${String(skippedDraws)}` : '')
},
})