Initial commit: Cocos Creator恋爱游戏引擎

- StoryManager: 剧情管理器
- DialogueBox: 对话框组件(带打字机效果)
- CharacterView: 立绘组件
- ChoiceButton: 选项按钮
- AffectionSystem: 好感度系统
- chapter1.json: 示例剧情
This commit is contained in:
openclaw_frontend_developer 2026-03-11 04:54:43 +00:00
commit 41e7ca05f1
8 changed files with 1460 additions and 0 deletions

148
README.md Normal file
View File

@ -0,0 +1,148 @@
# Cocos Dating Game
基于 Cocos Creator 3.x 的恋爱养成游戏引擎。
## 功能特性
- 🎭 剧情系统 - 支持分支对话和选项
- ❤️ 好感度系统 - 角色好感度等级
- 🎬 立绘切换 - 多种表情和位置
- 💾 存档系统 - 本地存储进度
## 项目结构
```
assets/
├── scripts/
│ ├── StoryManager.ts # 剧情管理器
│ ├── DialogueBox.ts # 对话框组件
│ ├── CharacterView.ts # 立绘组件
│ ├── ChoiceButton.ts # 选项按钮
│ └── AffectionSystem.ts # 好感度系统
├── resources/
│ ├── backgrounds/ # 背景图
│ ├── characters/ # 角色立绘
│ │ └── {角色ID}/
│ │ ├── normal.png
│ │ ├── happy.png
│ │ ├── angry.png
│ │ └── smile.png
│ └── story/
│ └── chapter1.json # 剧情脚本
└── scenes/
└── MainScene.ts # 主场景
```
## 快速开始
### 1. 安装 Cocos Creator
下载并安装 [Cocos Creator 3.x](https://www.cocos.com/creator)
### 2. 创建项目
1. 打开 Cocos Creator
2. 选择 "新建项目" -> "空白项目"
3. 将本项目 assets 目录下的文件复制到新项目中
### 3. 添加资源
`resources` 目录下添加:
- 背景图 (1920x1080)
- 角色立绘 (按角色ID和表情命名)
- 音效/音乐 (可选)
### 4. 运行项目
点击 Cocos Creator 中的 "运行" 按钮
## 剧情脚本格式
参考 `chapter1.json`:
```json
{
"title": "第一章",
"scenes": [
{
"id": "scene_1",
"background": "bg.jpg",
"characters": [
{
"id": "角色ID",
"name": "显示名称",
"emotion": "normal",
"position": "center",
"visible": true
}
],
"dialogue": [
{
"speaker": "说话者",
"text": "对话内容",
"emotion": "happy"
}
],
"choices": [
{
"text": "选项文本",
"nextScene": "下一场景ID",
"affectionChange": { "角色ID": 5 }
}
]
}
]
}
```
## API 文档
### StoryManager
```typescript
// 加载章节
StoryManager.instance.loadChapter('chapter1');
// 跳转到指定场景
StoryManager.instance.goToScene('scene_id');
// 重新开始
StoryManager.instance.restart();
```
### AffectionSystem
```typescript
// 获取好感度
const value = AffectionSystem.instance.getAffection('角色ID');
// 检查等级
const isLover = AffectionSystem.instance.hasReachedLevel(
'角色ID',
AffectionLevel.LOVER
);
// 保存存档
const saveData = AffectionSystem.instance.save();
// 加载存档
AffectionSystem.instance.load(saveData);
```
## 角色立绘规格
| 位置 | 尺寸 |
|------|------|
| left/right | 400x600 px |
| center | 500x700 px |
表情文件命名:`{emotion}.png`
例如:`normal.png`, `happy.png`, `angry.png`, `sad.png`, `surprised.png`, `smile.png`
## 许可证
MIT License
## 贡献
欢迎提交 Issue 和 Pull Request

View File

@ -0,0 +1,249 @@
{
"title": "第一章 - 相遇",
"scenes": [
{
"id": "scene_intro",
"background": "bg_park.jpg",
"characters": [],
"dialogue": [
{
"text": "春天的阳光洒在公园的长椅上..."
},
{
"text": "今天的天气真好啊,适合出来散步。"
}
],
"nextScene": "scene_meet_li"
},
{
"id": "scene_meet_li",
"background": "bg_park.jpg",
"characters": [
{
"id": "li_ze_yan",
"name": "李泽言",
"emotion": "normal",
"position": "center",
"visible": true
}
],
"dialogue": [
{
"speaker": "李泽言",
"text": "你好,请问你是?",
"emotion": "normal"
},
{
"speaker": "你",
"text": "啊...你好,我叫..."
},
{
"speaker": "李泽言",
"text": "看起来你很喜欢这里的风景。",
"emotion": "smile"
}
],
"choices": [
{
"text": "是的,这里很美。",
"nextScene": "scene_li_option1",
"affectionChange": {
"li_ze_yan": 5
}
},
{
"text": "我在等人。",
"nextScene": "scene_li_option2",
"affectionChange": {
"li_ze_yan": 0
}
},
{
"text": "你经常来这里吗?",
"nextScene": "scene_li_option3",
"affectionChange": {
"li_ze_yan": 3
}
}
]
},
{
"id": "scene_li_option1",
"background": "bg_park.jpg",
"characters": [
{
"id": "li_ze_yan",
"name": "李泽言",
"emotion": "happy",
"position": "center",
"visible": true
}
],
"dialogue": [
{
"speaker": "李泽言",
"text": "确实,这个季节的花开得很漂亮。",
"emotion": "happy"
},
{
"speaker": "李泽言",
"text": "如果你感兴趣,我知道一个更美的去处。",
"emotion": "smile"
}
],
"nextScene": "scene_meet_xu"
},
{
"id": "scene_li_option2",
"background": "bg_park.jpg",
"characters": [
{
"id": "li_ze_yan",
"name": "李泽言",
"emotion": "surprised",
"position": "center",
"visible": true
}
],
"dialogue": [
{
"speaker": "李泽言",
"text": "等人吗...那我不打扰了。",
"emotion": "surprised"
}
],
"nextScene": "scene_meet_xu"
},
{
"id": "scene_li_option3",
"background": "bg_park.jpg",
"characters": [
{
"id": "li_ze_yan",
"name": "李泽言",
"emotion": "normal",
"position": "center",
"visible": true
}
],
"dialogue": [
{
"speaker": "李泽言",
"text": "偶尔会来。工作之余放松一下。",
"emotion": "normal"
}
],
"nextScene": "scene_meet_xu"
},
{
"id": "scene_meet_xu",
"background": "bg_cafe.jpg",
"characters": [
{
"id": "li_ze_yan",
"name": "李泽言",
"emotion": "normal",
"position": "left",
"visible": true
},
{
"id": "xu_mo",
"name": "许墨",
"emotion": "smile",
"position": "right",
"visible": true
}
],
"dialogue": [
{
"text": "(画面切换到一家咖啡馆)"
},
{
"speaker": "许墨",
"text": "你好,看来我们又见面了。",
"emotion": "smile"
},
{
"speaker": "你",
"text": "你是?"
},
{
"speaker": "许墨",
"text": "我叫许墨。很高兴认识你。",
"emotion": "normal"
}
],
"choices": [
{
"text": "你好,许墨。",
"nextScene": "scene_xu_option1",
"affectionChange": {
"xu_mo": 5
}
},
{
"text": "我们之前见过吗?",
"nextScene": "scene_xu_option2",
"affectionChange": {
"xu_mo": 3
}
}
]
},
{
"id": "scene_xu_option1",
"background": "bg_cafe.jpg",
"characters": [
{
"id": "xu_mo",
"name": "许墨",
"emotion": "happy",
"position": "right",
"visible": true
}
],
"dialogue": [
{
"speaker": "许墨",
"text": "我也一样。这家咖啡不错,要一起吗?",
"emotion": "happy"
}
],
"nextScene": "scene_ending"
},
{
"id": "scene_xu_option2",
"background": "bg_cafe.jpg",
"characters": [
{
"id": "xu_mo",
"name": "许墨",
"emotion": "smile",
"position": "right",
"visible": true
}
],
"dialogue": [
{
"speaker": "许墨",
"text": "也许在某个地方见过吧。缘分是很奇妙的东西。",
"emotion": "smile"
}
],
"nextScene": "scene_ending"
},
{
"id": "scene_ending",
"background": "bg_cafe.jpg",
"characters": [],
"dialogue": [
{
"text": "(今天遇到了两个特别的人..."
},
{
"text": "(第一章完)"
}
]
}
]
}

View File

@ -0,0 +1,259 @@
import { _decorator, Component, Label, Node } from 'cc';
const { ccclass, property } = _decorator;
// 好感度等级
export enum AffectionLevel {
STRANGER = '陌生人', // 0-20
ACQUAINTANCE = '认识的人', // 21-40
FRIEND = '朋友', // 41-60
CLOSE = '恋人未满', // 61-80
LOVER = '恋人' // 81-100
}
// 角色好感度数据
interface CharacterAffection {
value: number;
level: AffectionLevel;
events: string[]; // 触发过的事件
}
// 好感度变化事件
interface AffectionEvent {
characterId: string;
change: number;
reason: string;
timestamp: number;
}
@ccclass('AffectionSystem')
export class AffectionSystem extends Component {
@property(Node)
affectionDisplay: Node;
// 角色好感度
private affection: Map<string, CharacterAffection> = new Map();
// 好感度变化历史
private history: AffectionEvent[] = [];
// 最大好感度
private maxAffection: number = 100;
onLoad() {
// 初始化角色好感度
this.initCharacters(['li_ze_yan', 'xu_mo', 'zhou_qi_luo', 'bai_qi']);
}
/**
*
*/
initCharacters(characterIds: string[]) {
characterIds.forEach(id => {
this.affection.set(id, {
value: 0,
level: AffectionLevel.STRANGER,
events: []
});
});
}
/**
*
*/
changeAffection(changes: { [key: string]: number }, reason: string = '选项') {
for (const [characterId, change] of Object.entries(changes)) {
const current = this.affection.get(characterId);
if (!current) {
console.warn(`角色不存在: ${characterId}`);
continue;
}
// 计算新值
const oldValue = current.value;
const newValue = Math.max(0, Math.min(this.maxAffection, current.value + change));
// 更新数据
current.value = newValue;
current.level = this.calculateLevel(newValue);
// 记录事件
current.events.push(reason);
// 记录历史
this.history.push({
characterId,
change,
reason,
timestamp: Date.now()
});
console.log(`好感度变化: ${characterId} ${oldValue} -> ${newValue} (${change > 0 ? '+' : ''}${change})`);
// 显示提示
this.showAffectionChange(characterId, change);
}
// 更新显示
this.updateDisplay();
}
/**
*
*/
private calculateLevel(value: number): AffectionLevel {
if (value <= 20) return AffectionLevel.STRANGER;
if (value <= 40) return AffectionLevel.ACQUAINTANCE;
if (value <= 60) return AffectionLevel.FRIEND;
if (value <= 80) return AffectionLevel.CLOSE;
return AffectionLevel.LOVER;
}
/**
*
*/
getAffection(characterId: string): number {
const data = this.affection.get(characterId);
return data ? data.value : 0;
}
/**
*
*/
getAffectionLevel(characterId: string): AffectionLevel {
const data = this.affection.get(characterId);
return data ? data.level : AffectionLevel.STRANGER;
}
/**
*
*/
getAllAffection(): Map<string, CharacterAffection> {
return this.affection;
}
/**
*
*/
hasReachedLevel(characterId: string, level: AffectionLevel): boolean {
const currentLevel = this.getAffectionLevel(characterId);
const levelOrder = [
AffectionLevel.STRANGER,
AffectionLevel.ACQUAINTANCE,
AffectionLevel.FRIEND,
AffectionLevel.CLOSE,
AffectionLevel.LOVER
];
return levelOrder.indexOf(currentLevel) >= levelOrder.indexOf(level);
}
/**
*
*/
getHighestAffectionCharacter(): string {
let maxId = '';
let maxValue = -1;
this.affection.forEach((data, id) => {
if (data.value > maxValue) {
maxValue = data.value;
maxId = id;
}
});
return maxId;
}
/**
*
*/
reset() {
this.affection.forEach((data) => {
data.value = 0;
data.level = AffectionLevel.STRANGER;
data.events = [];
});
this.history = [];
this.updateDisplay();
}
/**
*
*/
save(): string {
const saveData = {
affection: Object.fromEntries(this.affection),
history: this.history
};
return JSON.stringify(saveData);
}
/**
*
*/
load(jsonString: string) {
try {
const saveData = JSON.parse(jsonString);
if (saveData.affection) {
this.affection = new Map(Object.entries(saveData.affection));
}
if (saveData.history) {
this.history = saveData.history;
}
this.updateDisplay();
} catch (e) {
console.error('加载存档失败:', e);
}
}
/**
*
*/
private updateDisplay() {
if (!this.affectionDisplay) return;
// 遍历显示节点并更新
// 这里需要根据实际UI结构来实现
// 示例假设子节点名为角色ID
this.affectionDisplay.children.forEach(child => {
const characterId = child.name;
const data = this.affection.get(characterId);
if (data) {
const label = child.getComponent(Label);
if (label) {
label.string = `${characterId}: ${data.value} (${data.level})`;
}
}
});
}
/**
*
*/
private showAffectionChange(characterId: string, change: number) {
// 可以在这里添加飘字效果
// 示例:
// this.showFloatingText(`${change > 0 ? '+' : ''}${change}`, characterId);
console.log(`[${characterId}] 好感度 ${change > 0 ? '+' : ''}${change}`);
}
/**
*
*/
getHistory(): AffectionEvent[] {
return this.history;
}
/**
*
*/
getCharacterHistory(characterId: string): AffectionEvent[] {
return this.history.filter(e => e.characterId === characterId);
}
}

View File

@ -0,0 +1,183 @@
import { _decorator, Component, Node, Sprite, SpriteFrame, resources, tween, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
// 角色位置
type CharacterPosition = 'left' | 'center' | 'right';
// 角色数据
interface CharacterData {
id: string;
name: string;
emotion: string;
position: CharacterPosition;
visible: boolean;
}
@ccclass('CharacterView')
export class CharacterView extends Component {
@property(Node)
leftCharacter: Node;
@property(Node)
centerCharacter: Node;
@property(Node)
rightCharacter: Node;
// 角色资源缓存
private characterAssets: Map<string, Map<string, SpriteFrame>> = new Map();
onLoad() {
// 初始化所有角色隐藏
this.hideAllCharacters();
}
/**
*
*/
showCharacter(characterId: string, name: string, emotion: string, position: CharacterPosition) {
const node = this.getCharacterNode(position);
if (!node) return;
// 加载角色立绘
this.loadCharacterSprite(characterId, emotion, (spriteFrame) => {
if (spriteFrame) {
const sprite = node.getComponent(Sprite);
if (sprite) {
sprite.spriteFrame = spriteFrame;
}
// 显示节点
node.active = true;
// 播放出场动画
this.playEnterAnimation(node);
}
});
}
/**
*
*/
hideCharacter(characterId: string) {
// 遍历所有位置查找并隐藏
[this.leftCharacter, this.centerCharacter, this.rightCharacter].forEach(node => {
if (node && node.active) {
node.active = false;
}
});
}
/**
*
*/
hideAllCharacters() {
[this.leftCharacter, this.centerCharacter, this.rightCharacter].forEach(node => {
if (node) {
node.active = false;
}
});
}
/**
*
*/
setEmotion(emotion: string) {
// 这个方法需要在具体场景中根据当前显示的角色来实现
// 可以通过在场景中保存当前角色ID来调用
console.log('切换表情:', emotion);
}
/**
*
*/
private getCharacterNode(position: CharacterPosition): Node {
switch (position) {
case 'left':
return this.leftCharacter;
case 'center':
return this.centerCharacter;
case 'right':
return this.rightCharacter;
default:
return this.centerCharacter;
}
}
/**
*
*/
private loadCharacterSprite(characterId: string, emotion: string, callback: (spriteFrame: SpriteFrame) => void) {
const path = `characters/${characterId}/${emotion}`;
// 检查缓存
if (this.characterAssets.has(characterId)) {
const emotionMap = this.characterAssets.get(characterId);
if (emotionMap && emotionMap.has(emotion)) {
callback(emotionMap.get(emotion));
return;
}
}
// 加载新资源
resources.load(path, SpriteFrame, (err, spriteFrame) => {
if (err) {
console.warn(`加载角色立绘失败: ${path}`, err);
callback(null);
return;
}
// 缓存
if (!this.characterAssets.has(characterId)) {
this.characterAssets.set(characterId, new Map());
}
this.characterAssets.get(characterId).set(emotion, spriteFrame);
callback(spriteFrame);
});
}
/**
*
*/
private playEnterAnimation(node: Node) {
// 从下方进入
const originalPos = node.position.clone();
node.setPosition(originalPos.x, originalPos.y - 200, originalPos.z);
tween(node)
.to(0.3, { position: originalPos }, { easing: 'backOut' })
.start();
}
/**
* 退
*/
playExitAnimation(node: Node, callback?: () => void) {
tween(node)
.to(0.3, { position: new Vec3(node.position.x, node.position.y - 200, node.position.z) })
.call(() => {
node.active = false;
if (callback) callback();
})
.start();
}
/**
*
*/
preloadCharacter(characterId: string, emotions: string[]) {
emotions.forEach(emotion => {
const path = `characters/${characterId}/${emotion}`;
resources.preload(path, SpriteFrame);
});
}
/**
*
*/
clearCache() {
this.characterAssets.clear();
}
}

View File

@ -0,0 +1,136 @@
import { _decorator, Component, Node, Label, Button, Sprite, tween, Vec3, Color } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ChoiceButton')
export class ChoiceButton extends Component {
@property(Label)
textLabel: Label;
@property(Button)
button: Button;
@property(Sprite)
background: Sprite;
private onClickCallback: () => void = null;
private isClicked: boolean = false;
onLoad() {
// 确保有 Button 组件
if (!this.button) {
this.button = this.getComponent(Button);
}
if (this.button) {
this.button.node.on('click', this.onButtonClick, this);
}
// 默认隐藏
this.node.active = false;
}
onDestroy() {
if (this.button) {
this.button.node.off('click', this.onButtonClick, this);
}
}
/**
*
*/
setup(text: string, onClick: () => void) {
if (this.textLabel) {
this.textLabel.string = text;
}
this.onClickCallback = onClick;
this.isClicked = false;
this.node.active = true;
// 重置状态
this.setHoverState(false);
}
/**
*
*/
private onButtonClick() {
if (this.isClicked) return;
this.isClicked = true;
// 播放点击动画
this.playClickAnimation(() => {
// 执行回调
if (this.onClickCallback) {
this.onClickCallback();
}
});
}
/**
*
*/
private playClickAnimation(callback: () => void) {
// 缩小动画
tween(this.node)
.to(0.1, { scale: new Vec3(0.95, 0.95, 1) })
.to(0.1, { scale: new Vec3(1, 1, 1) })
.call(callback)
.start();
}
/**
*
*/
setHoverState(isHover: boolean) {
if (!this.background) return;
// 简单的悬停效果
// 实际项目中可以根据需要调整颜色或缩放
const targetScale = isHover ? 1.05 : 1;
tween(this.node)
.to(0.1, { scale: new Vec3(targetScale, targetScale, 1) })
.start();
}
/**
*
*/
setEnabled(enabled: boolean) {
if (this.button) {
this.button.interactable = enabled;
}
// 禁用时降低透明度
const opacity = enabled ? 255 : 128;
if (this.background) {
this.background.color = new Color(255, 255, 255, opacity);
}
}
/**
*
*/
getText(): string {
return this.textLabel ? this.textLabel.string : '';
}
/**
*
*/
hide() {
this.node.active = false;
this.isClicked = false;
}
/**
*
*/
show() {
this.node.active = true;
this.isClicked = false;
}
}

View File

@ -0,0 +1,177 @@
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
*
*/
setTypingSpeed(speed: number) {
this.typingSpeed = speed;
}
/**
*
*/
getCurrentSpeaker(): string {
return this.currentSpeaker;
}
/**
*
*/
getCurrentText(): string {
return this.currentText;
}
}

View File

@ -0,0 +1,280 @@
import { _decorator, Component, Node, resources, JsonAsset, Sprite, SpriteFrame, Label, Button, director } from 'cc';
import { DialogueBox } from './DialogueBox';
import { CharacterView } from './CharacterView';
import { ChoiceButton } from './ChoiceButton';
import { AffectionSystem } from './AffectionSystem';
const { ccclass, property } = _decorator;
// 剧情数据结构
interface StoryData {
title: string;
scenes: SceneData[];
}
interface SceneData {
id: string;
background?: string;
characters: CharacterShowData[];
dialogue: DialogueData[];
choices?: ChoiceData[];
nextScene?: string;
}
interface CharacterShowData {
id: string;
name: string;
emotion: string;
position: 'left' | 'center' | 'right';
visible: boolean;
}
interface DialogueData {
speaker?: string;
text: string;
emotion?: string;
}
interface ChoiceData {
text: string;
nextScene: string;
affectionChange?: { [key: string]: number };
}
@ccclass('StoryManager')
export class StoryManager extends Component {
private static _instance: StoryManager = null;
public static get instance(): StoryManager {
return StoryManager._instance;
}
@property(DialogueBox)
dialogueBox: DialogueBox;
@property(CharacterView)
characterView: CharacterView;
@property(Node)
choiceContainer: Node;
@property(AffectionSystem)
affectionSystem: AffectionSystem;
private storyData: StoryData = null;
private currentScene: SceneData = null;
private currentDialogueIndex: number = 0;
private isTyping: boolean = false;
onLoad() {
StoryManager._instance = this;
}
start() {
// 自动加载第一章
this.loadChapter('chapter1');
}
/**
*
*/
loadChapter(chapterId: string) {
resources.load(`story/${chapterId}`, JsonAsset, (err, jsonAsset) => {
if (err) {
console.error('加载剧情失败:', err);
return;
}
this.storyData = jsonAsset.json as StoryData;
console.log('剧情加载成功:', this.storyData.title);
// 开始播放第一个场景
if (this.storyData.scenes.length > 0) {
this.playScene(this.storyData.scenes[0].id);
}
});
}
/**
*
*/
playScene(sceneId: string) {
const scene = this.storyData.scenes.find((s) => s.id === sceneId);
if (!scene) {
console.error('未找到场景:', sceneId);
return;
}
this.currentScene = scene;
this.currentDialogueIndex = 0;
// 设置背景
if (scene.background) {
this.loadBackground(scene.background);
}
// 更新角色显示
this.updateCharacters(scene.characters);
// 隐藏选项
this.hideChoices();
// 开始对话
this.showNextDialogue();
}
/**
*
*/
showNextDialogue() {
if (!this.currentScene) return;
// 检查是否还有对话
if (this.currentDialogueIndex >= this.currentScene.dialogue.length) {
this.onDialogueEnd();
return;
}
const dialogue = this.currentScene.dialogue[this.currentDialogueIndex];
// 更新对话框
this.dialogueBox.show(dialogue.speaker, dialogue.text, () => {
this.isTyping = false;
});
// 更新立绘表情
if (dialogue.emotion && this.characterView) {
this.characterView.setEmotion(dialogue.emotion);
}
this.currentDialogueIndex++;
this.isTyping = true;
}
/**
*
*/
onDialogueClicked() {
if (this.isTyping) {
// 打字时点击直接显示完整文本
this.dialogueBox.finishTyping();
this.isTyping = false;
} else {
// 显示下一句
this.showNextDialogue();
}
}
/**
*
*/
private onDialogueEnd() {
if (!this.currentScene) return;
// 检查是否有选项
if (this.currentScene.choices && this.currentScene.choices.length > 0) {
this.showChoices(this.currentScene.choices);
}
// 检查是否有下一场景
else if (this.currentScene.nextScene) {
this.playScene(this.currentScene.nextScene);
}
else {
console.log('剧情结束');
}
}
/**
*
*/
private showChoices(choices: ChoiceData[]) {
this.hideChoices();
choices.forEach((choice, index) => {
const choiceNode = this.choiceContainer.children[index];
if (choiceNode) {
choiceNode.active = true;
const choiceBtn = choiceNode.getComponent(ChoiceButton);
if (choiceBtn) {
choiceBtn.setup(choice.text, () => {
this.onChoiceSelected(choice);
});
}
}
});
}
/**
*
*/
private hideChoices() {
this.choiceContainer.children.forEach(child => {
child.active = false;
});
}
/**
*
*/
private onChoiceSelected(choice: ChoiceData) {
// 更新好感度
if (choice.affectionChange) {
this.affectionSystem.changeAffection(choice.affectionChange);
}
// 跳转到下一场景
if (choice.nextScene) {
this.playScene(choice.nextScene);
}
}
/**
*
*/
private loadBackground(bgPath: string) {
resources.load(`backgrounds/${bgPath}`, SpriteFrame, (err, spriteFrame) => {
if (err) {
console.error('加载背景失败:', err);
return;
}
// 假设有一个背景节点
const bgNode = this.node.getChildByName('Background');
if (bgNode) {
const sprite = bgNode.getComponent(Sprite);
if (sprite) {
sprite.spriteFrame = spriteFrame;
}
}
});
}
/**
*
*/
private updateCharacters(characters: CharacterShowData[]) {
if (!this.characterView) return;
characters.forEach(char => {
if (char.visible) {
this.characterView.showCharacter(char.id, char.name, char.emotion, char.position);
} else {
this.characterView.hideCharacter(char.id);
}
});
}
/**
*
*/
public goToScene(sceneId: string) {
this.playScene(sceneId);
}
/**
*
*/
public restart() {
if (this.storyData && this.storyData.scenes.length > 0) {
this.affectionSystem.reset();
this.playScene(this.storyData.scenes[0].id);
}
}
}

28
project.json Normal file
View File

@ -0,0 +1,28 @@
{
"engine": "cocos-creator",
"version": "3.8.0",
"name": "cocos-dating-game",
"id": "cocos-dating-game",
"options": {
"appKey": "",
"companyName": "YourCompany",
"productName": "Cocos Dating Game",
"startScene": "db://assets/scenes/MainScene.scene"
},
"paths": {
"misc": "resources/misc",
"font": "resources/font",
"audio": "resources/audio",
"particle": "resources/particle",
"prefab": "resources/prefab",
"scene": "assets/scenes"
},
"scripts": {},
"library": {
"base": "library"
},
"native": {
"strip": true,
"compress": true
}
}