- 新增 StorageManager 本地存储管理器,管理用户生命值 - 新用户默认 10 点生命值,存储在 localStorage - 查看提示消耗 1 点生命值 - 通关奖励 1 点生命值 - PageLevel 集成生命值显示和消耗逻辑
746 lines
19 KiB
TypeScript
746 lines
19 KiB
TypeScript
import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource } from 'cc';
|
||
import { BaseView } from 'db://assets/scripts/core/BaseView';
|
||
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
||
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
|
||
const { ccclass, property } = _decorator;
|
||
|
||
/**
|
||
* 关卡配置类
|
||
*/
|
||
@ccclass('LevelConfig')
|
||
export class LevelConfig {
|
||
@property(SpriteFrame)
|
||
mainImage: SpriteFrame | null = null;
|
||
|
||
@property({ tooltip: '线索1内容(默认解锁)' })
|
||
clue1: string = '';
|
||
|
||
@property({ tooltip: '线索2内容' })
|
||
clue2: string = '';
|
||
|
||
@property({ tooltip: '线索3内容' })
|
||
clue3: string = '';
|
||
|
||
@property({ tooltip: '答案(用于确定输入框数量和验证)' })
|
||
answer: string = '';
|
||
}
|
||
|
||
/**
|
||
* 关卡页面组件
|
||
* 继承 BaseView,实现页面生命周期
|
||
*/
|
||
@ccclass('PageLevel')
|
||
export class PageLevel extends BaseView {
|
||
// ========== 节点引用 ==========
|
||
@property(Node)
|
||
inputLayout: Node | null = null;
|
||
|
||
@property(Node)
|
||
submitButton: Node | null = null;
|
||
|
||
@property(Node)
|
||
inputTemplate: Node | null = null;
|
||
|
||
@property(Node)
|
||
actionNode: Node | null = null;
|
||
|
||
@property(Node)
|
||
iconSetting: Node | null = null;
|
||
|
||
@property(Node)
|
||
tipsLayout: Node | null = null;
|
||
|
||
@property(Node)
|
||
mainImage: Node | null = null;
|
||
|
||
@property(Node)
|
||
tipsItem1: Node | null = null;
|
||
|
||
@property(Node)
|
||
tipsItem2: Node | null = null;
|
||
|
||
@property(Node)
|
||
tipsItem3: Node | null = null;
|
||
|
||
@property(Node)
|
||
unLockItem2: Node | null = null;
|
||
|
||
@property(Node)
|
||
unLockItem3: Node | null = null;
|
||
|
||
@property(Label)
|
||
clockLabel: Label | null = null;
|
||
|
||
@property(Label)
|
||
liveLabel: Label | null = null;
|
||
|
||
// ========== 配置属性 ==========
|
||
@property([LevelConfig])
|
||
levelConfigs: LevelConfig[] = [];
|
||
|
||
@property({
|
||
min: 0,
|
||
tooltip: '当前关卡索引'
|
||
})
|
||
currentLevelIndex: number = 0;
|
||
|
||
@property(AudioClip)
|
||
clickAudio: AudioClip | null = null;
|
||
|
||
@property(AudioClip)
|
||
successAudio: AudioClip | null = null;
|
||
|
||
@property(AudioClip)
|
||
failAudio: AudioClip | null = null;
|
||
|
||
// ========== 内部状态 ==========
|
||
/** 当前创建的输入框节点数组 */
|
||
private _inputNodes: Node[] = [];
|
||
|
||
/** 倒计时剩余秒数 */
|
||
private _countdown: number = 60;
|
||
|
||
/** 倒计时是否结束 */
|
||
private _isTimeUp: boolean = false;
|
||
|
||
/**
|
||
* 页面首次加载时调用
|
||
*/
|
||
onViewLoad(): void {
|
||
console.log('[PageLevel] onViewLoad');
|
||
this.updateLiveLabel();
|
||
this.initLevel();
|
||
this.initIconSetting();
|
||
this.initUnlockButtons();
|
||
this.initSubmitButton();
|
||
this.startCountdown();
|
||
}
|
||
|
||
/**
|
||
* 页面每次显示时调用
|
||
*/
|
||
onViewShow(): void {
|
||
console.log('[PageLevel] onViewShow');
|
||
this.updateLiveLabel();
|
||
}
|
||
|
||
/**
|
||
* 页面隐藏时调用
|
||
*/
|
||
onViewHide(): void {
|
||
console.log('[PageLevel] onViewHide');
|
||
}
|
||
|
||
/**
|
||
* 页面销毁时调用
|
||
*/
|
||
onViewDestroy(): void {
|
||
console.log('[PageLevel] onViewDestroy');
|
||
this.clearInputNodes();
|
||
this.stopCountdown();
|
||
}
|
||
|
||
/**
|
||
* 初始化关卡
|
||
*/
|
||
private initLevel(): void {
|
||
const config = this.levelConfigs[this.currentLevelIndex];
|
||
if (!config) {
|
||
console.warn('[PageLevel] 没有找到关卡配置');
|
||
return;
|
||
}
|
||
|
||
// 重置倒计时状态
|
||
this._isTimeUp = false;
|
||
this._countdown = 60;
|
||
|
||
// 设置主图
|
||
this.setMainImage(config.mainImage);
|
||
|
||
// 设置线索1(默认解锁)
|
||
this.setClue(1, config.clue1);
|
||
|
||
// 隐藏线索2、3
|
||
this.hideClue(2);
|
||
this.hideClue(3);
|
||
|
||
// 显示解锁按钮2、3
|
||
this.showUnlockButton(2);
|
||
this.showUnlockButton(3);
|
||
|
||
// 根据答案长度创建输入框
|
||
const inputCount = config.answer.length;
|
||
this.createInputs(inputCount);
|
||
|
||
// 隐藏提交按钮
|
||
if (this.submitButton) {
|
||
this.submitButton.active = false;
|
||
}
|
||
|
||
// 更新倒计时显示
|
||
this.updateClockLabel();
|
||
|
||
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${inputCount}`);
|
||
}
|
||
|
||
/**
|
||
* 动态创建输入框
|
||
*/
|
||
private createInputs(count: number): void {
|
||
if (!this.inputLayout || !this.inputTemplate) {
|
||
console.error('[PageLevel] inputLayout 或 inputTemplate 未设置');
|
||
return;
|
||
}
|
||
|
||
// 清理现有输入框
|
||
this.clearInputNodes();
|
||
|
||
// 隐藏模板节点
|
||
this.inputTemplate.active = false;
|
||
|
||
// 创建指定数量的输入框
|
||
for (let i = 0; i < count; i++) {
|
||
const inputNode = instantiate(this.inputTemplate);
|
||
inputNode.active = true;
|
||
inputNode.name = `input${i}`;
|
||
|
||
// 设置位置(Layout 会自动排列)
|
||
inputNode.setPosition(new Vec3(0, 0, 0));
|
||
|
||
// 获取 EditBox 组件并监听事件
|
||
const editBox = inputNode.getComponent(EditBox);
|
||
if (editBox) {
|
||
// 清空输入内容
|
||
editBox.string = '';
|
||
// 监听文本变化事件
|
||
editBox.node.on(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
|
||
// 监听编辑结束事件
|
||
editBox.node.on(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
|
||
}
|
||
|
||
this.inputLayout.addChild(inputNode);
|
||
this._inputNodes.push(inputNode);
|
||
}
|
||
|
||
console.log(`[PageLevel] 创建了 ${count} 个输入框`);
|
||
}
|
||
|
||
/**
|
||
* 清理所有输入框节点
|
||
*/
|
||
private clearInputNodes(): void {
|
||
for (const node of this._inputNodes) {
|
||
if (node.isValid) {
|
||
node.destroy();
|
||
}
|
||
}
|
||
this._inputNodes = [];
|
||
}
|
||
|
||
/**
|
||
* 检查所有输入框是否都已填写
|
||
*/
|
||
private checkAllInputsFilled(): void {
|
||
let allFilled = true;
|
||
|
||
for (const node of this._inputNodes) {
|
||
const editBox = node.getComponent(EditBox);
|
||
if (!editBox || editBox.string.trim() === '') {
|
||
allFilled = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 根据填写状态显示/隐藏提交按钮
|
||
if (this.submitButton) {
|
||
this.submitButton.active = allFilled;
|
||
}
|
||
|
||
console.log(`[PageLevel] 检查输入状态: ${allFilled ? '全部已填写' : '未全部填写'}`);
|
||
}
|
||
|
||
/**
|
||
* 获取所有输入框的值
|
||
*/
|
||
getInputValues(): string[] {
|
||
const values: string[] = [];
|
||
for (const node of this._inputNodes) {
|
||
const editBox = node.getComponent(EditBox);
|
||
// 只取第一个字符,去除空格
|
||
const str = (editBox?.string ?? '').trim();
|
||
values.push(str.charAt(0));
|
||
}
|
||
return values;
|
||
}
|
||
|
||
/**
|
||
* 获取拼接后的答案字符串
|
||
*/
|
||
getAnswer(): string {
|
||
return this.getInputValues().join('');
|
||
}
|
||
|
||
// ========== EditBox 事件回调 ==========
|
||
|
||
/** 是否正在处理输入(防止递归) */
|
||
private _isHandlingInput: boolean = false;
|
||
|
||
/**
|
||
* 输入框文本变化回调
|
||
*/
|
||
private onInputTextChanged(editBox: EditBox): void {
|
||
// 防止递归调用
|
||
if (this._isHandlingInput) return;
|
||
|
||
// 处理多字符输入,自动分配到后续输入框
|
||
this.handleMultiCharInput(editBox);
|
||
this.checkAllInputsFilled();
|
||
}
|
||
|
||
/**
|
||
* 处理多字符输入,自动分配到后续输入框
|
||
*/
|
||
private handleMultiCharInput(editBox: EditBox): void {
|
||
const text = editBox.string;
|
||
if (text.length <= 1) return;
|
||
|
||
// 找到当前输入框的索引
|
||
const currentIndex = this._inputNodes.findIndex(node => node.getComponent(EditBox) === editBox);
|
||
if (currentIndex === -1) return;
|
||
|
||
// 标记正在处理输入
|
||
this._isHandlingInput = true;
|
||
|
||
// 保留第一个字符在当前输入框
|
||
const firstChar = text[0];
|
||
const remainingChars = text.slice(1);
|
||
|
||
// 设置当前输入框只保留第一个字符
|
||
editBox.string = firstChar;
|
||
|
||
// 将剩余字符分配到后续输入框
|
||
for (let i = 0; i < remainingChars.length; i++) {
|
||
const nextIndex = currentIndex + 1 + i;
|
||
if (nextIndex < this._inputNodes.length) {
|
||
const nextEditBox = this._inputNodes[nextIndex].getComponent(EditBox);
|
||
if (nextEditBox) {
|
||
// 只在目标输入框为空时填入
|
||
if (nextEditBox.string === '') {
|
||
nextEditBox.string = remainingChars[i];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理完成
|
||
this._isHandlingInput = false;
|
||
}
|
||
|
||
/**
|
||
* 输入框编辑结束回调
|
||
*/
|
||
private onInputEditingEnded(_editBox: EditBox): void {
|
||
this.checkAllInputsFilled();
|
||
}
|
||
|
||
// ========== IconSetting 按钮相关 ==========
|
||
|
||
/**
|
||
* 初始化 IconSetting 按钮事件
|
||
*/
|
||
private initIconSetting(): void {
|
||
if (!this.iconSetting) {
|
||
console.warn('[PageLevel] iconSetting 节点未设置');
|
||
return;
|
||
}
|
||
|
||
const button = this.iconSetting.getComponent(Button);
|
||
if (!button) {
|
||
console.warn('[PageLevel] iconSetting 节点缺少 Button 组件');
|
||
return;
|
||
}
|
||
|
||
this.iconSetting.on(Node.EventType.TOUCH_END, this.onIconSettingClick, this);
|
||
console.log('[PageLevel] IconSetting 按钮事件已绑定');
|
||
}
|
||
|
||
/**
|
||
* IconSetting 按钮点击回调
|
||
*/
|
||
private onIconSettingClick(): void {
|
||
console.log('[PageLevel] IconSetting 点击,返回主页');
|
||
this.playClickSound();
|
||
ViewManager.instance.back();
|
||
}
|
||
|
||
// ========== 线索相关方法 ==========
|
||
|
||
/**
|
||
* 获取线索节点
|
||
*/
|
||
private getTipsItem(index: number): Node | null {
|
||
switch (index) {
|
||
case 1: return this.tipsItem1;
|
||
case 2: return this.tipsItem2;
|
||
case 3: return this.tipsItem3;
|
||
default: return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置线索内容
|
||
*/
|
||
private setClue(index: number, content: string): void {
|
||
const tipsItem = this.getTipsItem(index);
|
||
if (!tipsItem) return;
|
||
|
||
// 查找 TipsLabel 节点:Content -> TipsLabel
|
||
const contentNode = tipsItem.getChildByName('Content');
|
||
if (!contentNode) return;
|
||
|
||
const tipsLabelNode = contentNode.getChildByName('TipsLabel');
|
||
if (!tipsLabelNode) return;
|
||
|
||
const label = tipsLabelNode.getComponent(Label);
|
||
if (label) {
|
||
label.string = `提示 ${index}: ${content}`;
|
||
console.log(`[PageLevel] 设置线索${index}: ${content}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示线索
|
||
*/
|
||
private showClue(index: number): void {
|
||
const tipsItem = this.getTipsItem(index);
|
||
if (tipsItem) {
|
||
tipsItem.active = true;
|
||
console.log(`[PageLevel] 显示线索${index}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 隐藏线索
|
||
*/
|
||
private hideClue(index: number): void {
|
||
const tipsItem = this.getTipsItem(index);
|
||
if (tipsItem) {
|
||
tipsItem.active = false;
|
||
console.log(`[PageLevel] 隐藏线索${index}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示解锁按钮
|
||
*/
|
||
private showUnlockButton(index: number): void {
|
||
const unlockItem = index === 2 ? this.unLockItem2 : this.unLockItem3;
|
||
if (unlockItem) {
|
||
unlockItem.active = true;
|
||
console.log(`[PageLevel] 显示解锁按钮${index}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 隐藏解锁按钮
|
||
*/
|
||
private hideUnlockButton(index: number): void {
|
||
const unlockItem = index === 2 ? this.unLockItem2 : this.unLockItem3;
|
||
if (unlockItem) {
|
||
unlockItem.active = false;
|
||
console.log(`[PageLevel] 隐藏解锁按钮${index}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化解锁按钮事件
|
||
*/
|
||
private initUnlockButtons(): void {
|
||
// 解锁按钮2
|
||
if (this.unLockItem2) {
|
||
this.unLockItem2.on(Node.EventType.TOUCH_END, () => this.onUnlockClue(2), this);
|
||
}
|
||
|
||
// 解锁按钮3
|
||
if (this.unLockItem3) {
|
||
this.unLockItem3.on(Node.EventType.TOUCH_END, () => this.onUnlockClue(3), this);
|
||
}
|
||
|
||
console.log('[PageLevel] 解锁按钮事件已绑定');
|
||
}
|
||
|
||
/**
|
||
* 初始化提交按钮事件
|
||
*/
|
||
private initSubmitButton(): void {
|
||
if (!this.submitButton) {
|
||
console.warn('[PageLevel] submitButton 节点未设置');
|
||
return;
|
||
}
|
||
|
||
this.submitButton.on(Node.EventType.TOUCH_END, this.onSubmitAnswer, this);
|
||
console.log('[PageLevel] 提交按钮事件已绑定');
|
||
}
|
||
|
||
/**
|
||
* 点击解锁线索
|
||
*/
|
||
private onUnlockClue(index: number): void {
|
||
// 检查生命值是否足够
|
||
if (!this.hasLives()) {
|
||
console.warn('[PageLevel] 生命值不足,无法解锁线索');
|
||
return;
|
||
}
|
||
|
||
// 消耗一颗生命值
|
||
if (!this.consumeLife()) {
|
||
return;
|
||
}
|
||
|
||
// 播放点击音效
|
||
this.playClickSound();
|
||
|
||
// 隐藏解锁按钮
|
||
this.hideUnlockButton(index);
|
||
|
||
// 显示线索
|
||
this.showClue(index);
|
||
|
||
// 设置线索内容
|
||
const config = this.levelConfigs[this.currentLevelIndex];
|
||
if (config) {
|
||
const clueContent = index === 2 ? config.clue2 : config.clue3;
|
||
this.setClue(index, clueContent);
|
||
}
|
||
|
||
console.log(`[PageLevel] 解锁线索${index}`);
|
||
}
|
||
|
||
// ========== 主图相关方法 ==========
|
||
|
||
/**
|
||
* 设置主图
|
||
*/
|
||
private setMainImage(spriteFrame: SpriteFrame | null): void {
|
||
if (!this.mainImage) return;
|
||
|
||
const sprite = this.mainImage.getComponent(Sprite);
|
||
if (sprite && spriteFrame) {
|
||
sprite.spriteFrame = spriteFrame;
|
||
console.log('[PageLevel] 设置主图');
|
||
}
|
||
}
|
||
|
||
// ========== 音效相关方法 ==========
|
||
|
||
/**
|
||
* 播放点击音效
|
||
*/
|
||
private playClickSound(): void {
|
||
if (this.clickAudio) {
|
||
// 使用 audioSource 组件播放一次性音效
|
||
const audioSource = this.node.getComponent(AudioSource);
|
||
if (audioSource) {
|
||
audioSource.playOneShot(this.clickAudio);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 播放成功音效
|
||
*/
|
||
private playSuccessSound(): void {
|
||
if (this.successAudio) {
|
||
const audioSource = this.node.getComponent(AudioSource);
|
||
if (audioSource) {
|
||
audioSource.playOneShot(this.successAudio);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 播放失败音效
|
||
*/
|
||
private playFailSound(): void {
|
||
if (this.failAudio) {
|
||
const audioSource = this.node.getComponent(AudioSource);
|
||
if (audioSource) {
|
||
audioSource.playOneShot(this.failAudio);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 倒计时相关方法 ==========
|
||
|
||
/**
|
||
* 开始倒计时
|
||
*/
|
||
private startCountdown(): void {
|
||
this._countdown = 60;
|
||
this._isTimeUp = false;
|
||
this.updateClockLabel();
|
||
this.schedule(this.onCountdownTick, 1);
|
||
console.log('[PageLevel] 开始倒计时 60 秒');
|
||
}
|
||
|
||
/**
|
||
* 停止倒计时
|
||
*/
|
||
private stopCountdown(): void {
|
||
this.unschedule(this.onCountdownTick);
|
||
}
|
||
|
||
/**
|
||
* 倒计时每秒回调
|
||
*/
|
||
private onCountdownTick(): void {
|
||
if (this._isTimeUp) return;
|
||
|
||
this._countdown--;
|
||
this.updateClockLabel();
|
||
|
||
if (this._countdown <= 0) {
|
||
this._isTimeUp = true;
|
||
this.stopCountdown();
|
||
this.onTimeUp();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新倒计时显示
|
||
*/
|
||
private updateClockLabel(): void {
|
||
if (this.clockLabel) {
|
||
this.clockLabel.string = `${this._countdown}s`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 倒计时结束
|
||
*/
|
||
private onTimeUp(): void {
|
||
console.log('[PageLevel] 倒计时结束!');
|
||
this.playFailSound();
|
||
// 可以在这里添加游戏结束逻辑
|
||
}
|
||
|
||
// ========== 生命值相关方法 ==========
|
||
|
||
/**
|
||
* 更新生命值显示
|
||
*/
|
||
private updateLiveLabel(): void {
|
||
if (this.liveLabel) {
|
||
const lives = StorageManager.getLives();
|
||
this.liveLabel.string = `x ${lives}`;
|
||
console.log(`[PageLevel] 更新生命值显示: ${lives}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 消耗一颗生命值(用于查看提示)
|
||
* @returns 是否消耗成功
|
||
*/
|
||
private consumeLife(): boolean {
|
||
const success = StorageManager.consumeLife();
|
||
if (success) {
|
||
this.updateLiveLabel();
|
||
console.log('[PageLevel] 消耗一颗生命');
|
||
} else {
|
||
console.warn('[PageLevel] 生命值不足,无法消耗');
|
||
}
|
||
return success;
|
||
}
|
||
|
||
/**
|
||
* 增加一颗生命值(用于通关奖励)
|
||
*/
|
||
private addLife(): void {
|
||
StorageManager.addLife();
|
||
this.updateLiveLabel();
|
||
console.log('[PageLevel] 获得一颗生命');
|
||
}
|
||
|
||
/**
|
||
* 检查是否有足够的生命值
|
||
*/
|
||
private hasLives(): boolean {
|
||
return StorageManager.hasLives();
|
||
}
|
||
|
||
// ========== 答案提交与关卡切换 ==========
|
||
|
||
/**
|
||
* 提交答案
|
||
*/
|
||
onSubmitAnswer(): void {
|
||
const config = this.levelConfigs[this.currentLevelIndex];
|
||
if (!config) return;
|
||
|
||
const userAnswer = this.getAnswer();
|
||
console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${config.answer}`);
|
||
|
||
if (userAnswer === config.answer) {
|
||
// 答案正确
|
||
this.playClickSound();
|
||
this.showSuccess();
|
||
} else {
|
||
// 答案错误
|
||
this.showError();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示成功提示
|
||
*/
|
||
private showSuccess(): void {
|
||
console.log('[PageLevel] 答案正确!');
|
||
|
||
// 停止倒计时
|
||
this.stopCountdown();
|
||
|
||
// 播放成功音效
|
||
this.playSuccessSound();
|
||
|
||
// 通关奖励:增加一颗生命值
|
||
this.addLife();
|
||
|
||
// 延迟后进入下一关
|
||
this.scheduleOnce(() => {
|
||
this.nextLevel();
|
||
}, 1.0);
|
||
}
|
||
|
||
/**
|
||
* 显示错误提示
|
||
*/
|
||
private showError(): void {
|
||
console.log('[PageLevel] 答案错误!');
|
||
|
||
// 播放失败音效
|
||
this.playFailSound();
|
||
}
|
||
|
||
/**
|
||
* 进入下一关
|
||
*/
|
||
private nextLevel(): void {
|
||
this.currentLevelIndex++;
|
||
|
||
if (this.currentLevelIndex >= this.levelConfigs.length) {
|
||
// 所有关卡完成
|
||
console.log('[PageLevel] 恭喜通关!');
|
||
this.stopCountdown();
|
||
ViewManager.instance.back();
|
||
return;
|
||
}
|
||
|
||
// 重置并加载下一关,重新开始倒计时
|
||
this.initLevel();
|
||
this.startCountdown();
|
||
console.log(`[PageLevel] 进入关卡 ${this.currentLevelIndex + 1}`);
|
||
}
|
||
}
|
||
|
||
|