Files
compose/assets/scripts/GameManager.ts

290 lines
8.3 KiB
TypeScript
Raw Permalink Normal View History

1970-01-01 08:23:24 +08:00
// 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 {}
}
}