feat: 在 Act 场景中挂载 GameEngine 提供真实的模拟物理状态
将刚才提取出的引擎初始化进 `act-scene.ts`,配置一个空的怪物列表。 使得 Act 地图不再仅仅是个“游览器”,而是拥有血条、法力、体力、碰撞并进行 ticks 的真实服务器沙盒对象。
This commit is contained in:
parent
38d06cdb28
commit
1e7645d388
|
|
@ -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)}` : '')
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue