Chat相关表现&逻辑分离,新增ChatController

新增情绪机器人逻辑,
新增情绪切换视频逻辑
This commit is contained in:
2025-08-26 19:41:13 +08:00
parent 58bac34580
commit 1507fff7c5
25 changed files with 2763 additions and 6933 deletions
BIN
View File
Binary file not shown.
+49 -7
View File
@@ -145,18 +145,60 @@ export default class LanguageUtils {
return text;
}
public static getTextWithParams(
public static getTextByLanguage(
key: string,
params: { [key: string]: any },
language: LanguageType = LanguageType.EN,
defaultText: string = ""
): string {
let text = this.getText(key, defaultText);
for (let paramKey in params) {
const regex = new RegExp(`{${paramKey}}`, "g");
text = text.replace(regex, params[paramKey].toString());
if (!this.isInitialized) {
this.init();
}
if (!key) {
return defaultText;
}
if (this.languageCache.has(key)) {
return this.languageCache.get(key);
}
if (!ConfigManager.tables || !ConfigManager.tables.TbLanguage) {
console.warn("Language table not loaded yet");
return defaultText || key;
}
const languageData = ConfigManager.tables.TbLanguage.get(key);
if (!languageData) {
console.warn(`Language key not found: ${key}`);
return defaultText || key;
}
let text: string = "";
switch (language) {
case LanguageType.EN:
text = languageData.languageEn;
break;
case LanguageType.CN:
text = languageData.languageCn;
break;
case LanguageType.HI:
text = languageData.languageHi;
break;
case LanguageType.FR:
text = languageData.languageFr;
break;
case LanguageType.DE:
text = languageData.languageDe;
break;
default:
text = languageData.languageEn;
}
if (!text || text.length === 0) {
text = languageData.languageEn || defaultText || key;
}
this.languageCache.set(key, text);
return text;
}
@@ -23,6 +23,7 @@ export enum InnerMsgCode {
BgVideo_End, //背景视频播放完毕
Chat_DialogRefresh,
Chat_EmotionInitialized,
SimplePlayVide,
LanguageChange,
}
@@ -60,6 +60,20 @@ export class ApiConfig {
return { ...this.config };
}
/**
* 获取情绪AI配置
*/
public getEmotionAIConfig(): AIConfig {
const config = ConfigManager.tables.TbGlobalConfig;
return {
apiKey: config.emotionApiKey,
model: config.Model,
temperature: config.Temperature,
maxTokens: config.MaxTokens,
timeout: config.Timeout,
};
}
/**
* 更新生成参数
* @param temperature 温度参数
@@ -5,6 +5,7 @@ import { RoleConfigLoader } from "./RoleConfigLoader";
import { ChatHistoryManager } from "../manager/ChatHistoryManager";
import { ApiConfig } from "./ApiConfigLoader";
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
import { EmotionAIService } from "./EmotionAIService";
/**
* AI聊天服务类
@@ -123,6 +124,8 @@ export class ChatAIService {
this.currentRoleId = roleId;
// 预创建聊天实例
this.createOrGetChat(roleId);
// 预创建情绪聊天实例
EmotionAIService.Instance.ensureEmotionChatExists(roleId);
}
/**
@@ -186,6 +189,14 @@ export class ChatAIService {
role: "model",
parts: [{ text: response.text }],
});
// 更新情绪状态
try {
await EmotionAIService.Instance.updateEmotionFromChat(roleId, message, response.text);
} catch (emotionError) {
console.warn(`Failed to update emotion for role ${roleId}:`, emotionError);
// 情绪更新失败不影响聊天功能
}
} catch (storageError) {
ErrorHandler.Instance.handleError(
storageError as Error,
@@ -224,6 +235,8 @@ export class ChatAIService {
if (this.chatInstances.has(roleId)) {
this.chatInstances.delete(roleId);
}
// 清除情绪聊天历史
EmotionAIService.Instance.clearEmotionHistory(roleId);
// 清除本地存储的历史
ChatHistoryManager.Instance.clearHistory(roleId);
console.log(`Cleared chat history for role ${roleId}`);
@@ -234,6 +247,8 @@ export class ChatAIService {
*/
public clearAllChatHistory(): void {
this.chatInstances.clear();
// 清除所有情绪聊天历史
EmotionAIService.Instance.clearAllEmotionHistory();
// 清除本地存储的所有历史
ChatHistoryManager.Instance.clearAllHistory();
console.log("Cleared all chat histories");
@@ -0,0 +1,250 @@
import { ChatAIService } from "./ChatAIService";
import { EmotionAIService } from "./EmotionAIService";
import { DialogManager } from "../manager/DialogManager";
import { VideoEmotion } from "../../schema/schema";
import ConfigManager from "../manager/ConfigManager";
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
/**
* 聊天控制器接口 - 定义Panel和Controller之间的通信协议
*/
export interface IChatPanelCallback {
/**
* 消息发送开始回调
* @param message 用户发送的消息
*/
onMessageSent(message: string): void;
/**
* 接收到AI回复回调
* @param response AI的回复内容
*/
onMessageReceived(response: string): void;
/**
* 情绪状态更新回调
* @param emotion 更新后的情绪状态
*/
onEmotionUpdated(emotion: VideoEmotion): void;
/**
* 对话更新回调
*/
onDialogUpdated(): void;
/**
* 错误处理回调
* @param error 错误信息
*/
onError(error: Error): void;
}
/**
* 聊天控制器类
*
* 负责处理聊天相关的所有业务逻辑,包括:
* - 角色数据管理
* - 消息发送和接收
* - 情绪状态管理
* - 与AI服务的交互
* - 对话历史管理
*
* @example
* ```typescript
* const controller = new ChatController();
* controller.initialize(10001, panelCallback);
* const response = await controller.sendMessage("Hello");
* ```
*/
export class ChatController {
private roleId: number | null = null;
private callback: IChatPanelCallback | null = null;
private dialogManager: DialogManager | null = null;
/**
* 初始化聊天控制器
* @param roleId 角色ID
* @param callback 回调接口实现
*/
public initialize(roleId: number, callback: IChatPanelCallback): void {
this.roleId = roleId;
this.callback = callback;
this.dialogManager = DialogManager.getInstance();
// 设置当前聊天的角色ID
if (roleId && roleId > 0) {
ChatAIService.Instance.setCurrentRole(roleId);
console.log(`ChatController initialized with role ${roleId}`);
} else {
const error = new Error(`Invalid roleId: ${roleId}`);
this.handleError(error);
}
}
/**
* 发送消息给AI并处理回复
* @param message 用户消息内容
* @returns Promise<string | null> AI的回复,失败时返回null
*/
public async sendMessage(message: string): Promise<string | null> {
if (!this.validateSendMessage(message)) {
return null;
}
try {
// 通知界面消息发送开始
this.callback?.onMessageSent(message);
// 更新对话显示 - 用户消息
this.dialogManager?.updateDialog(true, message, true);
this.callback?.onDialogUpdated();
console.log(`Sending message to role ${this.roleId}: ${message}`);
// 发送消息给AI服务
const response = await ChatAIService.Instance.sendMessage(this.roleId!, message);
if (response) {
// 更新对话显示 - AI回复
this.dialogManager?.updateDialog(false, response);
this.callback?.onDialogUpdated();
// 通知界面收到回复
this.callback?.onMessageReceived(response);
// 获取更新后的情绪状态
try {
const currentEmotion = EmotionAIService.Instance.getCurrentEmotion(this.roleId!);
console.log(`Current emotion for role ${this.roleId}: ${VideoEmotion[currentEmotion]}`);
// 通知界面情绪更新
this.callback?.onEmotionUpdated(currentEmotion);
} catch (emotionError) {
console.warn("Failed to get current emotion:", emotionError);
// 情绪获取失败不影响聊天功能
}
console.log(`Response received from role ${this.roleId}: ${response}`);
return response;
} else {
const error = new Error("AI返回了空响应");
this.handleError(error);
return null;
}
} catch (error) {
ErrorHandler.Instance.handleApiError(error, "ChatController.sendMessage", {
roleId: this.roleId,
message: message.substring(0, 100) + "..."
});
this.handleError(error as Error);
return null;
}
}
/**
* 获取当前角色的情绪状态
* @returns VideoEmotion 当前情绪状态
*/
public getCurrentEmotion(): VideoEmotion {
if (!this.roleId) {
return VideoEmotion.calm_down;
}
return EmotionAIService.Instance.getCurrentEmotion(this.roleId);
}
/**
* 获取当前角色数据
* @returns 角色数据对象,失败时返回null
*/
public getRoleData(): any {
if (!this.roleId) {
return null;
}
try {
const roleData = ConfigManager.tables.TbGirls.get(this.roleId);
const roleDetail = ConfigManager.tables.TbGirlsDetail.get(this.roleId);
return {
basic: roleData,
detail: roleDetail
};
} catch (error) {
console.error(`Failed to get role data for ${this.roleId}:`, error);
this.handleError(error as Error);
return null;
}
}
/**
* 清除当前角色的聊天历史
*/
public clearChatHistory(): void {
if (!this.roleId) {
console.warn("Cannot clear history: roleId is null");
return;
}
try {
ChatAIService.Instance.clearChatHistory(this.roleId);
console.log(`Chat history cleared for role ${this.roleId}`);
} catch (error) {
console.error("Failed to clear chat history:", error);
this.handleError(error as Error);
}
}
/**
* 获取当前角色ID
* @returns 当前角色ID
*/
public getCurrentRoleId(): number | null {
return this.roleId;
}
/**
* 销毁控制器,清理资源
*/
public destroy(): void {
this.roleId = null;
this.callback = null;
this.dialogManager = null;
console.log("ChatController destroyed");
}
/**
* 验证发送消息的参数
* @param message 消息内容
* @returns 验证是否通过
*/
private validateSendMessage(message: string): boolean {
if (!this.roleId || this.roleId <= 0) {
const error = new Error("角色ID无效");
this.handleError(error);
return false;
}
if (!message || message.trim() === "") {
const error = new Error("消息内容不能为空");
this.handleError(error);
return false;
}
if (!this.callback) {
const error = new Error("回调接口未设置");
this.handleError(error);
return false;
}
return true;
}
/**
* 统一错误处理
* @param error 错误对象
*/
private handleError(error: Error): void {
console.error("ChatController error:", error);
this.callback?.onError(error);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "458edd93-5ff2-4c11-a573-f8985c48cc15",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,418 @@
// 首先加载 polyfills 以确保兼容性
import "../utils/polyfills";
import { GoogleGenAI } from "@google/genai";
import { RoleConfigLoader } from "./RoleConfigLoader";
import { ChatHistoryManager } from "../manager/ChatHistoryManager";
import { ApiConfig } from "./ApiConfigLoader";
import { ErrorHandler, ErrorType } from "../utils/ErrorHandler";
import { VideoEmotion } from "../../schema/schema";
import ConfigManager from "../manager/ConfigManager";
import LanguageUtils, { LanguageType } from "../../Main/Common/LanguageUtils";
import Utils from "../../Main/Common/Utils";
import { InnerMsgCode } from "../../Main/Config/InnerMsgCode";
/**
* AI情绪分析服务类
*
* 基于Google Gemini API实现的情绪分析系统,支持:
* - 基于聊天历史的情绪状态分析
* - 多角色独立的情绪聊天实例管理
* - 返回标准的VideoEmotion枚举值
* - 自动初始化历史情绪分析
*
* @example
* ```typescript
* const emotionService = EmotionAIService.Instance;
* const emotion = await emotionService.analyzeEmotionalState(10001);
* console.log(VideoEmotion[emotion]); // "calm_down"
* ```
*
* @author AI Chat System
* @version 1.0.0
*/
export class EmotionAIService {
private static _instance: EmotionAIService;
private emotionAI: GoogleGenAI;
private emotionChatInstances: Map<number, any> = new Map();
private currentEmotions: Map<number, VideoEmotion> = new Map();
private constructor() {
const emotionConfig = ApiConfig.Instance.getEmotionAIConfig();
try {
this.emotionAI = new GoogleGenAI({ apiKey: emotionConfig.apiKey });
} catch (error) {
ErrorHandler.Instance.handleError(
error as Error,
ErrorType.API_ERROR,
{ config: { ...emotionConfig, apiKey: "***" } }, // 隐藏API密钥
true
);
throw error;
}
}
/**
* 获取EmotionAIService的单例实例
*
* @returns {EmotionAIService} 情绪分析服务实例
* @static
*/
public static get Instance(): EmotionAIService {
if (!this._instance) {
this._instance = new EmotionAIService();
}
return this._instance;
}
/**
* 创建或获取指定角色的情绪聊天实例
* @param roleId 角色ID
*/
private createOrGetEmotionChat(roleId: number): any {
if (!this.emotionChatInstances.has(roleId)) {
const systemInstruction = RoleConfigLoader.getEmotionInstruction(roleId);
const config = ApiConfig.Instance.getEmotionAIConfig();
const chat = this.emotionAI.chats.create({
model: config.model,
config: {
temperature: config.temperature,
systemInstruction: systemInstruction,
},
});
this.emotionChatInstances.set(roleId, chat);
console.log(`Created new emotion chat instance for role ${roleId}`);
// 异步分析历史情绪状态
this.initializeEmotionAnalysis(roleId);
}
return this.emotionChatInstances.get(roleId);
}
/**
* 初始化情绪分析(检查是否有历史记录并进行分析)
* @param roleId 角色ID
*/
private async initializeEmotionAnalysis(roleId: number): Promise<void> {
try {
// 检查是否有聊天历史
const messageCount = ChatHistoryManager.Instance.getMessageCount(roleId);
let emotionalState: VideoEmotion;
if (messageCount > 0) {
console.log(
`Analyzing emotional state for role ${roleId} based on ${messageCount} messages`
);
// 分析历史情绪状态
emotionalState = await this.analyzeEmotionalState(roleId);
console.log(
`Initial emotional state for role ${roleId}: ${VideoEmotion[emotionalState]}`
);
} else {
console.log(
`No history found for role ${roleId}, using default emotion: calm_down`
);
emotionalState = VideoEmotion.calm_down;
}
// 发送情绪初始化完成事件
Utils.sendInnerMsg(InnerMsgCode.Chat_EmotionInitialized, {
roleId: roleId,
emotion: emotionalState
});
console.log(`Emotion initialization completed for role ${roleId}: ${VideoEmotion[emotionalState]}`);
} catch (error) {
console.error(
`Failed to initialize emotion analysis for role ${roleId}:`,
error
);
// 即使发生错误,也发送默认情绪状态
Utils.sendInnerMsg(InnerMsgCode.Chat_EmotionInitialized, {
roleId: roleId,
emotion: VideoEmotion.calm_down
});
}
}
/**
* 格式化聊天历史用于情绪分析
* @param messages 聊天消息数组
* @returns 格式化后的分析提示
*/
private formatHistoryForEmotionAnalysis(messages: any[]): string {
if (messages.length === 0) {
return "没有聊天历史,请返回默认情绪状态:calm_down";
}
let conversationText =
"请分析以下对话的情绪状态,返回对应的VideoEmotion枚举值:\n\n";
messages.forEach((message, index) => {
const speaker = message.role === "user" ? "用户" : "AI";
const text = message.parts[0]?.text || "";
conversationText += `${speaker}: ${text}\n`;
});
conversationText +=
"\n请返回以下枚举值之一:calm_down, arousal, desire, passion, orgasm";
return conversationText;
}
/**
* 向指定角色的情绪AI发送消息并获取分析回复,返回VideoEmotion枚举值
* @param roleId 角色ID
* @param message 用户发送的消息内容
* @returns Promise<VideoEmotion> 情绪AI的分析结果,如果发生错误返回默认值calm_down
*/
public async sendEmotionMessage(
roleId: number,
message: string
): Promise<VideoEmotion> {
// 输入验证
if (!roleId || roleId <= 0) {
ErrorHandler.Instance.handleValidationError(
"roleId",
"角色ID必须是正整数",
roleId
);
return VideoEmotion.calm_down;
}
if (!message || message.trim() === "") {
ErrorHandler.Instance.handleValidationError(
"message",
"消息内容不能为空",
message
);
return VideoEmotion.calm_down;
}
try {
const emotionChat = this.createOrGetEmotionChat(roleId);
const response = await emotionChat.sendMessage({
message: message.trim(),
});
if (response && response.text) {
console.log(`Emotion analysis from role ${roleId}:`, response.text);
return this.parseEmotionResponse(response.text);
} else {
const warningMsg = `情绪AI返回了空响应 (角色ID: ${roleId})`;
ErrorHandler.Instance.handleError(
new Error(warningMsg),
ErrorType.API_ERROR,
{ roleId, message, response },
true
);
return VideoEmotion.calm_down;
}
} catch (error) {
ErrorHandler.Instance.handleApiError(error, "sendEmotionMessage", {
roleId,
message: message.substring(0, 100) + "...",
});
return VideoEmotion.calm_down;
}
}
/**
* 分析指定角色的情绪状态(基于最近10条聊天记录)
* @param roleId 角色ID
* @returns Promise<VideoEmotion> 情绪分析结果
*/
public async analyzeEmotionalState(roleId: number): Promise<VideoEmotion> {
try {
// 获取最近10条消息
const recentMessages = ChatHistoryManager.Instance.getRecentMessages(
roleId,
10
);
// 格式化历史记录用于分析
const analysisPrompt =
this.formatHistoryForEmotionAnalysis(recentMessages);
// 发送给情绪AI进行分析
return await this.sendEmotionMessage(roleId, analysisPrompt);
} catch (error) {
ErrorHandler.Instance.handleError(
error as Error,
ErrorType.API_ERROR,
{ roleId },
false
);
return VideoEmotion.calm_down;
}
}
/**
* 解析情绪AI返回的文本为VideoEmotion枚举值
* @param responseText AI返回的文本
* @returns VideoEmotion枚举值
*/
private parseEmotionResponse(responseText: string): VideoEmotion {
const text = responseText.toLowerCase().trim();
if (text.includes("arousal")) {
return VideoEmotion.arousal;
} else if (text.includes("desire")) {
return VideoEmotion.desire;
} else if (text.includes("passion")) {
return VideoEmotion.passion;
} else if (text.includes("orgasm")) {
return VideoEmotion.orgasm;
} else {
return VideoEmotion.calm_down; // 默认值
}
}
/**
* 确保指定角色的情绪聊天实例已创建
* @param roleId 角色ID
*/
public ensureEmotionChatExists(roleId: number): void {
this.createOrGetEmotionChat(roleId);
}
/**
* 清除指定角色的情绪聊天历史
* @param roleId 角色ID
*/
public clearEmotionHistory(roleId: number): void {
if (this.emotionChatInstances.has(roleId)) {
this.emotionChatInstances.delete(roleId);
}
console.log(`Cleared emotion chat history for role ${roleId}`);
}
/**
* 清除所有情绪聊天历史
*/
public clearAllEmotionHistory(): void {
this.emotionChatInstances.clear();
console.log("Cleared all emotion chat histories");
}
/**
* 获取当前活跃的情绪聊天实例数量
*/
public getActiveEmotionChatCount(): number {
return this.emotionChatInstances.size;
}
/**
* 获取指定角色的当前情绪状态
* @param roleId 角色ID
* @returns VideoEmotion 当前情绪状态
*/
public getCurrentEmotion(roleId: number): VideoEmotion {
return this.currentEmotions.get(roleId) || VideoEmotion.calm_down;
}
/**
* 设置指定角色的情绪状态
* @param roleId 角色ID
* @param emotion 情绪状态
*/
private setCurrentEmotion(roleId: number, emotion: VideoEmotion): void {
this.currentEmotions.set(roleId, emotion);
console.log(`Updated emotion for role ${roleId}: ${VideoEmotion[emotion]}`);
}
/**
* 获取角色的英文名
* @param roleId 角色ID
* @returns 角色英文名,如果未找到返回"AI"
*/
private getRoleEnglishName(roleId: number): string {
try {
if (ConfigManager.tables && ConfigManager.tables.TbGirls) {
const girlData = ConfigManager.tables.TbGirls.get(roleId);
if (girlData && girlData.nameKey) {
return (
LanguageUtils.getTextByLanguage(
girlData.nameKey,
LanguageType.EN
) || "AI"
);
}
}
} catch (error) {
console.warn(`Failed to get role name for ${roleId}:`, error);
}
return "AI";
}
/**
* 根据聊天对话更新情绪状态
* @param roleId 角色ID
* @param userMessage 用户消息
* @param aiResponse AI回复
* @returns Promise<VideoEmotion> 更新后的情绪状态
*/
public async updateEmotionFromChat(
roleId: number,
userMessage: string,
aiResponse: string
): Promise<VideoEmotion> {
try {
// 构建情绪分析提示,包含当前情绪上下文
const currentEmotion = this.getCurrentEmotion(roleId);
const analysisPrompt = this.formatChatForEmotionAnalysis(
roleId,
userMessage,
aiResponse,
currentEmotion
);
// 发送给情绪AI进行分析
const newEmotion = await this.sendEmotionMessage(roleId, analysisPrompt);
// 更新并保存情绪状态
this.setCurrentEmotion(roleId, newEmotion);
return newEmotion;
} catch (error) {
ErrorHandler.Instance.handleError(
error as Error,
ErrorType.API_ERROR,
{ roleId, userMessage: userMessage.substring(0, 50) },
false
);
return this.getCurrentEmotion(roleId); // 返回当前情绪状态
}
}
/**
* 格式化聊天对话用于情绪分析
* @param roleId 角色ID
* @param userMessage 用户消息
* @param aiResponse AI回复
* @param currentEmotion 当前情绪状态
* @returns 格式化后的分析提示
*/
private formatChatForEmotionAnalysis(
roleId: number,
userMessage: string,
aiResponse: string,
currentEmotion: VideoEmotion
): string {
const roleName = this.getRoleEnglishName(roleId);
let analysisText = `当前情绪状态: ${VideoEmotion[currentEmotion]}\n\n`;
analysisText += `请分析以下最新对话的情绪变化,返回对应的VideoEmotion枚举值:\n\n`;
analysisText += `用户: ${userMessage}\n`;
analysisText += `${roleName}: ${aiResponse}\n\n`;
analysisText += `请基于对话内容和当前情绪状态,返回以下枚举值之一:calm_down, arousal, desire, passion, orgasm`;
return analysisText;
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "624f6a81-539f-4347-aef1-d7c98c226a68",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -13,10 +13,26 @@ export class RoleConfigLoader {
*/
public static getRoleInstruction(roleId: number): string {
const AiCharacter = ConfigManager.tables.TbAiCharacters.get(roleId);
const globalPrompt = ConfigManager.tables.TbGlobalConfig.girlBasePrompt;
const prompt =
globalPrompt +
"\n" +
AiCharacter.basePrompt +
"\n" +
AiCharacter.additionPrompt;
return prompt;
}
return AiCharacter
? AiCharacter.systemInstruction
: ConfigManager.tables.TbAiCharacters.get(10001).systemInstruction;
/**
* 根据角色ID获取对应的情绪机器人prompt
* @param roleId 角色ID
* @returns System Instruction字符串,如果未找到则返回默认Role_1
*/
public static getEmotionInstruction(roleId: number): string {
const AiCharacter = ConfigManager.tables.TbAiCharacters.get(roleId);
const globalPrompt = ConfigManager.tables.TbGlobalConfig.EmotionRating;
const prompt = globalPrompt + "\n" + AiCharacter.basePrompt;
return prompt;
}
/**
+300 -29
View File
@@ -6,11 +6,12 @@ import {
Label,
Sprite,
UITransform,
VideoPlayer,
} from "cc";
// 首先加载 polyfills 以确保兼容性
import "../../utils/polyfills";
import { DialogManager } from "../../manager/DialogManager";
import { ChatAIService } from "../../core/ChatAIService";
import { ChatController, IChatPanelCallback } from "../../core/ChatController";
import li_BaseView from "db://assets/Scripts/Main/Common/li_BaseView";
import Utils from "db://assets/Scripts/Main/Common/Utils";
import { InnerMsgCode } from "db://assets/Scripts/Main/Config/InnerMsgCode";
@@ -20,15 +21,15 @@ import GameRootUI from "db://assets/Scripts/Main/Common/GameRootUI";
import { ImagePopup } from "../components/ImagePopup";
import ResManager from "db://assets/Scripts/Main/Manager/ResManager";
import { VideoRoleType } from "db://assets/Scripts/Main/Common/GlobalValue";
import ConfigManager from "../../manager/ConfigManager";
import LanguageUtils from "../../../Main/Common/LanguageUtils";
import { UITransitionHelper } from "../../utils/UITransitionHelper";
import { VideoEmotion, purchase } from "../../../schema/schema";
const { ccclass, property } = _decorator;
@ccclass("ChatPanel")
export class ChatPanel extends li_BaseView {
export class ChatPanel extends li_BaseView implements IChatPanelCallback {
manager: DialogManager = null;
private chatController: ChatController = new ChatController();
@property(EditBox)
editBox: EditBox = null;
@@ -39,6 +40,9 @@ export class ChatPanel extends li_BaseView {
@property(Sprite)
girlImg: Sprite = null;
@property(VideoPlayer)
girlVideo: VideoPlayer = null;
@property(ChatContentsLayout)
layout: ChatContentsLayout = null;
@@ -49,6 +53,8 @@ export class ChatPanel extends li_BaseView {
private _nodeTab: any = {};
nameKey: string;
private commercialVideos: purchase.CommercialVideo[] = [];
private currentEmotion: VideoEmotion | null = null;
private onLanguageChangeCallback = () => {
if (!this.girlName) return;
@@ -57,9 +63,9 @@ export class ChatPanel extends li_BaseView {
openUIDataCT(data) {
// 处理新的数据格式,支持过渡动画参数
if (typeof data === 'object' && data.roleId !== undefined) {
if (typeof data === "object" && data.roleId !== undefined) {
this.id = data.roleId;
// 如果标记了需要滑入动画,则执行动画
if (data.withSlideTransition && this.node && this.node.isValid) {
// 延迟一帧执行动画,确保节点已正确加载到场景中
@@ -71,10 +77,10 @@ export class ChatPanel extends li_BaseView {
// 兼容原来的数字格式
this.id = data;
}
// 设置当前聊天的角色ID
// 初始化ChatController
if (this.id && this.id > 0) {
ChatAIService.Instance.setCurrentRole(this.id);
this.chatController.initialize(this.id, this);
}
}
@@ -83,6 +89,17 @@ export class ChatPanel extends li_BaseView {
this.register();
this.refresh(this.id);
// Add video loaded event callback
if (this.girlVideo) {
this.girlVideo.node.on(
VideoPlayer.EventType.READY_TO_PLAY,
() => {
this.adjustVideoScale();
},
this
);
}
}
register() {
Utils.addInnerEL(
@@ -96,6 +113,12 @@ export class ChatPanel extends li_BaseView {
this,
this.onLanguageChangeCallback
);
Utils.addInnerEL(
InnerMsgCode.Chat_EmotionInitialized,
this,
this.onEmotionInitialized
);
}
onDestroy(): void {
Utils.removeInnerEL(
@@ -108,12 +131,37 @@ export class ChatPanel extends li_BaseView {
this,
this.onLanguageChangeCallback
);
Utils.removeInnerEL(
InnerMsgCode.Chat_EmotionInitialized,
this,
this.onEmotionInitialized
);
// Cleanup video event listeners
if (this.girlVideo && this.girlVideo.node) {
this.girlVideo.node.off(
VideoPlayer.EventType.READY_TO_PLAY,
this.adjustVideoScale,
this
);
}
// 销毁ChatController
if (this.chatController) {
this.chatController.destroy();
}
}
refresh(id: number) {
this.id = id;
const data = ConfigManager.tables.TbGirls.get(this.id);
const dataDetail = ConfigManager.tables.TbGirlsDetail.get(this.id);
if (!data) return;
// 通过ChatController获取角色数据
const roleData = this.chatController.getRoleData();
if (!roleData || !roleData.basic) return;
const data = roleData.basic;
const dataDetail = roleData.detail;
this.nameKey = data.nameKey;
this.girlName.string = LanguageUtils.getText(data.nameKey);
ResManager.I.changeBundleSpriteFrame(
@@ -129,15 +177,172 @@ export class ChatPanel extends li_BaseView {
);
}
);
let uiTransform: UITransform =
this._nodeTab.BgFrame.getComponent(UITransform);
Utils.sendInnerMsg(InnerMsgCode.SimplePlayVide, {
path: dataDetail.pics[0],
size: uiTransform.contentSize,
bgFrameNode: this._nodeTab.BgFrame,
});
// Store commercialVideos for emotion-based switching
if (dataDetail && dataDetail.commercialVideos) {
this.commercialVideos = dataDetail.commercialVideos;
console.log(`Loaded ${this.commercialVideos.length} videos for role ${this.id}`);
// Load initial video based on current emotion
if (this.commercialVideos.length > 0 && this.girlVideo) {
// Get current emotion from ChatController
let currentEmotion = VideoEmotion.calm_down; // Default fallback
try {
if (this.chatController) {
currentEmotion = this.chatController.getCurrentEmotion();
console.log(`Current emotion for role ${this.id}: ${VideoEmotion[currentEmotion]}`);
}
} catch (error) {
console.warn("Failed to get current emotion during initialization, using calm_down as default:", error);
}
// Find video matching current emotion or fallback
const initialVideo = this.commercialVideos.find(video => video.emotion === currentEmotion)
|| this.commercialVideos.find(video => video.emotion === VideoEmotion.calm_down)
|| this.commercialVideos[0]; // Final fallback to first video
console.log(`Loading initial video: ${initialVideo.path} (emotion: ${VideoEmotion[initialVideo.emotion]})`);
// Set current emotion to the loaded video's emotion
this.currentEmotion = initialVideo.emotion;
ResManager.I.changeBundleVideo(
this.girlVideo,
initialVideo.path,
"Chat18x"
);
// Also immediately try to adjust scale (in case already loaded)
this.adjustVideoScale();
}
} else {
this.commercialVideos = [];
this.currentEmotion = null; // Reset current emotion when no videos available
console.log("No commercialVideos data available for role", this.id);
}
// if (dataDetail && dataDetail.commercialVideos && dataDetail.commercialVideos.length > 0) {
// let uiTransform: UITransform =
// this._nodeTab.BgFrame.getComponent(UITransform);
// Utils.sendInnerMsg(InnerMsgCode.SimplePlayVide, {
// path: dataDetail.commercialVideos[0].path,
// size: uiTransform.contentSize,
// bgFrameNode: this._nodeTab.BgFrame,
// });
// }
}
adjustVideoScale() {
if (!this.girlVideo || !this.girlVideo.node.parent) {
return;
}
const tryAdjustScale = () => {
const parentTransform =
this.girlVideo.node.parent.getComponent(UITransform);
const videoTransform = this.girlVideo.node.getComponent(UITransform);
if (parentTransform && videoTransform && videoTransform.height > 0) {
const scale = parentTransform.height / videoTransform.height;
this.girlVideo.node.setScale(scale, scale, 1);
console.log(
`ChatPanel Video scaled to: ${scale}, parent height: ${parentTransform.height}, video height: ${videoTransform.height}`
);
return true;
}
return false;
};
// Immediately try once
if (!tryAdjustScale()) {
// If failed, delay and try again
this.scheduleOnce(() => {
if (!tryAdjustScale()) {
// Try again with longer delay
this.scheduleOnce(() => {
tryAdjustScale();
}, 0.5);
}
}, 0.1);
}
}
setVideoEnable(enable: boolean) {
if (!this.girlVideo) return;
this.girlVideo.enabled = enable;
if (enable) {
this.girlVideo.play();
}
}
private switchVideoByEmotion(emotion: VideoEmotion): void {
if (!this.girlVideo || !this.commercialVideos || this.commercialVideos.length === 0) {
console.warn("Cannot switch video: missing video player or commercialVideos data");
return;
}
// Check if emotion has changed
if (this.currentEmotion === emotion) {
console.log(`Emotion ${VideoEmotion[emotion]} unchanged, skipping video switch`);
return;
}
// Find video matching the emotion
const matchingVideo = this.commercialVideos.find(video => video.emotion === emotion);
if (matchingVideo) {
console.log(`Switching to video for emotion ${VideoEmotion[emotion]}: ${matchingVideo.path} (from ${this.currentEmotion !== null ? VideoEmotion[this.currentEmotion] : 'null'})`);
// Load the new video
ResManager.I.changeBundleVideo(
this.girlVideo,
matchingVideo.path,
"Chat18x"
);
// Update current emotion state
this.currentEmotion = emotion;
// Adjust scale and enable playback
this.adjustVideoScale();
this.setVideoEnable(true);
} else {
console.warn(`No video found for emotion ${VideoEmotion[emotion]}, falling back to first available video`);
// Fallback to first video if no match found
if (this.commercialVideos.length > 0) {
const fallbackVideo = this.commercialVideos[0];
console.log(`Loading fallback video: ${fallbackVideo.path} (emotion: ${VideoEmotion[fallbackVideo.emotion]})`);
ResManager.I.changeBundleVideo(
this.girlVideo,
fallbackVideo.path,
"Chat18x"
);
// Update current emotion state to the fallback video's emotion
this.currentEmotion = fallbackVideo.emotion;
this.adjustVideoScale();
this.setVideoEnable(true);
}
}
}
private onEmotionInitialized(data: {roleId: number, emotion: VideoEmotion}): void {
// 检查是否是当前角色
if (data.roleId === this.id) {
console.log(`Emotion initialized for current role ${this.id}: ${VideoEmotion[data.emotion]}`);
// 切换到对应的视频
this.switchVideoByEmotion(data.emotion);
} else {
console.log(`Emotion initialized for role ${data.roleId} (not current role ${this.id}), ignoring`);
}
}
onDialogUpdate() {
if (this.layout) {
this.layout.UpdateDialog(DialogManager.getInstance().getDialogs());
@@ -152,24 +357,90 @@ export class ChatPanel extends li_BaseView {
public async OnClickSend() {
const str = this.editBox.string;
if (!str || str == "") return;
console.log("post:" + str);
// 清空输入框
this.editBox.string = "";
DialogManager.getInstance().updateDialog(true, str, true);
// 使用新的sendMessage方法,传入角色ID
const ret = await ChatAIService.Instance.sendMessage(this.id, str);
if (this.manager) {
this.manager.updateDialog(false, ret);
}
console.log("ret:" + ret);
// 通过ChatController发送消息
await this.chatController.sendMessage(str);
//测试
//this.popUpImage.refresh("Image/1/blur_naked_1");
}
// === IChatPanelCallback 接口实现 ===
/**
* 消息发送开始回调
* @param message 用户发送的消息
*/
public onMessageSent(message: string): void {
console.log("Message sent:", message);
// 可以在这里添加发送中的UI状态显示,比如显示loading等
}
/**
* 接收到AI回复回调
* @param response AI的回复内容
*/
public onMessageReceived(response: string): void {
console.log("Message received:", response);
// 可以在这里添加接收到回复的UI效果,比如播放声音等
}
/**
* 对话更新回调
*/
public onDialogUpdated(): void {
// 更新对话显示
this.onDialogUpdate();
}
/**
* 错误处理回调
* @param error 错误信息
*/
public onError(error: Error): void {
console.error("ChatPanel error:", error);
// 可以在这里显示错误提示给用户
}
/**
* 情绪状态更新回调 (实现IChatPanelCallback接口)
* @param emotion 当前情绪状态
*/
public onEmotionUpdated(emotion: VideoEmotion): void {
console.log(`ChatPanel: Emotion updated to ${VideoEmotion[emotion]} (${emotion})`);
// Switch video based on the new emotion
this.switchVideoByEmotion(emotion);
// Additional UI updates can be added here based on emotion
// 例如:改变角色表情、背景色、播放相应的动画等
switch (emotion) {
case VideoEmotion.calm_down:
// 平静状态的UI更新
console.log("UI state: Calm");
break;
case VideoEmotion.arousal:
// 兴奋状态的UI更新
console.log("UI state: Arousal");
break;
case VideoEmotion.desire:
// 渴望状态的UI更新
console.log("UI state: Desire");
break;
case VideoEmotion.passion:
// 激情状态的UI更新
console.log("UI state: Passion");
break;
case VideoEmotion.orgasm:
// 高潮状态的UI更新
console.log("UI state: Orgasm");
break;
}
}
returnBtn() {
this.onClose();
GameRootUI.I.showDefaultView();
@@ -16,7 +16,7 @@ import LanguageUtils from "../../../Main/Common/LanguageUtils";
import Utils from "../../../Main/Common/Utils";
import { InnerMsgCode } from "../../../Main/Config/InnerMsgCode";
import { SimpleToggle } from "../components/SimpleToggle";
import { GirlDetail } from "../../../schema/schema";
import { GirlDetail, purchase } from "../../../schema/schema";
const { ccclass, property } = _decorator;
@@ -151,31 +151,37 @@ export class GirlDetailPanel extends li_BaseView {
}
this.tags.string = desc;
for (let i = 0; i < 5; i++) {
this.starParent.children[i].active = i < data.starCount;
this.starParent.children[i].active = i < 5;
}
this.descAge.string = data.age.toString();
this.descAge.string =
LanguageUtils.getText("girldetailpanel.age") + data.age.toString();
this.desc.string = dataDetail.detailDesc;
ResManager.I.changeBundleVideo(
this.avatarVideo,
dataDetail.vids[0],
dataDetail.commercialVideos[0].path,
"Chat18x"
);
// 也立即尝试调整(以防已经加载完成)
this.adjustVideoScale();
this.vids = dataDetail.vids.slice(0);
this.refreshImgs(dataDetail.pics, true);
this.vids = dataDetail.commercialVideos.slice(0);
const paths = dataDetail.commercialImages.map((a) => a.path);
this.refreshImgs(paths, true);
}
vids: string[];
vids: purchase.CommercialVideo[];
bindImgToggle() {
const dataDetail = ConfigManager.tables.TbGirlsDetail.get(this.id);
this.refreshImgs(dataDetail.pics, true);
const paths = dataDetail.commercialImages.map((a) => a.path);
this.refreshImgs(paths, true);
}
bindVideoToggle() {
this.refreshImgs(this.vids, false);
const paths = this.vids.map((a) => a.path);
this.refreshImgs(paths, false);
}
refreshImgs(paths: string[], isImg: boolean) {
+120 -16
View File
@@ -60,6 +60,16 @@ export enum PriceType {
}
export enum VideoEmotion {
calm_down = 0,
arousal = 1,
desire = 2,
passion = 3,
orgasm = 4,
}
@@ -69,7 +79,8 @@ export class AiCharacter {
constructor(_buf_: ByteBuf) {
this.id = _buf_.readInt()
this.systemInstruction = _buf_.readString()
this.basePrompt = _buf_.readString()
this.additionPrompt = _buf_.readString()
}
/**
@@ -77,13 +88,18 @@ export class AiCharacter {
*/
readonly id: number
/**
* 系统指令
* 个人信息基础prompt(聊天&amp;评分都需要)
*/
readonly systemInstruction: string
readonly basePrompt: string
/**
* 额外信息prompt(聊天&amp;评分都需要)
*/
readonly additionPrompt: string
resolve(tables:Tables) {
}
}
@@ -101,7 +117,6 @@ export class Girl {
this.tagKey = _buf_.readString()
this.priceType = _buf_.readInt()
this.avatarPath = _buf_.readString()
this.starCount = _buf_.readInt()
}
/**
@@ -132,10 +147,6 @@ export class Girl {
* 头像路径
*/
readonly avatarPath: string
/**
* 评价星数
*/
readonly starCount: number
resolve(tables:Tables) {
@@ -145,7 +156,6 @@ export class Girl {
}
}
@@ -158,8 +168,8 @@ export class GirlDetail {
constructor(_buf_: ByteBuf) {
this.id = _buf_.readInt()
this.detailDesc = _buf_.readString()
{ let n = Math.min(_buf_.readSize(), _buf_.size); this.pics = []; for(let i = 0 ; i < n ; i++) { let _e0 ;_e0 = _buf_.readString(); this.pics.push(_e0);}}
{ let n = Math.min(_buf_.readSize(), _buf_.size); this.vids = []; for(let i = 0 ; i < n ; i++) { let _e0 ;_e0 = _buf_.readString(); this.vids.push(_e0);}}
{ let n = Math.min(_buf_.readSize(), _buf_.size); this.commercialImages = []; for(let i = 0 ; i < n ; i++) { let _e0 ;_e0 = new purchase.CommercialImage(_buf_); this.commercialImages.push(_e0);}}
{ let n = Math.min(_buf_.readSize(), _buf_.size); this.commercialVideos = []; for(let i = 0 ; i < n ; i++) { let _e0 ;_e0 = new purchase.CommercialVideo(_buf_); this.commercialVideos.push(_e0);}}
}
/**
@@ -171,19 +181,19 @@ export class GirlDetail {
*/
readonly detailDesc: string
/**
* 图片资源列表
* 类型
*/
readonly pics: string[]
readonly commercialImages: purchase.CommercialImage[]
/**
* 视频资源列表
*/
readonly vids: string[]
readonly commercialVideos: purchase.CommercialVideo[]
resolve(tables:Tables) {
for (let _e of this.commercialImages) { _e?.resolve(tables); }
for (let _e of this.commercialVideos) { _e?.resolve(tables); }
}
}
@@ -195,18 +205,25 @@ export class GlobalConfig {
constructor(_buf_: ByteBuf) {
this.ApiKey = _buf_.readString()
this.emotionApiKey = _buf_.readString()
this.Model = _buf_.readString()
this.Temperature = _buf_.readFloat()
this.MaxTokens = _buf_.readInt()
this.Timeout = _buf_.readInt()
this.FreeChatTimes = _buf_.readInt()
this.GameName = _buf_.readString()
this.EmotionRating = _buf_.readString()
this.girlBasePrompt = _buf_.readString()
}
/**
* api键
*/
readonly ApiKey: string
/**
* 情绪判断机器人api
*/
readonly emotionApiKey: string
/**
* ai模型
*/
@@ -228,6 +245,14 @@ export class GlobalConfig {
* 游戏名
*/
readonly GameName: string
/**
* 情绪评分机器人system_prompt,需拼接角色prompt
*/
readonly EmotionRating: string
/**
* ai机器人基础规则
*/
readonly girlBasePrompt: string
resolve(tables:Tables) {
@@ -237,6 +262,9 @@ export class GlobalConfig {
}
}
@@ -287,6 +315,70 @@ export class Language {
export namespace purchase {
export class CommercialImage {
constructor(_buf_: ByteBuf) {
this.imageType = _buf_.readInt()
this.imagePrice = _buf_.readFloat()
this.path = _buf_.readString()
}
/**
* 1=详情页展示<br/>2=聊天触发
*/
readonly imageType: number
/**
* 价格
*/
readonly imagePrice: number
/**
* 资源路径
*/
readonly path: string
resolve(tables:Tables) {
}
}
}
export namespace purchase {
export class CommercialVideo {
constructor(_buf_: ByteBuf) {
this.path = _buf_.readString()
this.emotion = _buf_.readInt()
this.videoPrice = _buf_.readFloat()
}
/**
* 资源路径
*/
readonly path: string
/**
* 关联情绪
*/
readonly emotion: VideoEmotion
/**
* 价格
*/
readonly videoPrice: number
resolve(tables:Tables) {
}
}
}
export class PurchaseConfig {
@@ -625,6 +717,10 @@ export class TbGlobalConfig {
* api键
*/
get ApiKey(): string { return this._data.ApiKey; }
/**
* 情绪判断机器人api
*/
get emotionApiKey(): string { return this._data.emotionApiKey; }
/**
* ai模型
*/
@@ -646,6 +742,14 @@ export class TbGlobalConfig {
* 游戏名
*/
get GameName(): string { return this._data.GameName; }
/**
* 情绪评分机器人system_prompt,需拼接角色prompt
*/
get EmotionRating(): string { return this._data.EmotionRating; }
/**
* ai机器人基础规则
*/
get girlBasePrompt(): string { return this._data.girlBasePrompt; }
resolve(tables:Tables) {
this._data.resolve(tables)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -163,7 +163,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1094.75,
"y": 1087.5,
"z": 0
},
"_lrot": {
@@ -218,8 +218,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -520,
"y": -28.75,
"x": -510,
"y": -83.42099999999982,
"z": 0
},
"_lrot": {
@@ -259,8 +259,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 36,
"height": 64
"width": 89,
"height": 90
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -404,9 +404,9 @@
},
"_alignFlags": 9,
"_target": null,
"_left": 20,
"_left": 30,
"_right": 0,
"_top": 40,
"_top": 75.92099999999982,
"_bottom": 30,
"_horizontalCenter": 0,
"_verticalCenter": 0,
@@ -419,7 +419,7 @@
"_originalWidth": 0,
"_originalHeight": 64,
"_alignMode": 2,
"_lockFlags": 12,
"_lockFlags": 4,
"_id": ""
},
{
@@ -469,7 +469,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -0.9460000000000051,
"y": -56.11999999999989,
"z": 0
},
"_lrot": {
@@ -607,7 +607,7 @@
"_target": null,
"_left": 0,
"_right": 0,
"_top": 20,
"_top": 82.4239999999999,
"_bottom": 17.180999999999948,
"_horizontalCenter": 0,
"_verticalCenter": 0,
@@ -676,7 +676,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1080,
"height": 150.5
"height": 165
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -817,7 +817,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -69.10000000000002,
"y": -87.5,
"z": 0
},
"_lrot": {
@@ -4942,7 +4942,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1080,
"height": 2201.8
"height": 2165
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5087,7 +5087,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1080,
"height": 2201.8
"height": 2165
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5240,7 +5240,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1080,
"height": 2201.8
"height": 2165
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5269,7 +5269,7 @@
"_target": null,
"_left": 0,
"_right": 0,
"_top": 138.2,
"_top": 175,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
+24 -24
View File
@@ -175,7 +175,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1059,
"y": 1060,
"z": 0
},
"_lrot": {
@@ -231,7 +231,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": -501.7,
"y": -52.75199999999995,
"y": -53.75199999999995,
"z": 0
},
"_lrot": {
@@ -467,7 +467,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 54.803999999999995,
"y": -22.478999999999814,
"z": 0
},
"_lrot": {
@@ -508,7 +508,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 410.224609375,
"height": 112.39200000000001
"height": 93.87
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -549,7 +549,7 @@
"_actualFontSize": 70,
"_fontSize": 70,
"_fontFamily": "NotoSans-Regular",
"_lineHeight": 89.2,
"_lineHeight": 74.5,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
@@ -605,7 +605,7 @@
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_top": 85.54399999999981,
"_bottom": 108.25899999999993,
"_horizontalCenter": 0,
"_verticalCenter": 0,
@@ -926,7 +926,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 207.47199999999998,
"y": -57.613000000000056,
"y": -105.53999999999996,
"z": 0
},
"_lrot": {
@@ -1240,7 +1240,7 @@
"_left": 194.1,
"_right": 282.528,
"_top": 0,
"_bottom": 53.386999999999944,
"_bottom": 4.460000000000036,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
@@ -1306,7 +1306,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 365.847,
"y": -58.52800000000002,
"y": -106.45499999999993,
"z": 0
},
"_lrot": {
@@ -1818,7 +1818,7 @@
"_left": 194.1,
"_right": 124.15300000000002,
"_top": 0,
"_bottom": 52.47199999999998,
"_bottom": 3.5450000000000728,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
@@ -1871,7 +1871,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 29.928999999999974,
"y": -54.12300000000005,
"y": -103.05000000000018,
"z": 0
},
"_lrot": {
@@ -1968,8 +1968,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -195.44499999999994,
"y": -57.613000000000056,
"x": -400.672,
"y": -105.53999999999996,
"z": 0
},
"_lrot": {
@@ -2281,9 +2281,9 @@
"_alignFlags": 36,
"_target": null,
"_left": 194.1,
"_right": 685.4449999999999,
"_right": 890.672,
"_top": 0,
"_bottom": 53.386999999999944,
"_bottom": 4.460000000000036,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
@@ -2330,7 +2330,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1080,
"height": 222
"height": 220
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -2433,8 +2433,6 @@
"__id__": 0
},
"fileId": "43dBOZjJBF/btIjHVb4m2d",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -2474,7 +2472,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -86.60000000000002,
"y": -110,
"z": 0
},
"_lrot": {
@@ -2534,7 +2532,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 902.3710000000001,
"y": 878.971,
"z": 0
},
"_lrot": {
@@ -3811,6 +3809,8 @@
"__id__": 0
},
"fileId": "40sHQFKzNMq51+Dtkb1mcc",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
@@ -5590,7 +5590,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1060,
"height": 1750.8195000000003
"height": 1704.0195
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5735,7 +5735,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1060,
"height": 1750.8195000000003
"height": 1704.0195
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5888,7 +5888,7 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 1080,
"height": 2166.8
"height": 2120
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5962,7 +5962,7 @@
"_target": null,
"_left": 0,
"_right": 0,
"_top": 173.2,
"_top": 220,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -6,4 +6,4 @@ Maman sexy Heiße Mama category_1004Binding捆绑 बंधनBondageFesseln
Sana Reddyè¨å¨œसाना रेडà¥à¤¡à¥€
Sana Reddy
Sana Reddygirl_10004_nameScarlettScarlettसà¥à¤•ारलेटScarlettScarlettgirl_10001_tag Hot|Horny␍ç«è¾£|饥渴"सेकà¥à¤¸à¥€|कामà¥à¤•Chaude|Excitée
Heiß|Geilgirl_10002_tag%Earth-tone linens,handcrafted jewelry土色亚麻制å“|手工ç å®Yभूरे रंग के लिनन|हसà¥à¤¤à¤¨à¤¿à¤°à¥à¤®à¤¿à¤¤ गहनेLinge terreux,Bijoux artisanaux'Erdfarbene Leinen,Handgemachter Schmuckgirl_10003_tagSinger-Songwriter,Dancer␍舞者|歌手2गायक-गीतकार|नरà¥à¤¤à¤•ीChanteuse-compositrice,Danseuse Sängerin-Songwriterin,Tänzeringirl_10004_tag!RadiantOldSoul,BlueprintsAndBeats!RadiantOldSoul,BlueprintsAndBeats!RadiantOldSoul,BlueprintsAndBeats!RadiantOldSoul,BlueprintsAndBeats!RadiantOldSoul,BlueprintsAndBeats
Heiß|Geilgirl_10002_tag%Earth-tone linens,handcrafted jewelry土色亚麻制å“|手工ç å®Yभूरे रंग के लिनन|हसà¥à¤¤à¤¨à¤¿à¤°à¥à¤®à¤¿à¤¤ गहनेLinge terreux,Bijoux artisanaux'Erdfarbene Leinen,Handgemachter Schmuckgirl_10003_tagSinger-Songwriter,Dancer␍舞者|歌手2गायक-गीतकार|नरà¥à¤¤à¤•ीChanteuse-compositrice,Danseuse Sängerin-Songwriterin,Tänzeringirl_10004_tagGoldenDreamer,BlueEyesAndPearlsGoldenDreamer,BlueEyesAndPearlsGoldenDreamer,BlueEyesAndPearlsGoldenDreamer,BlueEyesAndPearlsGoldenDreamer,BlueEyesAndPearls
Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 B

After

Width:  |  Height:  |  Size: 2.9 KiB

+20 -20
View File
@@ -46,10 +46,10 @@
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 36,
"height": 64,
"rawWidth": 36,
"rawHeight": 64,
"width": 89,
"height": 90,
"rawWidth": 89,
"rawHeight": 90,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
@@ -61,17 +61,17 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-18,
-32,
-44.5,
-45,
0,
18,
-32,
44.5,
-45,
0,
-18,
32,
-44.5,
45,
0,
18,
32,
44.5,
45,
0
],
"indexes": [
@@ -84,12 +84,12 @@
],
"uv": [
0,
64,
36,
64,
90,
89,
90,
0,
0,
36,
89,
0
],
"nuv": [
@@ -103,13 +103,13 @@
1
],
"minPos": [
-18,
-32,
-44.5,
-45,
0
],
"maxPos": [
18,
32,
44.5,
45,
0
]
},