// 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, 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 = `Laser Cocos Preview
Loading Cocos…Drag the four gray squares across the beams. Production LaserView + LaserRules; isolated artwork fixture.
`; 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));