// renderer.js — createPixiRenderer:实现 @drama/core Renderer 全部方法,持有 stage / actor 注册表。 // // 这是 core 接口的实现外壳:把每个接口方法转成对 stage / actor 的操作。 // core 那侧全是数字与字符串(SlotTransform / CameraRenderState / 字符串 / 数字), // 本包这侧才出现 PIXI.Sprite——本文件是「翻译层」的总装配。 // // addActor / removeActor / loadScene 作为 Renderer 的**扩展方法**挂在返回对象上 // (scheduler 的 cast 出场 / transition 换班底调用),委托 stage(render-pixi §4)。 import { createAssetLoader } from './assets.js' import { PixiStage } from './stage.js' import { PixiActor } from './actor.js' /** * 创建满足 @drama/core Renderer 接口的 PixiJS 渲染后端。 * @param {import('pixi.js').Application} app 已 init 的 Pixi Application * @param {import('./index.js').RendererOpts} opts * @returns {import('./index.js').PixiRenderer} */ export function createPixiRenderer(app, opts) { const o = opts || /** @type {*} */ ({}) const { PIXI, rig } = o // 工厂入口自检(架构 §10.3):少注入即在装配时崩,不拖到运行某行。 if (!app) throw new Error('createPixiRenderer: app is required') if (!PIXI) throw new Error('createPixiRenderer: opts.PIXI is required') if (!rig) throw new Error('createPixiRenderer: opts.rig is required') const assets = createAssetLoader({ PIXI, loadTexture: o.loadTexture }) const stage = new PixiStage({ app, PIXI, assets, resolveEffect: o.resolveEffect }) // 道具库解析:name -> propDef({url,scale,anchor,holds}),供手持道具挂载(DramaPlayer 注入 bundle.props)。 const resolveProp = typeof o.resolveProp === 'function' ? o.resolveProp : () => null /** @type {Map} */ const actors = new Map() /** @param {string} id */ const getActor = (id) => actors.get(id) || null // 场景表(id -> sceneDef)+ 转场镜头回调,由装配层(usePlayback)注入。 // transition.to 是场景 id(移植自 OLD play-demo.html:100 `getScene(t.to)`),按表解析成 def → loadScene。 /** @type {Record} */ let sceneTable = {} /** @type {((sceneDef:any)=>void)|null} 换场后施新场景标准镜头(OLD play-demo.html:104 `cut(def.camera)`) */ let onSceneCut = null // ───────────────────── Renderer 扩展方法(出场 / 换场) ───────────────────── /** * 注册一个角色到舞台(建容器、按 rig.slots 贴皮)。 * @param {string} actorId * @param {import('./index.js').CharacterSkin} skin * @param {object} placement {x,y,facing,scale} */ function addActor(actorId, skin, placement) { if (actors.has(actorId)) return const p = /** @type {{x:number,y:number,facing:'left'|'right',scale:number}} */ ( placement || { x: 0, y: 0, facing: 'right', scale: 1 } ) const actor = new PixiActor({ PIXI, rig, skin, assets, placement: p, depthScale: stage.depthScale, }) stage.addActorRoot(actor.root) actors.set(actorId, actor) } /** @param {string} actorId */ function removeActor(actorId) { const a = getActor(actorId) if (!a) return a.destroy() actors.delete(actorId) } /** * 一条龙加载场景:背景 + 世界尺寸 + 纵深 + 遮挡片。 * @param {{background?:string,world?:{w:number,h:number},depth?:{nearY:number,farY:number,nearScale:number,farScale:number}|null,occluders?:Array<{polygon:number[][],baseY:number}>}} [def] */ async function loadScene(def = {}) { if (def.world) stage.setWorldSize(def.world.w, def.world.h) stage.setDepth(def.depth || null) // 同步已有角色的透视。 for (const a of actors.values()) { a.depthScale = stage.depthScale a.setWorldPos(a.worldX, a.worldY) } let tex = null if (def.background) tex = await assets.load(def.background) // 先就绪(遮挡片要裁它) if (def.background) stage.setBackgroundImage(def.background, tex) stage.clearOccluders() for (const o of def.occluders || []) stage.addOccluder(o) } // ───────────────────────── Renderer 接口(core §3.3) ───────────────────────── /** @type {import('@drama/core').Renderer} */ const renderer = { applyActorPose(actorId, slotTransforms) { const a = getActor(actorId) if (a) a.applyActorPose(slotTransforms || {}) }, applyFace(actorId, emotion, talking) { const a = getActor(actorId) if (a) a.applyFace(emotion, talking, o.faceFrameDt || 16) }, applyFacing(actorId, facing, headFacing) { const a = getActor(actorId) if (a) a.applyFacing(facing, headFacing) }, applyCamera(state) { stage.applyCamera(state) }, setActorWorldPos(actorId, worldX, worldY) { const a = getActor(actorId) if (a) a.setWorldPos(worldX, worldY) }, moveActor(actorId, worldX, worldY, durMs, easing) { const a = getActor(actorId) if (a) a.moveTo(worldX, worldY, durMs, easing) }, playEffect(name, fxOpts) { stage.effects.play(name, fxOpts || {}) }, stopEffect(name) { stage.effects.stop(name) }, /** * 角色手持道具:把 name 解析成 propDef(resolveProp)后挂到该角色(actor.holdProp)。 * @param {string} actorId @param {string} name 道具名 @param {string=} hold 握法(carry/raise) */ holdProp(actorId, name, hold) { const a = getActor(actorId) const def = resolveProp(name) if (a && def) a.holdProp(def, hold) }, /** @param {string} actorId 松开该角色手持道具。 */ releaseProp(actorId) { const a = getActor(actorId) if (a) a.releaseProp() }, resetActor(actorId) { const a = getActor(actorId) if (a) a.reset() }, async runTransition(transition) { // 换场数据落地(移植自 OLD play-demo.html:100-104 transitionTo 的换场段): // 黑场动画 / 停旧场景音频 / 换班底 由 player-vue(DramaPlayer overlay)与 ctx.applyCast 负责; // 本包只负责「把 transition.to 解析成场景 def → loadScene(换背景/地面/遮挡/点位)→ 施新场景标准镜头」。 /** @type {{to?:string,scene?:*,clearActors?:boolean,cast?:Array<{actorId:string,skin:*,placement:*}>}} */ const t = transition || {} if (t.clearActors) { for (const id of [...actors.keys()]) removeActor(id) } // to=场景 id(OLD 行为):查场景表拿 def;也兼容直接传 scene 对象。 const def = t.scene || (t.to != null ? sceneTable[t.to] : null) if (def) { await loadScene(def) if (onSceneCut) onSceneCut(def) // 施新场景标准镜头 cut(def.camera)(OLD play-demo.html:104) } if (Array.isArray(t.cast)) { for (const c of t.cast) addActor(c.actorId, c.skin, c.placement) } }, } // 扩展方法挂上返回对象(scheduler 的 cast / transition 调用)。 return Object.assign(renderer, { addActor, removeActor, loadScene, // 相机跟随焦点 / 当前世界坐标查询(usePlayback 的 ctx.focusLookup / getActorWorldPos 用, // 从 renderer 取 actor 当前真实坐标,moveTo 补间后自动是移动后的最新值——见任务清单 6)。 getActorFocus: (/** @type {string} */ id) => { const a = getActor(id) return a ? a.getFocusPoint() : null }, getActorWorldPos: (/** @type {string} */ id) => { const a = getActor(id) return a ? { x: a.worldX, y: a.worldY } : null }, // 转场场景表 + 标准镜头回调注入(transition.to 解析用)。 setSceneTable: (/** @type {Record} */ t) => { sceneTable = t || {} }, setSceneCut: (/** @type {((def:any)=>void)|null} */ fn) => { onSceneCut = fn || null }, // 内部访问(player-vue 装配 ticker 时驱动特效轮播 + 走位补间;非 core 接口)。 _stage: stage, _actors: actors, updateEffects: (/** @type {number} */ dtMs) => stage.effects.update(dtMs), // 每帧推进所有角色走位补间(移植自 OLD:每帧 actor.update 里 lerp 位置)。 updateMoves: (/** @type {number} */ dtMs) => { for (const a of actors.values()) a.updateMove(dtMs) }, // 每帧推进所有角色口型(说话态 talkFps 轮播;移植自 OLD actor.update 的表情段,任务清单 11)。 updateFaces: (/** @type {number} */ dtMs) => { for (const a of actors.values()) a.tickFace(dtMs) }, // 停所有屏幕特效(转场清场用)。 stopAllEffects: () => stage.effects.stopAll(), }) }