line-handlers.js
4.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// line-handlers.js —— 各指令到三接口调用的纯映射。
//
// 这里只把「一条 ScriptLine 的某个字段」翻译成「对 renderer / camera / animState 的一次调用」,
// **不决定时序**(并行还是阻塞)——时序归 scheduler。本文件按 §3.4 归类:
// 非阻塞、行首即发:cam / fx / sfx / pose / trigger / emotion / facing / headFacing / prop / propPlace。
// 阻塞项(moveTo / say / transition)由 scheduler 直接编排,不放这里。
//
// ctx 形状(由装配层 / 测试提供,scheduler 透传):
// {
// renderer, audio, tts,
// animStateOf(alias) => AnimationState|null,
// resolvePoint(nameOrXY) => {x,y}|null,
// getActorWorldPos(alias) => {x,y}|null,
// focusLookup(alias) => {x,y}|null,
// castVoice(alias) => { voice?, speechRate?, lang? },
// }
/**
* 行首即发的非阻塞副作用:把不需要 await 的指令一次性发出。
* @param {import('./types.js').ScriptLine} line
* @param {{renderer:import('./types.js').Renderer, audio:import('./types.js').AudioPort, camera:(import('./types.js').Camera|null)}} ports
* @param {import('./types.js').SchedulerContext} ctx
* @param {import('./types.js').LineSfxState} sfxState 本行级 sfx/fx 跟踪(供行末清理 until:lineEnd)
*/
export function emitNonBlocking(line, ports, ctx, sfxState) {
const { renderer, audio, camera } = ports
const alias = line.who || null
const anim = alias && ctx.animStateOf ? ctx.animStateOf(alias) : null
// 独立道具放置 / 移除(不依赖人)
if (line.propPlace) {
const pp = /** @type {{id:string,name?:string,x?:number,y?:number,scale?:number,remove?:boolean}} */ (line.propPlace)
if (pp.remove) renderer.stopEffect(`prop:${pp.id}`)
else renderer.playEffect(`prop:${pp.id}`, pp)
}
// 屏幕特效
if (line.fxStop) renderer.stopEffect(typeof line.fx === 'object' ? line.fx.name : 'fx')
if (line.fx) {
const name = typeof line.fx === 'string' ? line.fx : line.fx.name
const opts = typeof line.fx === 'object' ? line.fx : undefined
renderer.playEffect(name, opts)
}
// 音效 / BGM
if (line.sfxStop) audio.stopAll()
if (line.sfx) {
const url = typeof line.sfx === 'string' ? line.sfx : line.sfx.url
const volume = typeof line.sfx === 'object' ? line.sfx.volume : undefined
// 默认不循环(对齐 OLD playSfx:单次播放;要循环需显式 sfx.loop:true)。
const loop = typeof line.sfx === 'object' && line.sfx.loop === true
const handle = audio.play({ url }, { volume, loop })
if (typeof line.sfx === 'object' && line.sfx.until === 'lineEnd') {
sfxState.untilLineEnd = handle
}
}
// 镜头(行首即发,不阻塞、不计入本行时长)
if (line.cam && camera) camera.applyCommand(line.cam, ctx.focusLookup)
// 朝向 / 头朝向:拆开互不耦合(移植自 OLD play-demo.html:203-204 + character-actor setFacing/setHeadFacing)。
// 仅当行有 line.facing 才改 body;仅当行有 line.headFacing 才改头。
// applyFacing(facing, headFacing):facing=undefined → 不动身体;headFacing=undefined → 不动头。
if (alias && line.facing) renderer.applyFacing(alias, line.facing, undefined)
if (alias && line.headFacing !== undefined) {
renderer.applyFacing(alias, undefined, line.headFacing)
}
if (alias && line.emotion) renderer.applyFace(alias, line.emotion, false)
// 姿势(多轨道并行设置)
if (alias && anim && line.pose) {
for (const [track, clip] of Object.entries(line.pose)) {
anim.setAnimation(track, clip, { mix: 200 })
}
}
// 触发(oneshot)
if (alias && anim && line.trigger) {
anim.setAnimation(line.trigger.track, line.trigger.clip, { mix: 120 })
}
// 角色携带 / 放下 / 扔道具:真挂到骨骼(renderer.holdProp/releaseProp,移植自 OLD holdProp)。
if (alias && line.prop && typeof renderer.holdProp === 'function') {
const prop = /** @type {{name?:string,hold?:string,drop?:boolean,throw?:boolean}} */ (line.prop)
if (prop.drop) renderer.releaseProp(alias)
else if (prop.throw) {
if (anim) anim.setAnimation('arm_r', 'arm_r.punch', { mix: 120 })
renderer.releaseProp(alias)
} else if (prop.name) {
renderer.holdProp(alias, prop.name, prop.hold || 'carry')
}
}
}
/**
* 无 voice 时按字数估算台词时长(移植自原型 sayDuration 思路)。
* @param {string} text
* @returns {number} ms
*/
export function estimateSayDuration(text) {
if (!text) return 0
// 对齐 OLD play-demo.html:120 `sayDuration = Math.max(900, text.length*55 + 500)`。
return Math.max(900, text.length * 55 + 500)
}