Files
cocos/assets/scripts/manager/GameManager.ts
刘宇飞 cba5105908 Initial Cocos Creator port of main-site Unity WebGL game.
Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 14:57:46 +08:00

389 lines
13 KiB
TypeScript

import { _decorator, Component, Node, Vec3, Color, Graphics, UITransform, director } from 'cc';
import { CELL_PIXEL } from '../AppBootstrap';
import {
CELL_SIZE, CommonDefine, Direction, GameState, GridType, Skin, addDirection,
} from '../core/Define';
import { EventManager, EventType } from '../core/EventManager';
import { JsBridge } from '../bridge/JsBridge';
import { getLevelConfig, hasLevel, registerLevel } from '../level/LevelRegistry';
import { LevelConfig, SpawnConfig } from '../level/LevelTypes';
import { PlayerController } from '../controller/PlayerController';
import { VehicleController } from '../controller/VehicleController';
import { PropController } from '../controller/PropController';
import { VisualAssets } from '../visual/VisualAssets';
import { SpawnKind } from '../level/LevelTypes';
const { ccclass, property } = _decorator;
interface GridEntry {
type: GridType;
node: Node;
}
@ccclass('GameManager')
export class GameManager extends Component {
static instance: GameManager | null = null;
@property(Node)
mainLevelEntrance: Node | null = null;
@property
initialLevelID = 1;
playerSkin: Skin = Skin.Silu;
multMode = false;
multPlayerRole = '';
gameState: GameState = GameState.Run;
isInputEnd = false;
uiStyle = 'default';
curLevelID = 1;
stepNum = 0;
stepA1Num = 0;
stepA2Num = 0;
stepA3Num = 0;
stepB1Num = 0;
stepB2Num = 0;
stepB3Num = 0;
private creating = false;
private curLevel: Node | null = null;
private curConfig: LevelConfig | null = null;
private gridTypes = new Map<string, GridEntry>();
private gridTypesForProps = new Map<string, GridEntry>();
private groundCells = new Map<string, string>();
private borderCells = new Set<string>();
onLoad() {
if (GameManager.instance && GameManager.instance !== this) {
this.destroy();
return;
}
GameManager.instance = this;
}
start() {
/* 关卡由 AppBootstrap 在就绪后加载 */
}
onDestroy() {
if (GameManager.instance === this) GameManager.instance = null;
}
// --- 状态 ---
setGameState(s: GameState) { this.gameState = s; }
initData() {
this.setGameState(GameState.Run);
this.stepNum = 0;
this.stepA1Num = this.stepA2Num = this.stepA3Num = 0;
this.stepB1Num = this.stepB2Num = this.stepB3Num = 0;
this.callSetIsInputEnd(0);
}
jsCallCheck(n: number): boolean {
if (this.isInputEnd || this.gameState !== GameState.Run) return false;
this.stepNum += Math.abs(n);
return true;
}
jsCallCheckMultMode(n: number, name: string): boolean {
if (this.isInputEnd || this.gameState !== GameState.Run) return false;
const a = Math.abs(n);
switch (name) {
case 'PlayerA1':
case 'VehicleA1': this.stepA1Num += a; break;
case 'PlayerA2':
case 'VehicleA2': this.stepA2Num += a; break;
case 'PlayerA3':
case 'VehicleA3': this.stepA3Num += a; break;
case 'PlayerB1':
case 'VehicleB1': this.stepB1Num += a; break;
case 'PlayerB2':
case 'VehicleB2': this.stepB2Num += a; break;
case 'PlayerB3':
case 'VehicleB3': this.stepB3Num += a; break;
default: break;
}
return true;
}
callSetIsInputEnd(v: number) {
this.isInputEnd = v !== 0;
if (this.isInputEnd) EventManager.dispatch(EventType.InputEnd, this.gameState);
}
getRelativePosition(given: Direction, input: Direction): string {
const rel = (((input - given) % 4) + 4) % 4;
return ['front', 'right', 'back', 'left'][rel];
}
// --- 坐标 ---
cellKey(x: number, y: number) { return `${x},${y}`; }
cellToWorld(cell: Vec3): Vec3 {
return new Vec3(cell.x * CELL_PIXEL, cell.y * CELL_PIXEL, 0);
}
worldToCell(world: Vec3): Vec3 {
return new Vec3(Math.round(world.x / CELL_PIXEL), Math.round(world.y / CELL_PIXEL), 0);
}
nextGridPosition(pos: Vec3, dir: Direction): Vec3 {
const c = this.worldToCell(pos);
switch (dir) {
case Direction.North: c.x += 1; break;
case Direction.South: c.x -= 1; break;
case Direction.East: c.y -= 1; break;
case Direction.West: c.y += 1; break;
}
return this.cellToWorld(c);
}
calculateGridType(pos: Vec3): GridType {
const cell = this.worldToCell(pos);
const key = this.cellKey(cell.x, cell.y);
const dyn = this.gridTypes.get(key);
if (dyn) return dyn.type;
if (this.borderCells.has(key)) return GridType.Block;
const g = this.groundCells.get(key);
if (g === CommonDefine.BlockBase) return GridType.Across;
if (g === CommonDefine.BlockJump) return GridType.Jump;
const b = this.curConfig?.boundary;
if (b && (Math.abs(cell.x) >= b.x || Math.abs(cell.y) >= b.y)) return GridType.Boundary;
return GridType.None;
}
calculateNextGridType(pos: Vec3, dir: Direction): GridType {
return this.calculateGridType(this.nextGridPosition(pos, dir));
}
calculateLastGridType(pos: Vec3, dir: Direction): GridType {
return this.calculateGridType(this.nextGridPosition(pos, addDirection(dir, 2)));
}
getGameObject(pos: Vec3): Node | null {
const key = this.cellKey(this.worldToCell(pos).x, this.worldToCell(pos).y);
return this.gridTypes.get(key)?.node ?? null;
}
addObj(pos: Vec3, type: GridType, node: Node) {
const c = this.worldToCell(pos);
this.gridTypes.set(this.cellKey(c.x, c.y), { type, node });
}
removeObj(pos: Vec3) {
const c = this.worldToCell(pos);
this.gridTypes.delete(this.cellKey(c.x, c.y));
}
removeProp(pos: Vec3) {
const c = this.worldToCell(pos);
this.gridTypesForProps.delete(this.cellKey(c.x, c.y));
}
countProp(): number {
if (!this.curLevel) return 0;
return this.curLevel.children.filter((c) => c.isValid && c.getComponent(PropController)).length;
}
getCurLevel() { return this.curConfig; }
findNodeByName(name: string): Node | null {
const scene = director.getScene();
if (!scene) return null;
return this.findInTree(scene, name);
}
private findInTree(root: Node, name: string): Node | null {
if (root.name === name) return root;
for (const ch of root.children) {
const f = this.findInTree(ch, name);
if (f) return f;
}
return null;
}
// --- 关卡 ---
destroyCurLevel() {
if (this.curLevel?.isValid) this.curLevel.destroy();
this.curLevel = null;
this.gridTypes.clear();
this.gridTypesForProps.clear();
this.groundCells.clear();
this.borderCells.clear();
}
switchLevel(levelID: number) {
if (!hasLevel(levelID) || this.creating) return;
this.multMode = levelID >= 999000;
this.destroyCurLevel();
this.createNewLevel(levelID);
}
async createNewLevel(levelID: number) {
if (this.creating) return;
this.creating = true;
const config = getLevelConfig(levelID);
if (!config || !this.mainLevelEntrance) {
this.creating = false;
return;
}
this.curLevelID = levelID;
this.curConfig = config;
const levelRoot = new Node(`Level_${levelID}`);
levelRoot.parent = this.mainLevelEntrance;
this.curLevel = levelRoot;
if (config.ground) {
for (const [k, v] of Object.entries(config.ground)) this.groundCells.set(k, v);
}
if (config.border) {
for (const k of Object.keys(config.border)) this.borderCells.add(k);
}
this.drawGridDebug(levelRoot, config);
const spawned: Node[] = [];
for (const s of config.spawns) {
const node = this.spawnEntity(levelRoot, s);
if (node) spawned.push(node);
}
this.initGridTypes();
this.initData();
EventManager.dispatch(EventType.LevelInit);
this.externalCallLevelInfo(spawned);
this.creating = false;
}
resetLevel() {
this.destroyCurLevel();
this.createNewLevel(this.curLevelID);
}
startMultPlay(role: string, coinsJson: string) {
this.multMode = true;
this.multPlayerRole = role;
let coins: string[] = [];
try {
coins = JSON.parse(coinsJson) as string[];
} catch (e) {
console.error('StartMultPlay coins parse', e);
}
const base = getLevelConfig(999001);
if (!base) return;
const extra = coins.map((s) => {
const [xs, ys] = s.split(',');
return { x: parseInt(xs, 10), y: parseInt(ys, 10), kind: 'prop' as const };
});
registerLevel({ ...base, spawns: [...base.spawns, ...extra] });
this.switchLevel(999001);
}
changeUIStyle(style: string) {
this.uiStyle = style;
}
callMute() { /* 可由 UIMain 实现 */ }
callUnmute() { /* 可由 UIMain 实现 */ }
private initGridTypes() {
this.gridTypes.clear();
this.gridTypesForProps.clear();
if (!this.curLevel) return;
const vehicles = this.curLevel.children.filter((c) => c.name.includes('Vehicle'));
for (const v of vehicles) {
const c = this.worldToCell(v.worldPosition);
this.gridTypes.set(this.cellKey(c.x, c.y), { type: GridType.Ride, node: v });
}
const props = this.curLevel.children.filter((c) => c.getComponent(PropController));
for (const p of props) {
const c = this.worldToCell(p.worldPosition);
this.gridTypesForProps.set(this.cellKey(c.x, c.y), {
type: this.calculateGridType(p.worldPosition),
node: p,
});
}
}
private spawnEntity(parent: Node, s: SpawnConfig): Node | null {
const pos = this.cellToWorld(new Vec3(s.x, s.y, 0));
let node: Node;
if (s.kind === 'player') {
node = new Node(s.x === -9 ? 'PlayerA1' : s.x === 9 ? 'PlayerB1' : 'Player');
node.addComponent(PlayerController);
const pc = node.getComponent(PlayerController)!;
pc.direction = s.playerDirection ?? Direction.South;
} else if (s.kind === 'vehicle') {
node = new Node(s.x < 0 ? 'VehicleA1' : 'VehicleB1');
node.addComponent(VehicleController);
const vc = node.getComponent(VehicleController)!;
vc.direction = s.vehicleDirection ?? Direction.North;
} else if (s.kind === 'prop') {
node = new Node('Prop');
node.addComponent(PropController);
} else if (s.kind === 'prop_decor') {
node = new Node('PropDecor');
} else {
return null;
}
this.attachVisual(node, s.kind, s.playerDirection, s.vehicleDirection);
node.setPosition(pos);
const ui = node.getComponent(UITransform) || node.addComponent(UITransform);
ui.setContentSize(CELL_PIXEL * 0.9, CELL_PIXEL * 0.9);
node.parent = parent;
return node;
}
private attachVisual(
node: Node,
kind: SpawnKind,
playerDir?: Direction,
vehicleDir?: Direction,
) {
const dir = kind === 'player' ? playerDir : kind === 'vehicle' ? vehicleDir : undefined;
VisualAssets.setupEntityVisual(node, kind, dir);
}
private drawGridDebug(root: Node, config: LevelConfig) {
const tiles = new Node('Ground');
tiles.parent = root;
const bx = config.boundary.x;
const by = config.boundary.y;
const half = CELL_PIXEL * 0.45;
for (let x = -bx + 1; x < bx; x++) {
for (let y = -by + 1; y < by; y++) {
const key = this.cellKey(x, y);
if (this.borderCells.has(key)) continue;
const p = this.cellToWorld(new Vec3(x, y, 0));
const cell = new Node(`cell_${x}_${y}`);
cell.parent = tiles;
cell.setPosition(p);
const cui = cell.addComponent(UITransform);
cui.setContentSize(CELL_PIXEL * 0.9, CELL_PIXEL * 0.9);
VisualAssets.applySprite(cell, 'tile', false, 1, 50);
}
}
}
externalCallLevelInfo(objects: Node[]) {
const info: { LevelID: number; PlayerName: string; VehicleName: string } = {
LevelID: this.curLevelID,
PlayerName: '',
VehicleName: '',
};
for (const obj of objects) {
if (this.multMode) {
if (obj.name === this.multPlayerRole) info.PlayerName = obj.name;
else if (obj.name === this.multPlayerRole.replace('Player', 'Vehicle')) info.VehicleName = obj.name;
} else {
if (obj.name.includes('Player')) info.PlayerName = obj.name;
if (obj.name.includes('Vehicle')) info.VehicleName = obj.name;
}
}
JsBridge.call('externalLevelInfo', JSON.stringify(info));
}
}