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'
|
} from '../game/d2map.ts'
|
||||||
import type { CollisionGrid } from '../game/d2map.ts'
|
import type { CollisionGrid } from '../game/d2map.ts'
|
||||||
import { depthInsertIndex } from '../game/map.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 { buildAtlas } from '../render/atlas.ts'
|
||||||
import type { AtlasFrame, SpriteAtlas } from '../render/atlas.ts'
|
import type { AtlasFrame, SpriteAtlas } from '../render/atlas.ts'
|
||||||
import { SpriteRenderer } from '../render/renderer.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 actSelect = document.querySelector<HTMLSelectElement>('#act')!
|
||||||
const variantSelect = document.querySelector<HTMLSelectElement>('#variant')!
|
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()
|
const input = new KeyboardInput()
|
||||||
input.attach()
|
input.attach()
|
||||||
let skippedDraws = 0
|
let skippedDraws = 0
|
||||||
|
|
@ -711,34 +712,76 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
|
||||||
const camera = new ViewportCamera(canvas, runtime.widthPx, runtime.heightPx)
|
const camera = new ViewportCamera(canvas, runtime.widthPx, runtime.heightPx)
|
||||||
camera.attach()
|
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({
|
const loop = new GameLoop({
|
||||||
tickRate: 25,
|
tickRate: 25,
|
||||||
onTick: () => {
|
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()
|
const movement = input.movement()
|
||||||
player.moving = movement.x !== 0 || movement.y !== 0
|
let playerMoving = movement.x !== 0 || movement.y !== 0
|
||||||
if (player.moving) {
|
if (playerMoving) {
|
||||||
// Input is screen-space: the diamond lattice is rotated 45° from the
|
playerWalkFrame = (playerWalkFrame + 1) % 24
|
||||||
// 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
|
|
||||||
} else {
|
} else {
|
||||||
player.walkFrame = 0
|
playerWalkFrame = 0
|
||||||
}
|
}
|
||||||
state.tick = loop.tick
|
|
||||||
|
state.tick = engine.world.tick
|
||||||
state.x = player.x
|
state.x = player.x
|
||||||
state.y = player.y
|
state.y = player.y
|
||||||
state.facing = player.facing
|
state.facing = player.facing
|
||||||
|
|
||||||
|
syncEngineState(engine, state)
|
||||||
},
|
},
|
||||||
onRender: () => {
|
onRender: () => {
|
||||||
if (state.pagesAtFirstFrame < 0) {
|
if (state.pagesAtFirstFrame < 0) {
|
||||||
state.pagesAtFirstFrame = runtime.pages.filter(page => page !== null).length
|
state.pagesAtFirstFrame = runtime.pages.filter(page => page !== null).length
|
||||||
state.firstFrameMs = Math.round(performance.now() - started)
|
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
|
skippedDraws = 0
|
||||||
const drawTiles = (list: readonly Drawable[], from: number, to: number): void => {
|
const drawTiles = (list: readonly Drawable[], from: number, to: number): void => {
|
||||||
for (let index = from; index < to; index += 1) {
|
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)
|
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
|
const playerDepth = cell.x + cell.y
|
||||||
|
|
||||||
let wallIdx = 0
|
let wallIdx = 0
|
||||||
|
|
@ -790,20 +833,20 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
|
||||||
drawUpToDepth(playerDepth)
|
drawUpToDepth(playerDepth)
|
||||||
if (character === null) {
|
if (character === null) {
|
||||||
// Placeholder until the character archive has been read.
|
// 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(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(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 - 6, MARKER_WIDTH, 6, [0.35, 0.25, 0.15, 1])
|
||||||
} else {
|
} else {
|
||||||
const direction = facingToDirection(player.facing, character.directions)
|
const direction = facingToDirection(engine.world.player.facing, character.directions)
|
||||||
const groupIndex = (player.moving ? character.walk : character.stand) + direction
|
const groupIndex = (playerMoving ? character.walk : character.stand) + direction
|
||||||
const group = character.groups[groupIndex] ?? character.groups[character.walk + direction]
|
const group = character.groups[groupIndex] ?? character.groups[character.walk + direction]
|
||||||
const frameIndex = player.moving
|
const frameIndex = playerMoving
|
||||||
? Math.floor(player.walkFrame / 3) % Math.max(1, group?.length ?? 1)
|
? Math.floor(playerWalkFrame / 3) % Math.max(1, group?.length ?? 1)
|
||||||
: 0
|
: 0
|
||||||
const frame = group?.[frameIndex]
|
const frame = group?.[frameIndex]
|
||||||
if (frame !== undefined) {
|
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.
|
// 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.characterFrames += 1
|
||||||
state.characterGroup = groupIndex
|
state.characterGroup = groupIndex
|
||||||
}
|
}
|
||||||
|
|
@ -823,7 +866,7 @@ function runScene(runtime: MapRuntime, renderer: SpriteRenderer, started: number
|
||||||
hud.textContent = `${runtime.level} · ${runtime.quadrant} · ${runtime.source === 'pack' ? '资源包' : '读归档'}`
|
hud.textContent = `${runtime.level} · ${runtime.quadrant} · ${runtime.source === 'pack' ? '资源包' : '读归档'}`
|
||||||
+ ` · ${String(runtime.cellsX)}×${String(runtime.cellsY)} 格 · ${String(runtime.floors.length)} 地面 / ${String(runtime.walls.length)} 墙`
|
+ ` · ${String(runtime.cellsX)}×${String(runtime.cellsY)} 格 · ${String(runtime.floors.length)} 地面 / ${String(runtime.walls.length)} 墙`
|
||||||
+ ` · 页 ${String(state.pagesLoaded)}/${String(state.pagesTotal)}`
|
+ ` · 页 ${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)}` : '')
|
+ (skippedDraws > 0 ? ` · 待页 ${String(skippedDraws)}` : '')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue