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>
This commit is contained in:
388
assets/scripts/manager/GameManager.ts
Normal file
388
assets/scripts/manager/GameManager.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
9
assets/scripts/manager/GameManager.ts.meta
Normal file
9
assets/scripts/manager/GameManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "bfc2e3c7-1217-4813-b019-2c0014cb1579",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user