Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene. Co-authored-by: Cursor <cursoragent@cursor.com>
389 lines
13 KiB
TypeScript
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));
|
|
}
|
|
}
|