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(); private gridTypesForProps = new Map(); private groundCells = new Map(); private borderCells = new Set(); 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)); } }