- StoryManager: 剧情管理器 - DialogueBox: 对话框组件(带打字机效果) - CharacterView: 立绘组件 - ChoiceButton: 选项按钮 - AffectionSystem: 好感度系统 - chapter1.json: 示例剧情
178 lines
4.3 KiB
TypeScript
178 lines
4.3 KiB
TypeScript
import { _decorator, Component, Node, Label, Sprite, RichText, tween, Vec3 } from 'cc';
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('DialogueBox')
|
||
export class DialogueBox extends Component {
|
||
@property(Label)
|
||
nameLabel: Label;
|
||
|
||
@property(RichText)
|
||
textRichText: RichText;
|
||
|
||
@property(Sprite)
|
||
nameBg: Sprite;
|
||
|
||
@property(Node)
|
||
continueIndicator: Node;
|
||
|
||
private currentText: string = '';
|
||
private currentSpeaker: string = '';
|
||
private typingInterval: number = null;
|
||
private onCompleteCallback: () => void = null;
|
||
private typingSpeed: number = 30; // 毫秒每个字符
|
||
|
||
onLoad() {
|
||
this.nameBg = this.nameLabel.node.parent.getComponent(Sprite);
|
||
}
|
||
|
||
start() {
|
||
// 默认隐藏
|
||
this.node.active = false;
|
||
}
|
||
|
||
/**
|
||
* 显示对话
|
||
*/
|
||
show(speaker: string | undefined, text: string, onComplete?: () => void) {
|
||
this.node.active = true;
|
||
this.currentText = text;
|
||
this.currentSpeaker = speaker || '';
|
||
this.onCompleteCallback = onComplete || null;
|
||
|
||
// 设置角色名
|
||
if (this.currentSpeaker) {
|
||
this.nameLabel.string = this.currentSpeaker;
|
||
this.nameBg.node.active = true;
|
||
} else {
|
||
this.nameBg.node.active = false;
|
||
}
|
||
|
||
// 隐藏继续提示
|
||
if (this.continueIndicator) {
|
||
this.continueIndicator.active = false;
|
||
}
|
||
|
||
// 开始打字机效果
|
||
this.typewriterEffect(text);
|
||
}
|
||
|
||
/**
|
||
* 打字机效果
|
||
*/
|
||
private typewriterEffect(text: string) {
|
||
// 清除之前的定时器
|
||
if (this.typingInterval) {
|
||
clearInterval(this.typingInterval);
|
||
}
|
||
|
||
let index = 0;
|
||
this.textRichText.string = '';
|
||
|
||
this.typingInterval = window.setInterval(() => {
|
||
if (index < text.length) {
|
||
// 使用 RichText 来支持富文本
|
||
this.textRichText.string = this.escapeXml(text.substring(0, index + 1));
|
||
index++;
|
||
} else {
|
||
this.finishTyping();
|
||
}
|
||
}, this.typingSpeed);
|
||
}
|
||
|
||
/**
|
||
* 完成打字(立即显示完整文本)
|
||
*/
|
||
finishTyping() {
|
||
if (this.typingInterval) {
|
||
clearInterval(this.typingInterval);
|
||
this.typingInterval = null;
|
||
}
|
||
|
||
this.textRichText.string = this.escapeXml(this.currentText);
|
||
|
||
// 显示继续提示
|
||
if (this.continueIndicator) {
|
||
this.continueIndicator.active = true;
|
||
this.playContinueIndicatorAnim();
|
||
}
|
||
|
||
// 回调
|
||
if (this.onCompleteCallback) {
|
||
this.onCompleteCallback();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 播放继续提示动画
|
||
*/
|
||
private playContinueIndicatorAnim() {
|
||
if (!this.continueIndicator) return;
|
||
|
||
// 简单的上下浮动动画
|
||
tween(this.continueIndicator)
|
||
.repeatForever(
|
||
tween()
|
||
.to(0.5, { position: new Vec3(0, 10, 0) })
|
||
.to(0.5, { position: new Vec3(0, 0, 0) })
|
||
)
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 隐藏对话框
|
||
*/
|
||
hide() {
|
||
this.node.active = false;
|
||
if (this.typingInterval) {
|
||
clearInterval(this.typingInterval);
|
||
this.typingInterval = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重置
|
||
*/
|
||
reset() {
|
||
this.nameLabel.string = '';
|
||
this.textRichText.string = '';
|
||
this.nameBg.node.active = false;
|
||
if (this.continueIndicator) {
|
||
this.continueIndicator.active = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 转义 XML 特殊字符(用于 RichText)
|
||
*/
|
||
private escapeXml(text: string): string {
|
||
return text
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
/**
|
||
* 设置打字速度
|
||
*/
|
||
setTypingSpeed(speed: number) {
|
||
this.typingSpeed = speed;
|
||
}
|
||
|
||
/**
|
||
* 获取当前说话者
|
||
*/
|
||
getCurrentSpeaker(): string {
|
||
return this.currentSpeaker;
|
||
}
|
||
|
||
/**
|
||
* 获取当前文本
|
||
*/
|
||
getCurrentText(): string {
|
||
return this.currentText;
|
||
}
|
||
}
|