Files
watermelon/assets/script/GameManager.ts
1970-01-01 08:26:29 +08:00

469 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
Component,
Node,
Prefab,
SpriteFrame,
input,
Input,
NodePool,
Script,
Sprite,
UITransform,
v3,
Vec3,
math,
Vec2,
instantiate,
RigidBody2D,
tween,
ERigidBody2DType,
CircleCollider2D,
PhysicsSystem2D,
sys,
Label,
UIOpacity,
director,
ParticleSystem2D,
Color,
EventTouch,
view,
AudioClip,
} from "cc";
import { AudioManager, BGMType, SFXType } from "./AudioManager";
import { Fruit } from "./Fruit";
const { ccclass, property } = _decorator;
import { FRUITS, clampLevel, randomStartLevel } from "./FruitConfig";
@ccclass("GameManager")
export class GameManager extends Component {
@property(Prefab)
fruitPrefab!: Prefab; // 统一水果预制
@property([SpriteFrame])
fruitSprites: SpriteFrame[] = []; // 长度需 >= FRUITS.length按等级顺序
@property(Node)
fruitRoot!: Node;
@property(Node)
dropLine!: Node;
@property(Node)
preview!: Node; // 顶部预览点,挂 Sprite
@property(Node)
nextPreview!: Node;
@property(Node)
topSensor!: Node;
@property(Node)
resultNode: Node = null;
@property(Label)
resultLabel: Label = null;
@property(Label)
scoreLabel!: Label;
@property(Label)
bestLabel!: Label;
@property(Prefab)
explosionPrefab: Prefab = null!;
@property(AudioClip)
mergeSFX: AudioClip = null!;
@property(AudioClip)
failSFX: AudioClip = null!;
@property(AudioClip)
victorySFX: AudioClip = null!;
@property(AudioClip)
playBgm: AudioClip = null;
// —— 可调参数 ——
@property
previewY = 560; // 预览/下落的 Y根据分辨率调整
@property
randomStartMaxLevel = 5; // 随机初级水果最大等级
@property
previewClampPadding = 40; // 贴边留白
@property
spawnCooldown = 1; // 连续投掷冷却
dangerOn = false;
// —— 运行时状态 ——
nextLevel = 1; // 将要投的等级
queuedLevel = 1; // 下一颗的预告
canDrop = true;
isGameOver = false;
score = 0;
best = 0;
private pool = new NodePool();
onLoad() {
AudioManager.instance.registerBGM(BGMType.Play, this.playBgm);
AudioManager.instance.registerSFX(SFXType.Merge, this.mergeSFX);
AudioManager.instance.registerSFX(SFXType.Fail, this.failSFX);
AudioManager.instance.registerSFX(SFXType.Victory, this.victorySFX);
AudioManager.instance.setCurBgm(BGMType.Play);
AudioManager.instance.playBGM(BGMType.Play);
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`;
}
protected onDestroy() {
this.unbindInput();
}
start() {
this.bindInput();
this.reset();
PhysicsSystem2D.instance.gravity = new Vec2(0, -800);
}
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;
// TODO 分数
this.updateScore(0);
// 清场(放回对象池而非直接删)
const children = [...this.fruitRoot.children];
for (const c of children) this.recycleFruit(c);
// 初始化两个预报等级
this.nextLevel = randomStartLevel(1, this.randomStartMaxLevel, this.score);
this.queuedLevel = randomStartLevel(
1,
this.randomStartMaxLevel,
this.score
);
this.refreshPreview();
this.refreshNextPreview();
this.setDanger(false);
}
setDanger(on: boolean) {
this.dangerOn = on;
}
recycleFruit(node: Node) {
const fr = node.getComponent(Fruit);
if (fr) fr.removed = true;
node.removeFromParent();
this.pool.put(node);
}
refreshPreview() {
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));
}
refreshNextPreview() {
const sp = this.nextPreview.getComponent(Sprite);
const def = FRUITS[this.queuedLevel - 1];
sp.spriteFrame = def.sprite!;
const ui = this.nextPreview.getComponent(UITransform)!;
ui.setContentSize(def.radius * 2, def.radius * 2);
// 初始位置居中(仅设置 XY 固定在预览线)
const p = this.nextPreview.position;
this.nextPreview.active = true;
// this.nextPreview.setPosition(v3(100, this.previewY, p.z));
}
onMove(e: EventTouch) {
if (this.isGameOver) return;
const screenPos = e.getUILocation();
const canvasUI = this.node.getComponent(UITransform)!;
const localPos = canvasUI.convertToNodeSpaceAR(
new Vec3(screenPos.x, screenPos.y, 0)
);
// 获取预览水果世界宽度
const fruitWorldWidth =
this.preview.getComponent(UITransform)!.contentSize.width *
this.preview.worldScale.x;
// 获取 Canvas 世界半宽
const canvasWorldHalfWidth = view.getVisibleSize().width / 2;
const clampedX = math.clamp(
localPos.x,
-canvasWorldHalfWidth + fruitWorldWidth / 2,
canvasWorldHalfWidth - fruitWorldWidth / 2
);
this.preview.setPosition(
v3(clampedX, this.previewY, this.preview.position.z)
);
}
onDrop() {
if (this.isGameOver || !this.canDrop) return;
// 禁止再次投掷
this.canDrop = false;
// 当前水果下落位置
const dropPos = this.preview.worldPosition;
// 1⃣ 生成当前水果
this.spawnFruit(this.nextLevel, dropPos);
// 2⃣ 隐藏预览,防止连续操作
this.preview.active = false;
// 3⃣ 延迟 1 秒刷新队列和显示预览
this.scheduleOnce(() => {
// 队列前移
this.nextLevel = this.queuedLevel;
this.queuedLevel = randomStartLevel(
1,
this.randomStartMaxLevel,
this.score
);
// 刷新预览
this.refreshPreview();
this.refreshNextPreview();
this.preview.active = true;
// 恢复投掷
this.canDrop = true;
}, 0.6); // 延迟 1 秒
}
spawnFruit(level: number, worldPos: Vec3, inheritVel?: Vec2) {
// 1⃣ 获取节点:从对象池或者实例化
const node =
this.pool.size() > 0 ? this.pool.get()! : instantiate(this.fruitPrefab);
node.parent = this.fruitRoot;
node.active = true;
// 2⃣ 获取水果半宽(世界显示宽度)
const fruitRadius = FRUITS[level - 1].radius;
const fruitUI = node.getComponent(UITransform)!;
const fruitHalfWidth = (fruitUI.contentSize.width * node.worldScale.x) / 2;
// 3⃣ 获取 Canvas / Root 容器缩放比例
const rootUI = this.fruitRoot.getComponent(UITransform)!;
const canvasScaleX = view.getVisibleSize().width / rootUI.contentSize.width;
const halfRootW = (rootUI.contentSize.width * canvasScaleX) / 2;
// 4⃣ 转换到 FruitRoot 坐标系
const local = rootUI.convertToNodeSpaceAR(worldPos.clone());
// 5⃣ 限制 X 坐标不超出屏幕边界
local.x = math.clamp(
local.x,
-halfRootW + fruitHalfWidth,
halfRootW - fruitHalfWidth
);
// 6⃣ 设置初始位置、缩放、旋转
node.setPosition(local);
node.setScale(1, 1, 1);
node.setRotationFromEuler(0, 0, 0);
// 7⃣ 初始化组件
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);
// 8⃣ 物理恢复
const rb = node.getComponent(RigidBody2D)!;
rb.enabled = true;
rb.linearVelocity = inheritVel || new Vec2(0, -10);
rb.angularVelocity = 0;
const col = node.getComponent(CircleCollider2D)!;
if (col) col.enabled = true;
// 9⃣ 出场缩放动画
node.setScale(0.9, 0.9, 1);
tween(node)
.to(0.1, { scale: v3(1, 1, 1) })
.start();
return fruit;
}
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}`;
}
// 被 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)
// );
const inherit = new Vec2(
(va.x + vb.x) * 0.25,
Math.max(3, (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 explosion = instantiate(this.explosionPrefab);
const canvas = director.getScene().getChildByName("Canvas")!;
canvas.addChild(explosion);
explosion.setWorldPosition(worldMid);
explosion.setSiblingIndex(canvas.children.length - 1);
const ps = explosion.getComponent(ParticleSystem2D);
if (ps) {
ps.resetSystem();
const brightColors = [
new Color(255, 255, 0), // 黄色
new Color(255, 128, 0), // 橙色
new Color(255, 0, 0), // 红色
new Color(0, 255, 255), // 青色
new Color(128, 0, 255), // 紫色
];
// 随机给粒子选择颜色
ps.startColor =
brightColors[Math.floor(Math.random() * brightColors.length)];
// 可选:结束颜色渐变为透明
ps.endColor = new Color(
ps.startColor.r,
ps.startColor.g,
ps.startColor.b,
0
);
}
this.scheduleOnce(() => {
explosion.destroy();
}, 1.5); // 粒子持续时间
const circle = new Node();
const sp = circle.addComponent(Sprite);
sp.spriteFrame = this.fruitSprites[nextLevel - 1]; // 你可以换一张发光圈贴图
circle.parent = this.fruitRoot;
circle.setPosition(worldMid);
circle.setScale(0, 0, 1);
// 添加透明度组件
const uiOpacity = circle.addComponent(UIOpacity);
uiOpacity.opacity = 255;
// 并行动画:缩放 + 渐隐
tween(circle)
.to(0.3, { scale: v3(2, 2, 1) })
.call(() => circle.destroy())
.start();
tween(uiOpacity).to(0.3, { opacity: 0 }).start();
// 合成特效:放大闪烁
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();
AudioManager.instance.playSFX(SFXType.Merge);
// const nextFruit = FRUITS[nextLevel+1].name
// if(nextFruit === '西瓜') this.gameOver(true)
// 音效:合成
// this.playSfx(0.2);
}
gameOver(success = false) {
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()
// );
// AudioManager.instance.playSFX(SFXType.Fail);
this.resultShow(success);
// 失败音
// this.playSfx(0.35);
}
resultShow(success: boolean) {
this.resultNode.active = true;
if (!success) {
AudioManager.instance.playSFX(SFXType.Fail);
this.resultLabel.string = "哎萌巴巴";
} else {
AudioManager.instance.playSFX(SFXType.Victory);
this.resultLabel.string = "恭喜哎萌";
}
}
startNewGame() {
director.loadScene("game");
}
}