Files
2026-10-20 11:24:09 +08:00

175 lines
13 KiB
JavaScript

// Local Cocos fixture using the production LaserView, prefab, artwork and hit rules.
const http = require('http'), fs = require('fs'), path = require('path'), ts = require('typescript');
const root = path.resolve(__dirname, '..');
const creator = 'C:/ProgramData/cocos/editors/Creator/2.4.15/resources';
const port = Number(process.env.LASER_PREVIEW_PORT || 8794);
const files = {
'/atlas.json': 'assets/UI/prop/laser/laser.plist.meta',
'/atlas.png': 'assets/UI/prop/laser/laser.png',
'/door-atlas.json': 'assets/Block/door.plist.meta',
'/door-atlas.png': 'assets/Block/door.png',
'/prefab.json': 'assets/prefab/prop/laser.prefab',
};
function modules() {
return [['LaserView', 'assets/Script/prop/LaserView.ts'], ['LaserRules', 'assets/Script/LaserRules.ts']]
.map(([name, file]) => `window.${name}=(function(){var exports={};${ts.transpileModule(fs.readFileSync(path.join(root, file), 'utf8'), {
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS, experimentalDecorators: true },
}).outputText};return exports;})();`).join('\n');
}
async function boot() {
window.errors = [];
const status = document.getElementById('status');
const fail = error => { const message = String(error.stack || error); errors.push(message); status.textContent = message; };
window.addEventListener('error', event => fail(event.error || event.message));
window.addEventListener('unhandledrejection', event => fail(event.reason));
const json = url => fetch(url).then(response => response.json());
cc.assetManager.init({ importBase: '/imports', nativeBase: '/native' });
cc.assetManager.loadBundle('/internal', error => {
if (error) return fail(error);
cc.game.run({ id: 'GameCanvas', debugMode: 1, frameRate: 60, renderMode: 0, groupList: ['default'], collisionMatrix: [[true]] }, async () => {
try {
const scene = new cc.Scene(), canvas = new cc.Node('Canvas');
scene.addChild(canvas); canvas.setContentSize(1180, 960);
const canvasComponent = canvas.addComponent(cc.Canvas);
canvasComponent.designResolution = cc.size(1180, 960);
canvasComponent.fitWidth = canvasComponent.fitHeight = true;
cc.view.resizeWithBrowserSize(false);
const fit = () => {
const stage = document.getElementById('stage');
stage.style.top = document.getElementById('controls').offsetHeight + 'px';
cc.view.setFrameSize(stage.clientWidth, stage.clientHeight);
cc.view.setDesignResolutionSize(1180, 960, cc.ResolutionPolicy.SHOW_ALL);
};
window.addEventListener('resize', fit); fit(); cc.director.runScene(scene);
const meta = await json('/atlas.json');
const texture = await new Promise((resolve, reject) => cc.assetManager.loadRemote('/atlas.png', (e, value) => e ? reject(e) : resolve(value)));
const atlas = new cc.SpriteAtlas(), frames = {};
atlas._spriteFrames = {};
for (const [name, m] of Object.entries(meta.subMetas)) {
const frame = new cc.SpriteFrame(texture, cc.rect(m.trimX, m.trimY, m.width, m.height), !!m.rotated,
cc.v2(m.offsetX, m.offsetY), cc.size(m.rawWidth, m.rawHeight));
atlas._spriteFrames[name] = frames[m.uuid] = frame;
}
// Wall.initColor uses door/<color>color<length>, adding 3 for vertical doors.
const doorMeta = await json('/door-atlas.json');
const doorTexture = await new Promise((resolve, reject) => cc.assetManager.loadRemote('/door-atlas.png', (e, value) => e ? reject(e) : resolve(value)));
function gateBase(x, y, length, vertical) {
const name = '5color' + (length + (vertical ? 3 : 0)) + '.png';
const m = doorMeta.subMetas[name];
const frame = new cc.SpriteFrame(doorTexture, cc.rect(m.trimX, m.trimY, m.width, m.height), !!m.rotated,
cc.v2(m.offsetX, m.offsetY), cc.size(m.rawWidth, m.rawHeight));
const node = new cc.Node('base-' + name); canvas.addChild(node);
node.setPosition(x + (vertical ? 0 : (length - 1) * 60), y + (vertical ? (length - 1) * 60 : 0));
const sprite = node.addComponent(cc.Sprite); sprite.sizeMode = cc.Sprite.SizeMode.RAW; sprite.spriteFrame = frame;
const bounds = node.getBoundingBox();
// The bottom 13 px belong to the shadow, not the solid door body.
return {
centerY: bounds.y + bounds.height / 2 - y + 6.5,
shadowEdge: vertical ? bounds.xMin + 43 - x : bounds.yMax - 38 - y,
};
}
const data = await json('/prefab.json'), component = data.find(row => row.atlas);
cc.js._setClassId(component.__type__, LaserView.default);
const details = new cc.deserialize.Details(), prefab = cc.deserialize(data, details);
details.uuidList.forEach((uuid, index) => {
if (uuid === meta.uuid || frames[uuid]) details.uuidObjList[index][details.uuidPropList[index]] = uuid === meta.uuid ? atlas : frames[uuid];
});
function view(x, y) {
const node = cc.instantiate(prefab); canvas.addChild(node); node.setPosition(x, y);
return node.getComponent(LaserView.default);
}
function label(text, x, y) {
const node = new cc.Node(text); canvas.addChild(node); node.setPosition(x, y);
const item = node.addComponent(cc.Label); item.string = text; item.fontSize = 20; item.lineHeight = 24;
node.color = cc.color(220, 228, 242); return item;
}
const groups = [
{ direction: 'right', opposite: 'left', start: [-490, 300], end: [-100, 300], color: 7 },
{ direction: 'left', opposite: 'right', start: [-100, 140], end: [-490, 140], color: 10 },
{ direction: 'up', opposite: 'down', start: [130, 100], end: [130, 350], color: 8 },
{ direction: 'down', opposite: 'up', start: [430, 350], end: [430, 100], color: 6 },
];
const blocks = [], gates = [];
for (const group of groups) {
view(...group.start).initHead('emitter', group.color, group.direction);
view(...group.end).initHead('receiver', group.color, group.opposite);
group.beam = view(...group.start); group.beam.initBeam(group.color, group.direction);
const block = new cc.Node('drag-block-' + group.direction); canvas.addChild(block);
block.setContentSize(56, 56); const graphic = block.addComponent(cc.Graphics);
graphic.fillColor = cc.color(74, 85, 109); graphic.strokeColor = cc.color(204, 217, 237); graphic.lineWidth = 3;
graphic.roundRect(-28, -28, 56, 56, 8); graphic.fill(); graphic.stroke();
block.on(cc.Node.EventType.TOUCH_START, () => {});
block.on(cc.Node.EventType.TOUCH_MOVE, event => {
const delta = event.getDelta(); // Cocos has already converted touch deltas to design units.
block.setPosition(block.x + delta.x, block.y + delta.y);
});
blocks.push(block);
const titleX = (group.start[0] + group.end[0]) / 2;
group.label = label(group.direction, titleX, group.direction === 'left' || group.direction === 'right' ? group.start[1] - 65 : 35);
}
for (let length = 1; length <= 3; length++) {
const horizontalY = -90 - (length - 1) * 145;
const horizontalBase = gateBase(-490, horizontalY, length, false);
const horizontal = view(-490, horizontalY); horizontal.initGate(length, 'up', 7, horizontalBase.centerY, horizontalBase.shadowEdge);
label('Horizontal ' + length, -155, -90 - (length - 1) * 145); gates.push(horizontal);
const verticalX = 80 + (length - 1) * 165;
const verticalBase = gateBase(verticalX, -390, length, true);
const vertical = view(verticalX, -390); vertical.initGate(length, 'right', 8, verticalBase.centerY, verticalBase.shadowEdge);
label('Vertical ' + length, 80 + (length - 1) * 165, -450); gates.push(vertical);
}
let preset = false;
window.setConnected = connected => {
preset = connected;
groups.forEach((group, index) => blocks[index].setPosition(
(group.start[0] + group.end[0]) / 2 + (connected && index > 1 ? 90 : 0),
(group.start[1] + group.end[1]) / 2 + (connected && index < 2 ? 90 : 0)));
};
document.getElementById('connected').onclick = () => setConnected(true);
document.getElementById('blocked').onclick = () => setConnected(false);
document.getElementById('toggle').onclick = () => setConnected(!preset);
const update = () => {
const colliders = blocks.map(block => ({ type: 'rect', minX: block.x - 28, maxX: block.x + 28, minY: block.y - 28, maxY: block.y + 28 }));
const states = groups.map(group => {
const start = { x: group.start[0], y: group.start[1] }, end = { x: group.end[0], y: group.end[1] };
const hit = LaserRules.nearestLaserHit(start, end, colliders), connected = !hit;
group.beam.setBeam(hit ? hit.distance : Math.hypot(end.x - start.x, end.y - start.y), connected);
group.label.string = group.direction + ': ' + (connected ? 'connected' : 'blocked'); return connected;
});
gates.forEach((gate, index) => gate.setGateOpen(states[index % 2 === 0 ? 0 : 2]));
status.textContent = groups.map((group, index) => group.direction + '=' + (states[index] ? 'connected' : 'blocked')).join(' | ');
};
setConnected(true); update(); cc.director.on(cc.Director.EVENT_AFTER_UPDATE, update);
window.ready = true;
} catch (error) { fail(error); }
});
});
}
const html = `<!doctype html><meta charset="utf-8"><title>Laser Cocos Preview</title>
<style>html,body{margin:0;height:100%;overflow:hidden;background:#252b3a;color:#e7edf9;font:14px sans-serif}canvas{display:block}#stage{position:absolute;top:100px;bottom:0;left:0;right:0;overflow:hidden}#controls{position:fixed;z-index:2;top:0;left:0;right:0;padding:8px;background:#151b29}button{padding:8px 12px;margin:0 5px 5px 0}#status{display:block}small{display:block;margin-top:5px}</style>
<div id="controls"><button id="connected">Connected / open</button><button id="blocked">Blocked / closed</button><button id="toggle">Toggle</button><span id="status" role="status">Loading Cocos…</span><small>Drag the four gray squares across the beams. Production LaserView + LaserRules; isolated artwork fixture.</small></div>
<div id="stage"><canvas id="GameCanvas"></canvas></div><script src="/engine.js"></script><script src="/modules.js"></script><script src="/boot.js"></script>`;
http.createServer((request, response) => {
const url = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
const send = (type, body) => { response.setHeader('Content-Type', type); response.end(body); };
if (url === '/') return send('text/html; charset=utf-8', html);
if (url === '/modules.js') return send('text/javascript', modules());
if (url === '/boot.js') return send('text/javascript', '(' + boot.toString() + ')();');
let target;
if (files[url]) target = path.join(root, files[url]);
else if (url === '/engine.js') target = path.join(creator, 'engine/bin/cocos2d-js-for-preview.js');
else if (/^\/imports\/[a-f0-9]{2}\/[a-f0-9-]+\.json$/.test(url)) target = path.join(root, 'library', url.slice(1));
else if (/^\/native\/[a-f0-9]{2}\/[a-f0-9-]+\.png$/.test(url)) target = path.join(root, 'library/imports', url.slice('/native/'.length));
else if (url.startsWith('/internal/') || url.startsWith('/assets/internal/')) {
const internalRoot = path.join(creator, 'cocos2d-x/simulator/win32/assets');
const candidate = path.resolve(internalRoot, url.replace(/^\/(assets\/)?/, ''));
if (candidate.startsWith(internalRoot + path.sep)) target = candidate;
}
if (target && fs.existsSync(target)) {
response.setHeader('Content-Type', target.endsWith('.js') ? 'text/javascript' : target.endsWith('.png') ? 'image/png' : 'application/json');
return fs.createReadStream(target).pipe(response);
}
response.statusCode = 404; response.end('not found');
}).listen(port, '127.0.0.1', () => console.log('http://127.0.0.1:' + port));