Files
mp-xieyingeng/assets/prefabs/PageLevel.ts
richarjiang 71a38c1fe5 feat: 添加生命值系统
- 新增 StorageManager 本地存储管理器,管理用户生命值
- 新用户默认 10 点生命值,存储在 localStorage
- 查看提示消耗 1 点生命值
- 通关奖励 1 点生命值
- PageLevel 集成生命值显示和消耗逻辑
2026-03-14 18:32:50 +08:00

746 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`);
}
}