Files
compose/assets/scripts/GameManager.ts
1970-01-01 08:23:24 +08:00

290 lines
8.3 KiB
TypeScript
Raw Permalink 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.
// assets/scripts/GameManager.ts
import {
_decorator,
Component,
Node,
Prefab,
instantiate,
NodePool,
UITransform,
Vec3,
v3,
input,
Input,
EventTouch,
SpriteFrame,
Sprite,
Label,
RigidBody2D,
Vec2,
tween,
AudioSource,
sys,
math,
} from 'cc';
import { FRUITS, clampLevel, randomStartLevel } from './FruitConfig';
import { Fruit } from './Fruit';
const { ccclass, property } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
@property(Prefab)
fruitPrefab!: Prefab; // 统一水果预制
@property([SpriteFrame])
fruitSprites: SpriteFrame[] = []; // 长度需 >= FRUITS.length按等级顺序
@property(Node)
fruitRoot!: Node;
@property(Node)
preview!: Node; // 顶部预览点,挂 Sprite
@property(Node)
topSensor!: Node; // 仅用于场景绑定与可视化
@property(Node)
walls!: Node; // Left/Right/Floor用于取边界宽度可留空
@property(Label)
scoreLabel!: Label;
@property(Label)
bestLabel!: Label;
@property(Node)
dropLine!: Node; // 危险红线(可选)
@property(AudioSource)
sfx!: AudioSource; // 可选
// —— 可调参数 ——
@property
previewY = 560; // 预览/下落的 Y根据分辨率调整
@property
spawnCooldown = 0.2; // 连续投掷冷却
@property
previewClampPadding = 40; // 贴边留白
@property
randomStartMaxLevel = 5; // 随机初级水果最大等级
// —— 运行时状态 ——
nextLevel = 1; // 将要投的等级
queuedLevel = 1; // 下一颗的预告
canDrop = true;
isGameOver = false;
dangerOn = false;
score = 0;
best = 0;
private pool = new NodePool();
onLoad() {
// 把 SpriteFrame 注入配置(可选)
for (let i = 0; i < FRUITS.length; i++) {
FRUITS[i].sprite = this.fruitSprites[i] || FRUITS[i].sprite;
}
this.best = Number(sys.localStorage.getItem('best') || 0);
if (this.bestLabel) this.bestLabel.string = `${this.best}`;
if (this.scoreLabel) this.scoreLabel.string = `0`;
}
start() {
this.bindInput();
this.reset();
}
protected onDestroy() {
this.unbindInput();
}
bindInput() {
input.on(Input.EventType.TOUCH_MOVE, this.onMove, this);
input.on(Input.EventType.TOUCH_START, this.onMove, this); // 触摸开始也更新预览位置
input.on(Input.EventType.TOUCH_END, this.onDrop, this);
}
unbindInput() {
input.off(Input.EventType.TOUCH_MOVE, this.onMove, this);
input.off(Input.EventType.TOUCH_START, this.onMove, this);
input.off(Input.EventType.TOUCH_END, this.onDrop, this);
}
reset() {
this.isGameOver = false;
this.canDrop = true;
this.updateScore(0);
// 清场(放回对象池而非直接删)
const children = [...this.fruitRoot.children];
for (const c of children) this.recycleFruit(c);
// 初始化两个预报等级
this.nextLevel = randomStartLevel(1, this.randomStartMaxLevel);
this.queuedLevel = randomStartLevel(1, this.randomStartMaxLevel);
this.refreshPreview();
this.setDanger(false);
if (this.dropLine) this.dropLine.active = false;
}
refreshPreview() {
if (!this.preview) return;
const sp = this.preview.getComponent(Sprite)!;
const def = FRUITS[this.nextLevel - 1];
sp.spriteFrame = def.sprite!;
const ui = this.preview.getComponent(UITransform)!;
ui.setContentSize(def.radius * 2, def.radius * 2);
// 初始位置居中(仅设置 XY 固定在预览线)
const p = this.preview.position;
this.preview.active = true;
this.preview.setPosition(v3(0, this.previewY, p.z));
}
onMove(e: EventTouch) {
if (this.isGameOver) return;
const loc = e.getUILocation();
// 将屏幕坐标转本地 Canvas 坐标(父节点是 Canvas
const ui = this.node.getComponent(UITransform)!;
const local = ui.convertToNodeSpaceAR(new Vec3(loc.x, loc.y, 0));
// 约束 X
const halfW = ui.contentSize.width / 2;
const padding = this.previewClampPadding;
const x = math.clamp(local.x, -halfW + padding, halfW - padding);
const p = this.preview.position;
this.preview.setPosition(v3(x, this.previewY, p.z));
}
onDrop() {
if (this.isGameOver || !this.canDrop) return;
this.canDrop = false;
const dropPos = this.preview.worldPosition;
this.spawnFruit(this.nextLevel, dropPos);
// 队列前移 & 随机新队列
this.nextLevel = this.queuedLevel;
this.queuedLevel = randomStartLevel(1, this.randomStartMaxLevel);
this.refreshPreview();
// 冷却
this.scheduleOnce(() => (this.canDrop = true), this.spawnCooldown);
}
spawnFruit(level: number, worldPos: Vec3, inheritVel?: Vec2) {
const node = this.pool.size() > 0 ? this.pool.get()! : instantiate(this.fruitPrefab);
node.parent = this.fruitRoot;
node.active = true;
// 转换到 FruitRoot 的本地坐标
const local = this.fruitRoot.getComponent(UITransform)!.convertToNodeSpaceAR(worldPos.clone());
node.setPosition(local);
node.setScale(1, 1, 1);
node.setRotationFromEuler(0, 0, 0);
const fruit = node.getComponent(Fruit)!;
const index = Math.max(0, Math.min(level - 1, FRUITS.length - 1));
const sf = FRUITS[index].sprite!;
fruit.init(level, sf, this);
// 让它有一点随机水平初速度,手感更“弹”
const rb = node.getComponent(RigidBody2D)!;
const vx = Math.random() * 200 - 100;
rb.linearVelocity = inheritVel || new Vec2(vx, 0);
// 轻微缩放弹出效果
node.setScale(0.9, 0.9, 1);
tween(node).to(0.1, { scale: v3(1, 1, 1) }).start();
// 可选音效:下落
// this.playSfx(0.1);
return fruit;
}
// 被 Fruit 调用:把两颗同级水果合成到 nextLevel
spawnMerged(nextLevel: number, worldMid: Vec3, a: Fruit, b: Fruit) {
if (this.isGameOver) return;
// 读两者速度,合成后给一个小的向上速度,避免“坍塌”
const va = a.getComponent(RigidBody2D)!.linearVelocity;
const vb = b.getComponent(RigidBody2D)!.linearVelocity;
const inherit = new Vec2((va.x + vb.x) * 0.25, Math.max(120, (va.y + vb.y) * 0.25));
// 回收/删除旧的
this.recycleFruit(a.node);
this.recycleFruit(b.node);
// 生成新的
const f = this.spawnFruit(clampLevel(nextLevel), worldMid, inherit);
// 计分:按目标等级给分
this.updateScore(this.score + FRUITS[clampLevel(nextLevel) - 1].score);
// 合成特效:放大闪烁
const n = f.node;
const s0 = n.scale.clone();
tween(n)
.to(0.06, { scale: v3(s0.x * 1.15, s0.y * 1.15, 1) })
.to(0.08, { scale: v3(1, 1, 1) })
.start();
// 音效:合成
this.playSfx(0.2);
}
recycleFruit(node: Node) {
const fr = node.getComponent(Fruit);
if (fr) fr.removed = true;
node.removeFromParent();
this.pool.put(node);
}
updateScore(v: number) {
this.score = v;
if (this.scoreLabel) this.scoreLabel.string = `${this.score}`;
if (this.score > this.best) {
this.best = this.score;
sys.localStorage.setItem('best', String(this.best));
}
if (this.bestLabel) this.bestLabel.string = `${this.best}`;
}
setDanger(on: boolean) {
this.dangerOn = on;
if (this.dropLine) this.dropLine.active = on;
}
onGameOver() {
if (this.isGameOver) return;
this.isGameOver = true;
this.canDrop = false;
this.setDanger(true);
// 所有水果轻微变暗/缩放(可选)
this.fruitRoot.children.forEach((n) =>
tween(n).to(0.2, { scale: v3(0.95, 0.95, 1) }).start()
);
// 失败音
this.playSfx(0.35);
}
restart() {
this.reset();
this.canDrop = true;
}
playSfx(vol = 0.2) {
if (!this.sfx || !this.sfx.clip) return;
try {
this.sfx.volume = vol;
this.sfx.playOneShot(this.sfx.clip, vol);
} catch {}
}
}