From 06ef53017dcc835768681846a4ceea1741bf383c Mon Sep 17 00:00:00 2001 From: "COMPUTER\\EDY" <249929363@qq.com> Date: Tue, 20 Oct 2026 11:24:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BF=80=E5=85=89=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/Scene/GameScene.fire | 3 + assets/Script/LaserRules.ts | 125 ++ assets/Script/LaserRules.ts.meta | 10 + assets/Script/LaserSystem.ts | 225 +++ assets/Script/LaserSystem.ts.meta | 10 + assets/Script/Map.ts | 31 + assets/Script/module/Tool/GameTool.ts | 4 +- assets/Script/prop/LaserView.ts | 186 ++ assets/Script/prop/LaserView.ts.meta | 10 + assets/UI/prop/laser.meta | 13 + assets/UI/prop/laser/laser.plist | 1114 ++++++++++++ assets/UI/prop/laser/laser.plist.meta | 1945 +++++++++++++++++++++ assets/UI/prop/laser/laser.png | Bin 0 -> 42618 bytes assets/UI/prop/laser/laser.png.meta | 15 + assets/custom/Json/level2021.json | 477 +++++ assets/custom/Json/level2021.json.meta | 6 + assets/custom/Json/level2022.json | 696 ++++++++ assets/custom/Json/level2022.json.meta | 6 + assets/custom/Json/level2023.json | 637 +++++++ assets/custom/Json/level2023.json.meta | 6 + assets/custom/Json/level2024.json | 482 +++++ assets/custom/Json/level2024.json.meta | 6 + assets/custom/Json/level2025.json | 637 +++++++ assets/custom/Json/level2025.json.meta | 6 + assets/custom/Json/level2026.json | 452 +++++ assets/custom/Json/level2026.json.meta | 6 + assets/custom/Json/level2027.json | 638 +++++++ assets/custom/Json/level2027.json.meta | 6 + assets/custom/Json/level2028.json | 379 ++++ assets/custom/Json/level2028.json.meta | 6 + assets/custom/Json/level2029.json | 509 ++++++ assets/custom/Json/level2029.json.meta | 6 + assets/custom/Json/level2030.json | 525 ++++++ assets/custom/Json/level2030.json.meta | 6 + assets/custom/Json/level2031.json | 626 +++++++ assets/custom/Json/level2031.json.meta | 6 + assets/custom/Json/level2032.json | 809 +++++++++ assets/custom/Json/level2032.json.meta | 6 + assets/custom/Json/level2033.json | 520 ++++++ assets/custom/Json/level2033.json.meta | 6 + assets/custom/Json/level2034.json | 867 +++++++++ assets/custom/Json/level2034.json.meta | 6 + assets/custom/Json/level2035.json | 451 +++++ assets/custom/Json/level2035.json.meta | 6 + assets/custom/Json/level2036.json | 517 ++++++ assets/custom/Json/level2036.json.meta | 6 + assets/custom/Json/level2037.json | 676 +++++++ assets/custom/Json/level2037.json.meta | 6 + assets/custom/Json/level2038.json | 756 ++++++++ assets/custom/Json/level2038.json.meta | 6 + assets/custom/Json/level2039.json | 625 +++++++ assets/custom/Json/level2039.json.meta | 6 + assets/custom/Json/level2040.json | 349 ++++ assets/custom/Json/level2040.json.meta | 6 + assets/custom/Json/level9001.json | 80 + assets/custom/Json/level9001.json.meta | 6 + assets/prefab/prop/laser.prefab | 1147 ++++++++++++ assets/prefab/prop/laser.prefab.meta | 9 + assets/resources/prefab/wall/down.prefab | 74 + assets/resources/prefab/wall/left.prefab | 74 + assets/resources/prefab/wall/right.prefab | 74 + assets/resources/prefab/wall/up.prefab | 74 + tools/preview-laser.cjs | 174 ++ tools/test-laser-level.cjs | 229 +++ tools/test-laser-rules.cjs | 123 ++ tools/test-laser-system.cjs | 460 +++++ 66 files changed, 17961 insertions(+), 2 deletions(-) create mode 100644 assets/Script/LaserRules.ts create mode 100644 assets/Script/LaserRules.ts.meta create mode 100644 assets/Script/LaserSystem.ts create mode 100644 assets/Script/LaserSystem.ts.meta create mode 100644 assets/Script/prop/LaserView.ts create mode 100644 assets/Script/prop/LaserView.ts.meta create mode 100644 assets/UI/prop/laser.meta create mode 100644 assets/UI/prop/laser/laser.plist create mode 100644 assets/UI/prop/laser/laser.plist.meta create mode 100644 assets/UI/prop/laser/laser.png create mode 100644 assets/UI/prop/laser/laser.png.meta create mode 100644 assets/custom/Json/level2021.json create mode 100644 assets/custom/Json/level2021.json.meta create mode 100644 assets/custom/Json/level2022.json create mode 100644 assets/custom/Json/level2022.json.meta create mode 100644 assets/custom/Json/level2023.json create mode 100644 assets/custom/Json/level2023.json.meta create mode 100644 assets/custom/Json/level2024.json create mode 100644 assets/custom/Json/level2024.json.meta create mode 100644 assets/custom/Json/level2025.json create mode 100644 assets/custom/Json/level2025.json.meta create mode 100644 assets/custom/Json/level2026.json create mode 100644 assets/custom/Json/level2026.json.meta create mode 100644 assets/custom/Json/level2027.json create mode 100644 assets/custom/Json/level2027.json.meta create mode 100644 assets/custom/Json/level2028.json create mode 100644 assets/custom/Json/level2028.json.meta create mode 100644 assets/custom/Json/level2029.json create mode 100644 assets/custom/Json/level2029.json.meta create mode 100644 assets/custom/Json/level2030.json create mode 100644 assets/custom/Json/level2030.json.meta create mode 100644 assets/custom/Json/level2031.json create mode 100644 assets/custom/Json/level2031.json.meta create mode 100644 assets/custom/Json/level2032.json create mode 100644 assets/custom/Json/level2032.json.meta create mode 100644 assets/custom/Json/level2033.json create mode 100644 assets/custom/Json/level2033.json.meta create mode 100644 assets/custom/Json/level2034.json create mode 100644 assets/custom/Json/level2034.json.meta create mode 100644 assets/custom/Json/level2035.json create mode 100644 assets/custom/Json/level2035.json.meta create mode 100644 assets/custom/Json/level2036.json create mode 100644 assets/custom/Json/level2036.json.meta create mode 100644 assets/custom/Json/level2037.json create mode 100644 assets/custom/Json/level2037.json.meta create mode 100644 assets/custom/Json/level2038.json create mode 100644 assets/custom/Json/level2038.json.meta create mode 100644 assets/custom/Json/level2039.json create mode 100644 assets/custom/Json/level2039.json.meta create mode 100644 assets/custom/Json/level2040.json create mode 100644 assets/custom/Json/level2040.json.meta create mode 100644 assets/custom/Json/level9001.json create mode 100644 assets/custom/Json/level9001.json.meta create mode 100644 assets/prefab/prop/laser.prefab create mode 100644 assets/prefab/prop/laser.prefab.meta create mode 100644 tools/preview-laser.cjs create mode 100644 tools/test-laser-level.cjs create mode 100644 tools/test-laser-rules.cjs create mode 100644 tools/test-laser-system.cjs diff --git a/assets/Scene/GameScene.fire b/assets/Scene/GameScene.fire index 91f2c64a..7bbccbf6 100644 --- a/assets/Scene/GameScene.fire +++ b/assets/Scene/GameScene.fire @@ -5165,6 +5165,9 @@ }, { "__uuid__": "076766a8-f2d3-4a3e-a24d-d83c7136172f" + }, + { + "__uuid__": "0f2a92d8-c6f6-4fc1-9803-8358549350dd" } ], "MapBlockPrefab": { diff --git a/assets/Script/LaserRules.ts b/assets/Script/LaserRules.ts new file mode 100644 index 00000000..c031b9cd --- /dev/null +++ b/assets/Script/LaserRules.ts @@ -0,0 +1,125 @@ +export type LaserPoint = { x: number; y: number }; + +export type LaserCollider = + | { type: 'rect'; minX: number; minY: number; maxX: number; maxY: number } + | { type: 'circle'; center: LaserPoint; radius: number } + | { type: 'polygon'; points: LaserPoint[] }; + +export type LaserHit = { + ratio: number; + distance: number; + point: LaserPoint; + colliderIndex: number; +}; + +const EPSILON = 1e-7; + +function cross(ax: number, ay: number, bx: number, by: number): number { + return ax * by - ay * bx; +} + +function pointOnEdge(point: LaserPoint, a: LaserPoint, b: LaserPoint): boolean { + const dx = b.x - a.x, dy = b.y - a.y; + const px = point.x - a.x, py = point.y - a.y; + const length = Math.sqrt(dx * dx + dy * dy); + if (length < EPSILON) return px * px + py * py <= EPSILON * EPSILON; + if (Math.abs(cross(dx, dy, px, py)) > EPSILON * length) return false; + const projection = (px * dx + py * dy) / length; + return projection >= -EPSILON && projection <= length + EPSILON; +} + +function pointInPolygon(point: LaserPoint, points: LaserPoint[]): boolean { + let inside = false; + for (let i = 0, j = points.length - 1; i < points.length; j = i++) { + const a = points[j], b = points[i]; + if (pointOnEdge(point, a, b)) return true; + if ((a.y > point.y) !== (b.y > point.y) + && point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x) inside = !inside; + } + return inside; +} + +function polygonHit(start: LaserPoint, end: LaserPoint, points: LaserPoint[]): number | null { + if (points.length < 3) return null; + if (pointInPolygon(start, points)) return 0; + const dx = end.x - start.x, dy = end.y - start.y; + const lengthSquared = dx * dx + dy * dy; + if (lengthSquared < EPSILON * EPSILON) return null; + let nearest = Infinity; + for (let i = 0, j = points.length - 1; i < points.length; j = i++) { + const a = points[j], b = points[i]; + const ex = b.x - a.x, ey = b.y - a.y; + const px = a.x - start.x, py = a.y - start.y; + const determinant = cross(dx, dy, ex, ey); + if (Math.abs(determinant) <= EPSILON) { + // A beam running along a polygon edge still touches the solid. + if (Math.abs(cross(px, py, dx, dy)) > EPSILON) continue; + const t1 = (px * dx + py * dy) / lengthSquared; + const t2 = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lengthSquared; + const entry = Math.max(0, Math.min(t1, t2)); + if (entry <= Math.min(1, Math.max(t1, t2))) nearest = Math.min(nearest, entry); + } else { + const ratio = cross(px, py, ex, ey) / determinant; + const edgeRatio = cross(px, py, dx, dy) / determinant; + if (ratio >= -EPSILON && ratio <= 1 + EPSILON + && edgeRatio >= -EPSILON && edgeRatio <= 1 + EPSILON) { + nearest = Math.min(nearest, Math.max(0, Math.min(1, ratio))); + } + } + } + return nearest === Infinity ? null : nearest; +} + +function colliderHit(start: LaserPoint, end: LaserPoint, collider: LaserCollider): number | null { + const dx = end.x - start.x, dy = end.y - start.y; + if (collider.type === 'circle') { + const x = start.x - collider.center.x, y = start.y - collider.center.y; + const c = x * x + y * y - collider.radius * collider.radius; + if (c <= 0) return 0; + const a = dx * dx + dy * dy; + if (a < EPSILON * EPSILON) return null; + const b = x * dx + y * dy; + const discriminant = b * b - a * c; + if (discriminant < -EPSILON) return null; + const ratio = (-b - Math.sqrt(Math.max(0, discriminant))) / a; + return ratio >= -EPSILON && ratio <= 1 + EPSILON ? Math.max(0, Math.min(1, ratio)) : null; + } + if (collider.type === 'polygon') return polygonHit(start, end, collider.points); + + let entry = 0, exit = 1; + for (const axis of ['x', 'y']) { + const origin = axis === 'x' ? start.x : start.y; + const delta = axis === 'x' ? dx : dy; + const min = axis === 'x' ? collider.minX : collider.minY; + const max = axis === 'x' ? collider.maxX : collider.maxY; + if (Math.abs(delta) < EPSILON) { + if (origin < min - EPSILON || origin > max + EPSILON) return null; + continue; + } + const t1 = (min - origin) / delta, t2 = (max - origin) / delta; + entry = Math.max(entry, Math.min(t1, t2)); + exit = Math.min(exit, Math.max(t1, t2)); + if (entry > exit) return null; + } + return entry; +} + +/** World-space beam centerline only: visual beam thickness never affects blocking. */ +export function nearestLaserHit(start: LaserPoint, end: LaserPoint, colliders: LaserCollider[]): LaserHit | null { + let ratio = Infinity, colliderIndex = -1; + for (let i = 0; i < colliders.length; i++) { + const hit = colliderHit(start, end, colliders[i]); + if (hit !== null && hit < ratio) { + ratio = hit; + colliderIndex = i; + } + } + if (colliderIndex < 0) return null; + const dx = end.x - start.x, dy = end.y - start.y; + return { + ratio, + distance: Math.sqrt(dx * dx + dy * dy) * ratio, + point: { x: start.x + dx * ratio, y: start.y + dy * ratio }, + colliderIndex, + }; +} diff --git a/assets/Script/LaserRules.ts.meta b/assets/Script/LaserRules.ts.meta new file mode 100644 index 00000000..b5c64c22 --- /dev/null +++ b/assets/Script/LaserRules.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "d970238b-6679-4f0d-8339-1de0c7b1f266", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/Script/LaserSystem.ts b/assets/Script/LaserSystem.ts new file mode 100644 index 00000000..85840335 --- /dev/null +++ b/assets/Script/LaserSystem.ts @@ -0,0 +1,225 @@ +import type MapController from "./Map"; +import type Wall from "./Wall"; +import { LQCollide } from "./lq_collide_system/lq_collide"; +import { LQCollideSystem } from "./lq_collide_system/lq_collide_system"; +import { LQCollideShape } from "./lq_base/data/lq_const"; +import { LaserCollider, nearestLaserHit } from "./LaserRules"; +import LaserView from "./prop/LaserView"; + +export interface LaserConfig { + id: string; + /** Artwork index (1..10), independent of the exit's normal color. */ + color: number; + emitter: { num: number }; + receiver: { num: number }; +} + +interface LaserGroup { + config: LaserConfig; + emitter: Wall; + receiver: Wall; + gates: Wall[]; + source?: LaserView; + target?: LaserView; + beam?: LaserView; + gate?: LaserView; + connected?: boolean; +} + +const DIRECTIONS = { + // Retraction excludes the wall texture's transparent edge/shadow and the beam's 1px clearance. + right: { x: 1, y: 0, opposite: "left", headRetraction: 8 }, + left: { x: -1, y: 0, opposite: "right", headRetraction: 6 }, + up: { x: 0, y: 1, opposite: "down", headRetraction: 1 }, + down: { x: 0, y: -1, opposite: "up", headRetraction: 14 }, +}; + +/** Keeps laser reception independent of door/beam animation. */ +export default class LaserSystem { + private groups: LaserGroup[] = []; + private nodes: cc.Node[] = []; + private layer: cc.Node; + + constructor(private map: MapController, slots: Wall[], configs: LaserConfig[], prefab: cc.Prefab) { + this.groups = this.validate(slots, configs); + if (!prefab) throw new Error("Map.Block_Prop[26] 缺少激光预制体"); + try { + this.layer = new cc.Node("LaserLayer"); + this.layer.parent = map.node; + this.layer.zIndex = 10; + for (const group of this.groups) { + group.source = this.attach(prefab, group.emitter.node.parent.getChildByName("laser")); + group.source.initHead("emitter", group.config.color, group.emitter.direction, + this.headInset(group.emitter), DIRECTIONS[group.emitter.direction].headRetraction); + group.target = this.attach(prefab, group.receiver.node.parent.getChildByName("laser")); + group.target.initHead("receiver", group.config.color, group.receiver.direction, + this.headInset(group.receiver), DIRECTIONS[group.receiver.direction].headRetraction); + const first = group.gates.find(wall => wall.wall_Info.length > 0); + group.gate = this.attach(prefab, first.node.parent.getChildByName("laser")); + const gateFace = first.node.getBoundingBox(); + const socket = group.gate.node.parent; + // The base door's bottom 13px are shadow, not part of its solid colored face. + const gateCenterY = (gateFace.yMin + gateFace.yMax) / 2 + 6.5 - socket.y; + // Align the native face/shadow boundary; color 8's boundary is one pixel earlier. + const colorOffset = first.wall_Info.color === 8 ? 1 : 0; + const shadowEdge = first.direction === "left" || first.direction === "right" + ? gateFace.xMin + 43 - colorOffset - socket.x + : gateFace.yMax - 38 + colorOffset - socket.y; + group.gate.initGate(first.wall_Info.length, first.direction, group.config.color, gateCenterY, shadowEdge); + group.beam = this.attach(prefab, this.layer); + // Start the body at the colored tip, but keep its flare clear of the wall shadow. + group.beam.initBeam(group.config.color, group.emitter.direction, DIRECTIONS[group.emitter.direction].headRetraction); + } + this.update(true); + } catch (error) { + this.dispose(); + throw error; + } + } + + private validate(slots: Wall[], configs: LaserConfig[]): LaserGroup[] { + const ids = new Set(); + const heads = new Set(); + const straight = (wall: Wall) => wall && DIRECTIONS[wall.direction] + && this.map.mapBlocksWall[wall.posX][wall.posY].opacity === 250 + && wall.node.parent.getChildByName("laser"); + const groups = configs.map(config => { + const fail = (message: string) => { throw new Error("激光组 " + config.id + ": " + message); }; + if (typeof config.id !== "string" || !config.id || ids.has(config.id)) fail("编号缺失或重复"); + ids.add(config.id); + if (!Number.isInteger(config.color) || config.color < 1 || config.color > 10) fail("激光色必须为 1~10"); + const emitter = slots[config.emitter && config.emitter.num]; + const receiver = slots[config.receiver && config.receiver.num]; + if (!straight(emitter) || !straight(receiver)) fail("发射和接收必须放在完整直墙段"); + for (const endpoint of [config.emitter, config.receiver]) { + if (!Number.isInteger(endpoint.num) || heads.has(endpoint.num) || slots[endpoint.num].wall_Info) { + fail("发射或接收墙位已被占用"); + } + heads.add(endpoint.num); + } + const direction = DIRECTIONS[emitter.direction]; + const dx = receiver.posX - emitter.posX, dy = receiver.posY - emitter.posY; + if (receiver.direction !== direction.opposite || dx * direction.y !== dy * direction.x + || dx * direction.x + dy * direction.y <= 0) fail("发射和接收必须同一直线且相向"); + const gates = slots.filter(wall => wall && wall.wall_Info && wall.wall_Info.laserId === config.id); + const starts = gates.filter(wall => wall.wall_Info.length > 0); + if (starts.length !== 1 || starts[0].wall_Info.length !== gates.length || gates.length > 3) { + fail("必须对应一扇完整的 1~3 格控制门"); + } + const first = starts[0]; + const vertical = first.direction === "left" || first.direction === "right"; + const sorted = gates.slice().sort((a, b) => vertical ? a.posY - b.posY : a.posX - b.posX); + sorted.forEach((wall, index) => { + const info = wall.wall_Info; + if (!straight(wall) || wall.direction !== first.direction || info.color !== first.wall_Info.color + || wall.posX !== first.posX + (vertical ? 0 : index) + || wall.posY !== first.posY + (vertical ? index : 0) + || info.special !== 0 || info.toggle != null || info.longAndShort || info.stickLength + || info.colorArray || info.jump != null || info.lockTime != null) { + fail("控制门必须是连续同色直门,不能叠加其他门机制"); + } + wall.open = false; + }); + return { config, emitter, receiver, gates }; + }); + if (slots.some(wall => wall && wall.wall_Info && wall.wall_Info.laserId != null + && !ids.has(wall.wall_Info.laserId))) throw new Error("激光控制门缺少对应组配置"); + return groups; + } + + private attach(prefab: cc.Prefab, parent: cc.Node): LaserView { + parent.active = true; + const node = cc.instantiate(prefab); + this.nodes.push(node); + node.parent = parent; + node.setPosition(0, 0); + const view = node.getComponent(LaserView); + if (!view) throw new Error("激光预制体缺少 LaserView 组件"); + return view; + } + + private headInset(wall: Wall): number { + const face = wall.node.getBoundingBox(); + const socket = wall.node.parent.getChildByName("laser"); + switch (wall.direction) { + case "right": return Math.max(0, face.xMax - socket.x) + 1; + case "left": return Math.max(0, socket.x - face.xMin) + 1; + case "up": return Math.max(0, face.yMax - socket.y) + 1; + case "down": return Math.max(0, socket.y - face.yMin) + 1; + } + return 0; + } + + update(immediate = false): void { + const colliders = this.collectColliders(); + for (const group of this.groups) { + const start = group.source.getBeamOrigin(), end = group.target.getBeamOrigin(); + const obstacles = colliders.filter(item => item.wall !== group.emitter.node.parent + && item.wall !== group.receiver.node.parent).map(item => item.shape); + const hit = nearestLaserHit(start, end, obstacles); + const connected = !hit; + // All segments change state before starting the visual tween. + group.gates.forEach(wall => wall.open = connected); + if (group.connected !== connected || immediate) { + group.connected = connected; + group.gate.setGateOpen(connected, immediate); + } + const localStart = this.layer.convertToNodeSpaceAR(group.source.getVisibleBeamOrigin()); + const localEnd = this.layer.convertToNodeSpaceAR(hit ? cc.v2(hit.point.x, hit.point.y) : group.target.getVisibleBeamOrigin()); + const direction = DIRECTIONS[group.emitter.direction]; + const visibleLength = (localEnd.x - localStart.x) * direction.x + (localEnd.y - localStart.y) * direction.y; + group.beam.node.setPosition(localStart); + group.beam.setBeam(Math.max(0, visibleLength), connected); + } + } + + private collectColliders(): { shape: LaserCollider; wall: cc.Node }[] { + const result: { shape: LaserCollider; wall: cc.Node }[] = []; + for (const collider of LQCollideSystem.collide_arr) { + const node = collider.node; + if (!cc.isValid(node, true) || !node.activeInHierarchy || !collider.is_enable + || !collider.enabled || collider.data_string === "-1" || collider.data_string === "-2") continue; + if (node.name === "rise" && this.map.riseFallBlock.indexOf(node.parent) >= 0) continue; + let ancestor = node, wall: cc.Node = null, transparent = false; + while (ancestor && ancestor !== this.map.node) { + const block = ancestor.getComponent("Block"); + if (block && (block.type === 22 || block.over)) transparent = true; + if (ancestor.getChildByName("wall") && ancestor.getChildByName("wall").getComponent("Wall")) wall = ancestor; + ancestor = ancestor.parent; + } + if (ancestor !== this.map.node || transparent) continue; + const shape = this.colliderShape(collider); + if (shape) result.push({ shape, wall }); + } + return result; + } + + private colliderShape(collider: LQCollide): LaserCollider { + // LQ scales dimensions/points at initialization. Transform only the center; + // applying the node's world scale again would enlarge the collision shape. + const center = collider.node.convertToWorldSpaceAR(collider.offset); + if (collider.collide_shape === LQCollideShape.Rect) { + return { type: "rect", minX: center.x - collider.size.width / 2, + maxX: center.x + collider.size.width / 2, minY: center.y - collider.size.height / 2, + maxY: center.y + collider.size.height / 2 }; + } + if (collider.collide_shape === LQCollideShape.Circle) { + return { type: "circle", center, radius: collider.radius }; + } + if (collider.collide_shape === LQCollideShape.Polygon) { + const angle = collider.node.angle * Math.PI / 180, cos = Math.cos(angle), sin = Math.sin(angle); + return { type: "polygon", points: collider.polygon_points.map(point => ({ + x: center.x + point.x * cos - point.y * sin, + y: center.y + point.x * sin + point.y * cos, + })) }; + } + return null; + } + + dispose(): void { + this.nodes.forEach(node => { if (cc.isValid(node)) node.destroy(); }); + this.nodes.length = 0; + if (this.layer && cc.isValid(this.layer)) this.layer.destroy(); + this.groups.length = 0; + } +} diff --git a/assets/Script/LaserSystem.ts.meta b/assets/Script/LaserSystem.ts.meta new file mode 100644 index 00000000..e747b646 --- /dev/null +++ b/assets/Script/LaserSystem.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "fcf81a53-2639-4ab4-b00e-caf68a587f45", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/Script/Map.ts b/assets/Script/Map.ts index 0ea9cc0d..735db603 100644 --- a/assets/Script/Map.ts +++ b/assets/Script/Map.ts @@ -1,4 +1,6 @@ import RainbowStreak from "./RainbowStreak"; +import LaserSystem from "./LaserSystem"; +import type Wall from "./Wall"; import type Block from "./Block"; import RainbowOldVisual from "./RainbowOldVisual"; import RainbowOldOpening from "./RainbowOldOpening"; @@ -50,6 +52,9 @@ export default class MapConroler extends cc.Component { @property({ type: [cc.Prefab], tooltip: "方块道具数组" }) Block_Prop: Array = []; + private laserSystem: LaserSystem = null; + private laserWallSlots: Wall[] = []; + @property(cc.Prefab) MapBlockPrefab: cc.Prefab = null; @@ -628,6 +633,8 @@ export default class MapConroler extends cc.Component { } onDestroy() { + if (this.laserSystem) this.laserSystem.dispose(); + this.laserSystem = null; if (this.oldRainbowOpening) this.oldRainbowOpening.dispose(); this.oldRainbowOpening = null; this.oldRainbowEntryReady = null; @@ -1319,6 +1326,9 @@ export default class MapConroler extends cc.Component { this.ismagic = false; this.guideHammerPending = false; this.wallNum = 0; + if (this.laserSystem) this.laserSystem.dispose(); + this.laserSystem = null; + this.laserWallSlots = []; this.setMapInfo(); this.blocks = []; this.blocks2 = []; @@ -1412,6 +1422,17 @@ export default class MapConroler extends cc.Component { if (this.oldRainbowPreparing) return; this.oldRainbowEntryReady = null; if (!this.spawnGatesInitialized) this.initSpawnGates(); + if (!this.laserSystem) { + const lasers = cc.fx.GameConfig.LEVEL_INFO[0].lasers || []; + if (lasers.length || this.laserWallSlots.some(wall => wall.wall_Info && wall.wall_Info.laserId != null)) { + try { + this.laserSystem = new LaserSystem(this, this.laserWallSlots, lasers, this.Block_Prop[26]); + } catch (error) { + cc.fx.GameConfig.showBlockLoadError(error, schedulePopupWarmup, "激光配置或资源加载失败\n点击重试"); + return; + } + } + } popupWarmupScheduled = true; // console.log(`[进关耗时][地图资源确认及生成] 完成,独立耗时=${Date.now() - this.mapPreparationStartedAt}ms,从开始累计=${Date.now() - this.entryStartedAt}ms`); this.resumeEntryBackgroundDownloads(); @@ -2582,6 +2603,7 @@ export default class MapConroler extends cc.Component { // 4. 开关门和冻结门的特殊属性 // ============================================ setDoorInfo(wall) { + this.laserWallSlots[this.wallNum] = wall.getComponent("Wall"); let doorInfo = cc.fx.GameConfig.WALL_INFO[0]; if (doorInfo) { for (let j = 0; j < doorInfo.length; j++) { @@ -3996,10 +4018,14 @@ export default class MapConroler extends cc.Component { // 3. 门的长度设置允许通过 // ============================================ passWall(jg, wallArray, node) { + // Touch release may run before lateUpdate; reception must be current here too. + if (this.laserSystem) this.laserSystem.update(); const block = node.getComponent("Block"); const anyColor = block.oldRainbow || block.color === 100; let colorArray = []; for (let i = 0; i < wallArray.length; i++) { + const laserWall = wallArray[i].getComponent("Wall"); + if (laserWall.wall_Info && laserWall.wall_Info.laserId != null && laserWall.open !== true) return false; if (anyColor) { const wall = wallArray[i].getComponent("Wall"); // 边界数组也包含普通墙;彩虹只能忽略真实门的颜色,不能穿墙或伸缩障碍杆。 @@ -5921,6 +5947,7 @@ export default class MapConroler extends cc.Component { downDoor(color, type, anyColor = false) { for (let i = 0; i < this.wallArray.length; i++) { const wall = this.wallArray[i].getChildByName("wall").getComponent("Wall"); + if (wall.wall_Info && wall.wall_Info.laserId != null && wall.open !== true) continue; if (anyColor || color == 100) { // 彩虹提示只忽略颜色,保持与实际通行相同的门状态限制。 if (!wall.wall_Info || wall.wall_Info.stickLength @@ -8432,6 +8459,10 @@ export default class MapConroler extends cc.Component { // 主要功能: // 1. 检测连击音效超时(3秒无碰撞消除则重置连击计数) // ============================================ + lateUpdate() { + if (this.laserSystem) this.laserSystem.update(); + } + update(dt) { if (!this.blockAssetsReady) return; this.updateOldRainbowOpening(dt); diff --git a/assets/Script/module/Tool/GameTool.ts b/assets/Script/module/Tool/GameTool.ts index 9209505a..ae3e811b 100644 --- a/assets/Script/module/Tool/GameTool.ts +++ b/assets/Script/module/Tool/GameTool.ts @@ -19,7 +19,7 @@ import DailyTaskSync from "../Task/DailyTaskSync"; //@ts-ignore //最大工具类 各种公共方法,以及处理上传,获取后端接口数据 var GameTool = { - MAX_NORMAL_LEVEL: 2020, + MAX_NORMAL_LEVEL: 9001, starterPackReactivationUid: "", starterPackQueuedReason: "", MEMORY_WARNING_EVENT: "game-memory-warning", @@ -2175,7 +2175,7 @@ var GameTool = { if (!scope) return; this.pendingActivityCoins[scope] = (this.pendingActivityCoins[scope] || 0) + amount; this.flushActivityCoins(); - if (this.pendingActivityCoins[scope]) DailyTaskSync.read(() => {}); + if (this.pendingActivityCoins[scope]) DailyTaskSync.read(() => { }); } catch (error) { cc.warn("活动金币任务统计失败", error); } }, flushActivityCoins() { diff --git a/assets/Script/prop/LaserView.ts b/assets/Script/prop/LaserView.ts new file mode 100644 index 00000000..a986041d --- /dev/null +++ b/assets/Script/prop/LaserView.ts @@ -0,0 +1,186 @@ +const { ccclass, property } = cc._decorator; + +export type LaserDirection = "right" | "up" | "left" | "down"; + +/** Laser artwork only. Reception and gate collision state belong to the map. */ +@ccclass +export default class LaserView extends cc.Component { + @property(cc.SpriteAtlas) + atlas: cc.SpriteAtlas = null; + + private colorIndex = 1; + private verticalGate = false; + private gateHalfLengths: number[] = []; + private gateOpen: boolean = null; + private slicedFrames: cc.SpriteFrame[] = []; + private beamFrames: { [name: string]: cc.SpriteFrame } = {}; + private beamStartOffset = 0; + + initBeam(color: number, direction: LaserDirection, startOffset = 0): void { + this.show("beam"); + this.colorIndex = color; + this.beamStartOffset = startOffset; + this.node.getChildByName("beam").angle = this.angle(direction); + this.setFrame("beam/start", "laser_start" + color); + this.setFrame("beam/end", "laser_over" + color); + this.setBeam(0, false); + } + + setBeam(length: number, connected: boolean): void { + length = Math.max(0, length); + const beam = this.node.getChildByName("beam"); + const body = this.setFrame("beam/body", (connected ? "laser_win" : "laser_") + this.colorIndex); + const start = beam.getChildByName("start"); + const end = beam.getChildByName("end"); + start.x = Math.min(this.beamStartOffset, length); + start.width = Math.min(56, length - start.x); + end.width = Math.min(64, length); + // The tail already contains the thin beam; join it instead of drawing both on top. + const bodyLength = connected ? length : Math.max(0, length - end.width); + body.node.setContentSize(bodyLength, connected ? 94 : 42); + body.node.x = bodyLength / 2; + end.x = length; + end.active = !connected && length > 0; + body.node.active = bodyLength > 0; + start.active = length > start.x; + } + + /** Place the hardware inside the wall's solid face, with its colored tip on the beam. */ + initHead(role: "emitter" | "receiver", color: number, direction: LaserDirection, inset = 0, retraction = 0): void { + this.show("head"); + const head = this.setFrame("head", (role === "emitter" ? "laser_send" : "laser_accept") + color); + head.node.setAnchorPoint(role === "emitter" ? 1 : 0, 0.5); + head.node.angle = this.angle(direction) + (role === "receiver" ? 180 : 0); + const x = direction === "right" ? 1 : direction === "left" ? -1 : 0; + const y = direction === "up" ? 1 : direction === "down" ? -1 : 0; + head.node.setPosition(x * (inset - retraction), y * (inset - retraction)); + } + + getBeamOrigin(): cc.Vec2 { + return this.node.convertToWorldSpaceAR(cc.v2(0, 0)); + } + + getVisibleBeamOrigin(): cc.Vec2 { + return this.node.getChildByName("head").convertToWorldSpaceAR(cc.v2(0, 0)); + } + + /** Attach at the first wall segment's laser socket. The door grows right/up. */ + initGate(length: number, direction: LaserDirection, laserColor: number, centerY?: number, shadowEdge?: number): void { + this.show("gate"); + this.verticalGate = direction === "left" || direction === "right"; + const gate = this.node.getChildByName("gate"); + gate.setPosition(this.verticalGate ? 0 : (length - 1) * 60, + centerY == null ? (this.verticalGate ? (length - 1) * 60 : 0) : centerY); + // The 2/3-cell vertical assets name the halves opposite to the 1-cell pair. + const names = this.verticalGate ? (length === 1 ? ["up", "down"] : ["down", "up"]) : ["left", "right"]; + this.clearSlicedFrames(); + names.forEach((name, index) => { + const sprite = this.setFrame("gate/" + (index === 0 ? "first" : "second"), "laser_" + name + "_" + length); + const originalSize = sprite.spriteFrame.getOriginalSize(); + const halfLength = this.verticalGate ? originalSize.height : originalSize.width; + this.gateHalfLengths[index] = halfLength; + const frame = sprite.spriteFrame.clone(); + // Both ends retain their original pixels, including the silver strip. + frame.insetLeft = frame.insetRight = this.verticalGate ? 2 : 8; + frame.insetTop = frame.insetBottom = this.verticalGate ? 8 : 2; + this.slicedFrames.push(frame); + sprite.spriteFrame = frame; + sprite.type = cc.Sprite.Type.SLICED; + // Cover the three transparent pixels at the inner edges without resizing the art. + const outerEdge = halfLength - 1.5; + if (this.verticalGate) { + sprite.node.setAnchorPoint(0.5, index === 0 ? 1 : 0); + sprite.node.setPosition(0, index === 0 ? outerEdge : -outerEdge); + } else { + sprite.node.setAnchorPoint(index === 0 ? 0 : 1, 0.5); + sprite.node.setPosition(index === 0 ? -outerEdge : outerEdge, 0); + } + }); + // Center the unequal vertical halves, then lift the whole gate slightly to expose more base below. + if (this.verticalGate) gate.y += 3 - (this.gateHalfLengths[0] - this.gateHalfLengths[1]) / 2; + if (shadowEdge != null) { + const first = gate.getChildByName("first"); + // Native artwork: the face/shadow boundary is at column 39 or row 42. + if (this.verticalGate) gate.x = shadowEdge - (39 - first.width / 2); + else gate.y = shadowEdge - (first.height / 2 - 42); + } + ["firstDot", "secondDot"].forEach(name => { + const dot = this.setFrame("gate/" + name, "laser_yuan" + laserColor); + dot.node.setContentSize(14, 14); + }); + this.gateOpen = null; + this.setGateOpen(false, true); + } + + setGateOpen(open: boolean, immediate = false): void { + if (this.gateOpen === open && !immediate) return; + this.gateOpen = open; + const gate = this.node.getChildByName("gate"); + ["first", "second"].forEach((name, index) => { + const halfLength = this.gateHalfLengths[index]; + const size = open ? 16 : halfLength; + const half = gate.getChildByName(name); + const dot = gate.getChildByName(name + "Dot"); + const offset = (open ? halfLength - size - 1.5 : 0) * (index === 0 ? 1 : -1); + const dotPosition = this.verticalGate ? { x: 0, y: offset } : { x: -offset, y: 0 }; + cc.Tween.stopAllByTarget(half); + cc.Tween.stopAllByTarget(dot); + if (immediate) { + if (this.verticalGate) half.height = size; + else half.width = size; + dot.setPosition(dotPosition.x, dotPosition.y); + } else { + cc.tween(half).to(0.2, this.verticalGate ? { height: size } : { width: size }).start(); + cc.tween(dot).to(0.2, dotPosition).start(); + } + }); + } + + private show(name: string): void { + this.node.children.forEach(child => child.active = child.name === name); + } + + private setFrame(path: string, name: string): cc.Sprite { + const node = path.split("/").reduce((parent, part) => parent.getChildByName(part), this.node); + const sprite = node.getComponent(cc.Sprite); + let frame = this.atlas.getSpriteFrame(name + ".png") || this.atlas.getSpriteFrame(name); + if (!frame) throw new Error("Missing laser artwork: " + name); + if (path === "beam/end" || (path === "beam/body" && name === "laser_" + this.colorIndex)) { + if (!this.beamFrames[name]) { + const joinedFrame = frame.clone(); + const rect = joinedFrame.getRect(); + // Sample inside the adjoining edges, avoiding the atlas's transparent padding. + if (path === "beam/end") { + if (joinedFrame.isRotated()) rect.y += 0.5; + else rect.x += 0.5; + } + rect.width -= 0.5; + joinedFrame.setRect(rect); + this.beamFrames[name] = joinedFrame; + } + frame = this.beamFrames[name]; + } + if (sprite.spriteFrame !== frame) { + sprite.spriteFrame = frame; + sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; + node.setContentSize(frame.getOriginalSize()); + } + return sprite; + } + + private angle(direction: LaserDirection): number { + return { right: 0, up: 90, left: 180, down: -90 }[direction]; + } + + private clearSlicedFrames(): void { + this.slicedFrames.forEach(frame => frame.destroy()); + this.slicedFrames.length = 0; + } + + onDestroy(): void { + const gate = this.node.getChildByName("gate"); + if (gate) gate.children.forEach(child => cc.Tween.stopAllByTarget(child)); + this.clearSlicedFrames(); + Object.keys(this.beamFrames).forEach(name => this.beamFrames[name].destroy()); + } +} diff --git a/assets/Script/prop/LaserView.ts.meta b/assets/Script/prop/LaserView.ts.meta new file mode 100644 index 00000000..bc9a2ef2 --- /dev/null +++ b/assets/Script/prop/LaserView.ts.meta @@ -0,0 +1,10 @@ +{ + "ver": "1.1.0", + "uuid": "d7662c1b-d635-43d4-80f9-2e1408efcfca", + "importer": "typescript", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/UI/prop/laser.meta b/assets/UI/prop/laser.meta new file mode 100644 index 00000000..e7095208 --- /dev/null +++ b/assets/UI/prop/laser.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.3", + "uuid": "8b004c21-225f-459d-88f2-6ce358b1774d", + "importer": "folder", + "isBundle": false, + "bundleName": "", + "priority": 1, + "compressionType": {}, + "optimizeHotUpdate": {}, + "inlineSpriteFrames": {}, + "isRemoteBundle": {}, + "subMetas": {} +} \ No newline at end of file diff --git a/assets/UI/prop/laser/laser.plist b/assets/UI/prop/laser/laser.plist new file mode 100644 index 00000000..af5e7575 --- /dev/null +++ b/assets/UI/prop/laser/laser.plist @@ -0,0 +1,1114 @@ + + + + + frames + + laser_1.png + + frame + {{300,581},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_10.png + + frame + {{204,603},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_2.png + + frame + {{142,603},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_3.png + + frame + {{212,559},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_4.png + + frame + {{150,559},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_5.png + + frame + {{495,534},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_6.png + + frame + {{472,490},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_7.png + + frame + {{451,534},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_8.png + + frame + {{410,490},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_9.png + + frame + {{248,515},{60,42}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{60,42}} + sourceSize + {60,42} + + laser_accept1.png + + frame + {{539,520},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept10.png + + frame + {{534,488},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept2.png + + frame + {{542,454},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept3.png + + frame + {{542,422},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept4.png + + frame + {{542,390},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept5.png + + frame + {{542,358},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept6.png + + frame + {{542,326},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept7.png + + frame + {{274,600},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept8.png + + frame + {{532,294},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_accept9.png + + frame + {{532,262},{30,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{30,24}} + sourceSize + {30,24} + + laser_down_1.png + + frame + {{358,462},{50,51}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{50,51}} + sourceSize + {50,51} + + laser_down_2.png + + frame + {{358,362},{50,98}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{50,98}} + sourceSize + {50,98} + + laser_down_3.png + + frame + {{161,2},{50,161}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{50,161}} + sourceSize + {50,161} + + laser_heng.png + + frame + {{2,124},{138,69}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{138,69}} + sourceSize + {138,69} + + laser_left_1.png + + frame + {{101,537},{47,59}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{47,59}} + sourceSize + {47,59} + + laser_left_2.png + + frame + {{480,68},{97,59}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{97,59}} + sourceSize + {97,59} + + laser_left_3.png + + frame + {{2,63},{157,59}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{157,59}} + sourceSize + {157,59} + + laser_over1.png + + frame + {{476,408},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over10.png + + frame + {{476,328},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over2.png + + frame + {{466,248},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over3.png + + frame + {{466,168},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over4.png + + frame + {{410,410},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over5.png + + frame + {{410,330},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over6.png + + frame + {{400,250},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over7.png + + frame + {{400,170},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over8.png + + frame + {{310,515},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_over9.png + + frame + {{480,2},{64,78}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{64,78}} + sourceSize + {64,78} + + laser_right_1.png + + frame + {{390,534},{47,59}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{47,59}} + sourceSize + {47,59} + + laser_right_2.png + + frame + {{2,579},{97,59}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{97,59}} + sourceSize + {97,59} + + laser_right_3.png + + frame + {{2,2},{157,59}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{157,59}} + sourceSize + {157,59} + + laser_send1.png + + frame + {{532,221},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send10.png + + frame + {{532,180},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send2.png + + frame + {{414,609},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send3.png + + frame + {{388,609},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send4.png + + frame + {{388,583},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send5.png + + frame + {{362,581},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send6.png + + frame + {{274,559},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send7.png + + frame + {{101,624},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send8.png + + frame + {{101,598},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_send9.png + + frame + {{275,117},{39,24}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{39,24}} + sourceSize + {39,24} + + laser_shu.png + + frame + {{161,54},{61,145}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{61,145}} + sourceSize + {61,145} + + laser_start1.png + + frame + {{248,289},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start10.png + + frame + {{248,175},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start2.png + + frame + {{161,117},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start3.png + + frame + {{308,112},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start4.png + + frame + {{422,54},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start5.png + + frame + {{96,423},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start6.png + + frame + {{248,403},{56,110}} + offset + {0,0} + rotated + + sourceColorRect + {{0,1},{56,110}} + sourceSize + {56,112} + + laser_start7.png + + frame + {{96,309},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start8.png + + frame + {{96,195},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_start9.png + + frame + {{308,54},{56,112}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{56,112}} + sourceSize + {56,112} + + laser_up_1.png + + frame + {{306,465},{50,48}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{50,48}} + sourceSize + {50,48} + + laser_up_2.png + + frame + {{306,362},{50,101}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{50,101}} + sourceSize + {50,101} + + laser_up_3.png + + frame + {{324,2},{50,154}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{50,154}} + sourceSize + {50,154} + + laser_win1.png + + frame + {{306,266},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win10.png + + frame + {{306,170},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win2.png + + frame + {{154,463},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win3.png + + frame + {{154,367},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win4.png + + frame + {{154,271},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win5.png + + frame + {{154,175},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win6.png + + frame + {{2,483},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win7.png + + frame + {{2,387},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win8.png + + frame + {{2,291},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_win9.png + + frame + {{2,195},{92,94}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{92,94}} + sourceSize + {92,94} + + laser_yuan1.png + + frame + {{524,608},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan10.png + + frame + {{496,596},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan2.png + + frame + {{468,596},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan3.png + + frame + {{440,596},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan4.png + + frame + {{539,580},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan5.png + + frame + {{539,552},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan6.png + + frame + {{541,152},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan7.png + + frame + {{541,124},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan8.png + + frame + {{541,96},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + laser_yuan9.png + + frame + {{541,68},{26,26}} + offset + {0,0} + rotated + + sourceColorRect + {{0,0},{26,26}} + sourceSize + {26,26} + + + metadata + + format + 2 + realTextureFileName + laser.png + size + {569,650} + smartupdate + $TexturePacker:SmartUpdate:0645979bb6e1f06e46c46220fdeccbbe$ + textureFileName + laser.png + + + diff --git a/assets/UI/prop/laser/laser.plist.meta b/assets/UI/prop/laser/laser.plist.meta new file mode 100644 index 00000000..227afb2e --- /dev/null +++ b/assets/UI/prop/laser/laser.plist.meta @@ -0,0 +1,1945 @@ +{ + "ver": "1.2.6", + "uuid": "4edccf38-53b4-4a32-a942-c725bf848023", + "importer": "asset", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "size": { + "width": 569, + "height": 650 + }, + "type": "Texture Packer", + "subMetas": { + "laser_1.png": { + "ver": "1.0.6", + "uuid": "94a4823c-ac6a-4bed-8bea-d9119c385559", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 300, + "trimY": 581, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_10.png": { + "ver": "1.0.6", + "uuid": "d1385da2-184d-4f2a-8377-82d49e4c0856", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 204, + "trimY": 603, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_2.png": { + "ver": "1.0.6", + "uuid": "eec87c04-b5dd-44b5-b574-70503015ab95", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 142, + "trimY": 603, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_3.png": { + "ver": "1.0.6", + "uuid": "9d0a3ce0-2654-402e-a3d6-a7b117d67348", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 212, + "trimY": 559, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_4.png": { + "ver": "1.0.6", + "uuid": "e804fa37-cb20-4a98-8bf9-8c074fa0fd15", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 150, + "trimY": 559, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_5.png": { + "ver": "1.0.6", + "uuid": "5d22029e-1637-469d-a924-770804e4e342", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 495, + "trimY": 534, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_6.png": { + "ver": "1.0.6", + "uuid": "e18af31a-3c7a-436c-a298-3cd4362f6ebe", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 472, + "trimY": 490, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_7.png": { + "ver": "1.0.6", + "uuid": "457235f4-b602-4d59-a255-c0469540b187", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 451, + "trimY": 534, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_8.png": { + "ver": "1.0.6", + "uuid": "b3fd3435-09b5-4972-ac41-b37dee07ed45", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 410, + "trimY": 490, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_9.png": { + "ver": "1.0.6", + "uuid": "8fb75e4c-39c7-4466-a0ae-6cbf99099901", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 248, + "trimY": 515, + "width": 60, + "height": 42, + "rawWidth": 60, + "rawHeight": 42, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept1.png": { + "ver": "1.0.6", + "uuid": "8f4808b3-bbf0-413d-a1f9-3d2ae2376af2", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 539, + "trimY": 520, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept10.png": { + "ver": "1.0.6", + "uuid": "dd162525-3302-415d-928b-3d21f8c07389", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 534, + "trimY": 488, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept2.png": { + "ver": "1.0.6", + "uuid": "e151ddc2-66ee-4de0-866c-32fef7f4550a", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 542, + "trimY": 454, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept3.png": { + "ver": "1.0.6", + "uuid": "f60275d0-20ce-422f-b6d9-dc5faf65efea", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 542, + "trimY": 422, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept4.png": { + "ver": "1.0.6", + "uuid": "c42734f5-5df5-4563-ae33-259c1c8beeba", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 542, + "trimY": 390, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept5.png": { + "ver": "1.0.6", + "uuid": "00b50e21-598a-483d-8cdb-e25badf4dffb", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 542, + "trimY": 358, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept6.png": { + "ver": "1.0.6", + "uuid": "052b74ab-9be0-4748-9b4f-86b25ccc3973", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 542, + "trimY": 326, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept7.png": { + "ver": "1.0.6", + "uuid": "1677a2d6-57a5-4e4e-83b8-2acbf2c4af82", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 274, + "trimY": 600, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept8.png": { + "ver": "1.0.6", + "uuid": "d4735f2a-c238-497b-8b0d-2e88ce8ff4bd", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 532, + "trimY": 294, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_accept9.png": { + "ver": "1.0.6", + "uuid": "819581f7-ce5e-4c5e-af0a-48fc7e068b83", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 532, + "trimY": 262, + "width": 30, + "height": 24, + "rawWidth": 30, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_down_1.png": { + "ver": "1.0.6", + "uuid": "8248f378-ba28-4650-879f-3b36dbf411ff", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 358, + "trimY": 462, + "width": 50, + "height": 51, + "rawWidth": 50, + "rawHeight": 51, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_down_2.png": { + "ver": "1.0.6", + "uuid": "78ea8d76-d64a-49ea-a0ea-8c62f0e886cc", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 358, + "trimY": 362, + "width": 50, + "height": 98, + "rawWidth": 50, + "rawHeight": 98, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_down_3.png": { + "ver": "1.0.6", + "uuid": "f9c3bb33-ba0a-47ac-89e8-04e847919376", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 161, + "trimY": 2, + "width": 50, + "height": 161, + "rawWidth": 50, + "rawHeight": 161, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_heng.png": { + "ver": "1.0.6", + "uuid": "7976c91c-2b4e-4f46-8594-efa1a0b4721a", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 124, + "width": 138, + "height": 69, + "rawWidth": 138, + "rawHeight": 69, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_left_1.png": { + "ver": "1.0.6", + "uuid": "761c9a8d-095d-4bcc-9251-d2d43ff376c2", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 101, + "trimY": 537, + "width": 47, + "height": 59, + "rawWidth": 47, + "rawHeight": 59, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_left_2.png": { + "ver": "1.0.6", + "uuid": "160c562c-35b1-4459-ab83-d235a6330542", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 480, + "trimY": 68, + "width": 97, + "height": 59, + "rawWidth": 97, + "rawHeight": 59, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_left_3.png": { + "ver": "1.0.6", + "uuid": "84135fac-2798-46ce-865a-85dba42c1f63", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 63, + "width": 157, + "height": 59, + "rawWidth": 157, + "rawHeight": 59, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over1.png": { + "ver": "1.0.6", + "uuid": "b682716c-bb0f-40c2-b177-c3d5956da181", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 476, + "trimY": 408, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over10.png": { + "ver": "1.0.6", + "uuid": "afeb46d8-8446-42fd-9249-e056805720c7", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 476, + "trimY": 328, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over2.png": { + "ver": "1.0.6", + "uuid": "03e86263-31c3-4076-b509-a46544e61100", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 466, + "trimY": 248, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over3.png": { + "ver": "1.0.6", + "uuid": "f6270441-1706-492e-9796-308517d68cee", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 466, + "trimY": 168, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over4.png": { + "ver": "1.0.6", + "uuid": "14183279-10f6-489f-b623-dfe3886635d1", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 410, + "trimY": 410, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over5.png": { + "ver": "1.0.6", + "uuid": "f844c445-1b61-4efe-9eee-c346bdeda451", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 410, + "trimY": 330, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over6.png": { + "ver": "1.0.6", + "uuid": "da77d6c1-2337-4c03-9888-e6fbb9ed95b9", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 400, + "trimY": 250, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over7.png": { + "ver": "1.0.6", + "uuid": "ef31a03d-98b3-46fd-9cd2-fb03816a1ba1", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 400, + "trimY": 170, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over8.png": { + "ver": "1.0.6", + "uuid": "d1d6b16c-5a3e-4cff-8b5c-6bb85146d24f", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 310, + "trimY": 515, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_over9.png": { + "ver": "1.0.6", + "uuid": "b673d160-1502-4f3b-8faf-94768d733b27", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 480, + "trimY": 2, + "width": 64, + "height": 78, + "rawWidth": 64, + "rawHeight": 78, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_right_1.png": { + "ver": "1.0.6", + "uuid": "e4e330d0-48d4-4652-834b-89a1263227bf", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 390, + "trimY": 534, + "width": 47, + "height": 59, + "rawWidth": 47, + "rawHeight": 59, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_right_2.png": { + "ver": "1.0.6", + "uuid": "ba9ce69a-3ecc-4228-b1fc-1e035afe14e6", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 579, + "width": 97, + "height": 59, + "rawWidth": 97, + "rawHeight": 59, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_right_3.png": { + "ver": "1.0.6", + "uuid": "2cd18e14-7883-4477-8e1b-5ada4fbc2e9b", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 2, + "width": 157, + "height": 59, + "rawWidth": 157, + "rawHeight": 59, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send1.png": { + "ver": "1.0.6", + "uuid": "6f4aa1ba-e2c2-43fe-a155-764ea6370c51", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 532, + "trimY": 221, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send10.png": { + "ver": "1.0.6", + "uuid": "7c80553b-46d1-4ffc-940d-0f49ab9bfe73", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 532, + "trimY": 180, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send2.png": { + "ver": "1.0.6", + "uuid": "395e316d-b1d8-49b3-8669-2d7737ecc84d", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 414, + "trimY": 609, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send3.png": { + "ver": "1.0.6", + "uuid": "a3314cbe-2cce-4de2-9911-cf92cde20631", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 388, + "trimY": 609, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send4.png": { + "ver": "1.0.6", + "uuid": "7ac5c482-345f-451f-af25-f8b50fba335c", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 388, + "trimY": 583, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send5.png": { + "ver": "1.0.6", + "uuid": "6f2116ae-2595-4ccf-b432-fce120efc9df", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 362, + "trimY": 581, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send6.png": { + "ver": "1.0.6", + "uuid": "9cb546b9-149b-4b4c-a632-9f0a9f0fe730", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 274, + "trimY": 559, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send7.png": { + "ver": "1.0.6", + "uuid": "e5e1365e-c0fe-4f4f-9abf-e4c533cf670b", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 101, + "trimY": 624, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send8.png": { + "ver": "1.0.6", + "uuid": "a5694cd8-7d02-4718-84cd-fecaa5f561cd", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 101, + "trimY": 598, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_send9.png": { + "ver": "1.0.6", + "uuid": "61c54931-13a7-455a-912c-2d4e60d37618", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 275, + "trimY": 117, + "width": 39, + "height": 24, + "rawWidth": 39, + "rawHeight": 24, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_shu.png": { + "ver": "1.0.6", + "uuid": "1a445260-d81c-4af1-b4ef-2865cefed2ab", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 161, + "trimY": 54, + "width": 61, + "height": 145, + "rawWidth": 61, + "rawHeight": 145, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start1.png": { + "ver": "1.0.6", + "uuid": "511cc0d5-3332-423b-a925-96e4fd743483", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 248, + "trimY": 289, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start10.png": { + "ver": "1.0.6", + "uuid": "bbad8535-7ab1-4771-9a3c-33c9528d36ca", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 248, + "trimY": 175, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start2.png": { + "ver": "1.0.6", + "uuid": "cafdc8a6-4a2e-4a24-b74a-e0fbaba05171", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 161, + "trimY": 117, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start3.png": { + "ver": "1.0.6", + "uuid": "f4fa1854-c81f-41a3-ba95-5d93ae6c8925", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 308, + "trimY": 112, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start4.png": { + "ver": "1.0.6", + "uuid": "6c654521-c753-484a-8c48-d5b6d1e6dfd6", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 422, + "trimY": 54, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start5.png": { + "ver": "1.0.6", + "uuid": "8dbb56ad-dbab-4dba-923f-241a0d26ca5f", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 96, + "trimY": 423, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start6.png": { + "ver": "1.0.6", + "uuid": "7f355346-17ae-425b-9b6b-3e66d1d9b783", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 248, + "trimY": 403, + "width": 56, + "height": 110, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start7.png": { + "ver": "1.0.6", + "uuid": "aa07d88b-f733-4cb2-aee8-3f776c295987", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 96, + "trimY": 309, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start8.png": { + "ver": "1.0.6", + "uuid": "3c9ebbf5-58dd-42da-998f-c1a7b982e2be", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 96, + "trimY": 195, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_start9.png": { + "ver": "1.0.6", + "uuid": "bad14256-4caa-4948-8877-f8cf13fe0976", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 308, + "trimY": 54, + "width": 56, + "height": 112, + "rawWidth": 56, + "rawHeight": 112, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_up_1.png": { + "ver": "1.0.6", + "uuid": "4b36fc6c-4ddf-4b30-b922-6d8a6cb83fdf", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 306, + "trimY": 465, + "width": 50, + "height": 48, + "rawWidth": 50, + "rawHeight": 48, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_up_2.png": { + "ver": "1.0.6", + "uuid": "43354fc6-55d8-4ca5-acd1-0e2474f6857f", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 306, + "trimY": 362, + "width": 50, + "height": 101, + "rawWidth": 50, + "rawHeight": 101, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_up_3.png": { + "ver": "1.0.6", + "uuid": "648c6950-4390-46ba-84f4-5725862f7c36", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": true, + "offsetX": 0, + "offsetY": 0, + "trimX": 324, + "trimY": 2, + "width": 50, + "height": 154, + "rawWidth": 50, + "rawHeight": 154, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win1.png": { + "ver": "1.0.6", + "uuid": "3712fd9b-7aef-412e-92b7-c9cb17a11bb9", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 306, + "trimY": 266, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win10.png": { + "ver": "1.0.6", + "uuid": "9db92e85-41ea-4226-90fe-42d303a1e2e8", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 306, + "trimY": 170, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win2.png": { + "ver": "1.0.6", + "uuid": "32a5b819-dc5f-4bf1-ad6d-48d514963f95", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 154, + "trimY": 463, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win3.png": { + "ver": "1.0.6", + "uuid": "8bdf25cc-4b4f-4f0d-a46d-2d0bf2b5dd56", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 154, + "trimY": 367, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win4.png": { + "ver": "1.0.6", + "uuid": "97bf3a7a-2c6c-4242-aa62-b34aa0756349", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 154, + "trimY": 271, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win5.png": { + "ver": "1.0.6", + "uuid": "b22a4ff5-2a88-479d-a455-58fc508a2ef8", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 154, + "trimY": 175, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win6.png": { + "ver": "1.0.6", + "uuid": "5de017fd-a1f5-4775-97a7-05cfa1a0705f", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 483, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win7.png": { + "ver": "1.0.6", + "uuid": "d3b5acf7-d39e-44c6-a874-65ea29546818", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 387, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win8.png": { + "ver": "1.0.6", + "uuid": "ce47d5c5-cd5a-403d-a51d-7f8bcb52fd95", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 291, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_win9.png": { + "ver": "1.0.6", + "uuid": "21354a64-579e-48f8-ad3e-964719fd3132", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 2, + "trimY": 195, + "width": 92, + "height": 94, + "rawWidth": 92, + "rawHeight": 94, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan1.png": { + "ver": "1.0.6", + "uuid": "b7918c85-548e-459e-adc3-6b053d92a27a", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 524, + "trimY": 608, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan10.png": { + "ver": "1.0.6", + "uuid": "32df0289-b8d4-45c4-aff6-d1bf3d0e4cfc", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 496, + "trimY": 596, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan2.png": { + "ver": "1.0.6", + "uuid": "415161fa-7347-4d1c-88cf-e232490789aa", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 468, + "trimY": 596, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan3.png": { + "ver": "1.0.6", + "uuid": "2b664c89-f69e-4eb3-b2b2-eac411f37c0a", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 440, + "trimY": 596, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan4.png": { + "ver": "1.0.6", + "uuid": "3b6090c1-87f0-49dd-8f19-abc3f2867484", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 539, + "trimY": 580, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan5.png": { + "ver": "1.0.6", + "uuid": "2ca00101-eb25-4d41-9685-c3d084cae199", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 539, + "trimY": 552, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan6.png": { + "ver": "1.0.6", + "uuid": "d992c89a-e496-49b2-97ea-645f61bf8719", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 541, + "trimY": 152, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan7.png": { + "ver": "1.0.6", + "uuid": "29f51d40-eb78-41e4-9f1c-3c011b93a8ac", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 541, + "trimY": 124, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan8.png": { + "ver": "1.0.6", + "uuid": "52a94f1a-cc33-48cf-9de9-3dfb289178d4", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 541, + "trimY": 96, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + }, + "laser_yuan9.png": { + "ver": "1.0.6", + "uuid": "88911ef1-85bf-4d20-b0fe-2cb2d1b03a48", + "importer": "sprite-frame", + "rawTextureUuid": "6b986358-f48c-48ed-be8c-13286edd9847", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 541, + "trimY": 68, + "width": 26, + "height": 26, + "rawWidth": 26, + "rawHeight": 26, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "spriteType": "normal", + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/UI/prop/laser/laser.png b/assets/UI/prop/laser/laser.png new file mode 100644 index 0000000000000000000000000000000000000000..011b0c3a86d9cf996c6a9c2819b3f3265e7a7f84 GIT binary patch literal 42618 zcmcF}gxifR;%*>fH=gc|tiFylBdQ8kfjD>~uSVdVu8w(2u^zcR#;y#pY zzHbV9Nbu}sHDs}{s^Um)tnjh0aIxO1>ng@4A?9W&U$YS2C()mGey5_My6UWd_}Tk% zB6sZaI=y6N^XTiR%ZHLM!~L`K-#6FM7d`EFU6Hrp+t*0^#vFn7VkNzet_Pr(_A*DmKU~W?9DVmVMx4C4yAQn*Yf3(_w1OR8XBn?0UhV5^ z{c*m#`p|GJE7ziV{{7nUgVH~>o!$E_6@9_Ue+Z>Xf5B?wbtpJu+{`XC9&yGlP9M(xTy{XsT z$8m(On$D?z-eRm@r=Ob&9w+`FX z<}Met!Rs44-QT+Yw(Nt03WGH@RHoJWX|eu_5jU;g_Ez?KnysxuPH}QF46;={!ZHQI zwfa>BLSNZ@qLCgBQMS!5y-ajGTeM*bzVfPaf`aVq@}H-C{H!1 z#n)Ctg0>X3CaSBoYt0w8owjI_#heFeo{)dW=0d~<%TO38$Rv`h2;m#iV9x+)Q|aG< zFoP9MDg1?J0&!MM+H5~&Y07A;6d>JL3s{QpGH{r|2eP69+< z^~MRZJZ{o=IUSGd;>(ogdww#8j6IzxL;KJ77L}+O1m}<6oMCc6ks9EkER!cP4+Dz> zgXmX({P^(*n{`E{802wbvN!yZ3$vyG_;r1_=u*grwR}sENa+aJBn@~P_}stP0%qKI zS%?VD#^PlZa}HyP3@WxwlXnWuA|wm2Hsc4e2to;yz9c(@$vnoUC;@SbF_sPEjygV& zR0ITTgILsME7{E5#R#2`9Tni3{}D5c$Hh}ein}a!MV&-3vGYc3u!6&e6s}4y}%=SupI|py^4MOX-t7AHSP^G+AV3t`6WHTJ+kq+k8LRRWLiolqS6N_y;jw8H1!>%WE0kMf6M-oA;f2s9C7m0L@OXv z(o^dd0phT~KMD(S4|2bH?YX#i@&}vbWW#P0NX;~s)-xQsG~6a-+{X*LxLRSaWpcQF zy{oXa^89M@TJiL8>)j{r>&g{3)^!SRtjrTsy7++A(j zFJ!bSWZ{4bZPnTBP$2?(>Ep#}Q=3;v78N!(D6J(|6 zvWoEFil`5FOCz)b5}qAZ$^eOyUeAoYAf&aYelt%JovU}?(Pz+V=>?jYBO~hM=O_2w zs-EXE^5Jwqc|dijrvUxuSL`5<)Vw(4Donp3rslU-!Szn}R;jA)dwpYerU|4?_d9+` zgI>`2yu0Sw{hM1_z5|8p;=EuayN5pI#z?dKJe=KAf3-r42liGI^?9lhb9eYM3||^W z1BFO_`J{NpA_q?a9u#sENMs}vyL(&o-kB*X0VMc;IlhTO`KZHl*J|BAGjT?I;zgSh zu}HiAPIRY^cc0(s{&xMV%|>$id0n2v>pLeUt$@I5vb0-EY*^$M{)*k{qn8t@{KL07 z@#9rCJ^U3d$CMX8wR?kqYud)U>&A;zIyB_coSWB%E`**l@$rj-3=%yQbRTRcTS8|W zr{_21Or!s#76n^_F~^W^b3m@h^|bp(SMXgWM*_#Psa!5DTHsq+r$JrfNmWka^Dn8F zQanuZGv9g}zvme?53_?VSY}rQ1+Ad(1ZDdAh*6_za8sOOcNdDUUkdq6Mb%fDgCUk; z;9#|O>4CpFovn0vOW|1^2a}Ed&I$#p)0CZGygfGgUKl-pwH^xR0;9{HTj|c$U6;m> zmowcNx2BHAq7^J=B%6ALe|{g|qL#)Nx@aR>q_={SN{rJ3?+OGSz2#(8`gD@~h>6`^ zI)URE=J4J$t4QfRjugGDQVfbKme`Ph9aFO>JqJ-BX#F}4={)rQRDXD}$=)Lr+CK5a ziNwB07yR)SUv45^KT#yEwDoy%f0R)@M>~u=b}( za5o)Ita(4>mNHE#)}^jSSBVf4FH>=kZKe#&AEg+IB7%X1K(SxqIqv#@mb^sRs8@{> zLc1*LQl*o79j9_~YHMp{dnqD&18;Z8o;X+}ha&eQVk$@K7x-#*bUgOFpB_^bfUFAn0`*KMVVX)!#}E z;*n$eH0|ljNE%mCGUlNK0&N!KA%k&0ryF=NvCkd-VUvAhejJu9thrWN3;X&eLP=_R z)AbMKU^%p_ckdRZstxUWp4M}?_I`!X*=EThW44jKmDH+0UVlFgHAxb|L2Vs-`@Hn1-;ir`{eI<4`(1oNV< zepJc3IUgFb_;@hztw`dG79{4Dwh&_86uRUgdRbDxT*2PQ<~kdjZegUAQ69tgpjRiJ znTLt9l7a+4R<8AJP1nD&KQH%{gRwnVrHmbmBH`>x)IE5;cZm>B82F;Ur{Bcw{0GPv ztM4cbnea;~mko@rl|XAb>VGO06I-G9$|qc&tT$c1q0a4Wc#ou z&lh}*XXk8)odc z`EgRd=gea?nQW1Oi!zPXpWnL>fhm(xXrR|-8Ci5DaxM&MbJpS%>c08q*MXuRzg5qZ zO+xV*(E_W3#qdofN5D^h9TL(QINzhp_uPiqGrW725b$JFvZ>_5BJ$!AQ^<$IxENm~ z-q5bujQxWFU<^8ClI7!AOH-DDMH==Gdzemj%;`_BP?L4L)iA!&I|pchr)!JD(E z&xKeE#IK%3dc`w0|3;v~0k*H3>rIB!Sz2gZBI;S{!NeE}u@5~cN!7kB=6ECzbTOq% z${2QY7fm9A?S!LRV=&#(6K}%@PX1%zR?0!2624Z`WP_=}{;-<|X-rnLlSxgrdw1fo z2Yy029c*adOCCgUORFG7&2L~(;=mn(T&q9m&q6jV`k#1wh73&vUVLyG(o#VQ-|=u8 zyN4tILQXtW-rBn;e9xO6Rjm=1bKkAur(kkEG`6%eq~*xcd#7WQAi_9IH>3V7PZU$$ zypbqdh`R#7du8UA?x1AL4Tebn42T$BoWGUD!G!^8I%i#XU*mZ0hUY~BaU%L=Ja`fs zs;6fzJ(XepTF{hTrwZ?)Ut+JKfr5xAi6FvckLOj>Z0kpUMayg;#hw#~R)z>r+UG|1 zsobG%L(KCl881;vT;_xfC%J;gUToOexJOwUxkM0TNJ|+aG}tExfh%)ILS$eJ|042V z{;_d{?$Q~Jq;jvZO`JVAO+^-MK8r@jS+7fJ>cPu-JSz{b}`{0DSLXEY1b4H2C*m|St^ zHSs2W(L@?aKsIJB>!auDc^V@q9bHfRi#7n3FBK4(lA$30wt}U-K~;^Sfqt+A5mqwj zV%|L83uU-Q#f@+^dn5<5{KMfqwry;Rb&yEE{o`fro8q1gBvKoirf6!{Al-K1P+`8b zb;=V4791mZB$bqp*o{r9X63|KL2+=!7MHSmy!$Wjd#z7ikEN|iHWL>?P&a%qf11C* z1t+ci+ev>WuHnp&;#?7$}WulRY>&Q{a}jOJSys0(V)6?H0e{sFBcvtB3{qDkN!; z29N3?pP>@Ue>loIXYDj|@wfC0Hq4WhQU2*HK`7X!Tk7 zyyWw5)ckO}`v_(HI8Kh6PuZCgbl>a5@^OerblX;NTj9Gw>q)Vzb3W=7J(DO4OJ7b$ ztY<0Jp$RfGFwc1iN68#GL%#E&TcJNGcxl8(k%2dPCwk z-4GE-z(UKuKu`f?t^zN|7^3lJRWZs*QYOQ>sZ6q{keRVcX|o~RI=`lno$us8)`*P1 zUd;KrG1EpdqktcV3XnQvy8VL-iUR_r;d+IW^v&@1jf$cpC+6|`Ff&X~dixq>T^-c3 z=3(DN%x2>nzvZpk_)&j8A98sC{;*l)OCgChY^qvS`mZ<{`ECUH)_r;I(@>wjO8qSC z>`kVL&u9_IBV{{=d=Mb73Jf1Hr8mX7>1R3$fMiE!=%rg{Q%fa@K>K4WP8%t}21T=V_57rq z_I^;QCncMnB){L;pQ%C6~j8HO@dT=T*H|j=!hu)w@1Fl9$(@xA&5W^QDCyy)&AYl!}z<{()%~bd> zo6w`qM2(@pr`BsfHU@|;!v$KSMG0nvZ(Pf}^<MCi!Y(@?dLIc(6iQCb1BIv?UP6mcZ+W=;>PY_$d?uBkFz@?2Rk^OwqbSJR@=a+ z$d!hubh)~*uP5vq$G=L3;!qC)1Ed`X-)$%xkPs6GyyBtik-sIOx785tuql zM5bOh<7&BkM)a+GI^)w*)5mO}t5Iv50ap`SeY=;RoJuWQkT92UYi=a9BSIQ&s-9$OYx&UQ4hx;EjwfqHRx0b0UK0 z^~%DU@cdJfaPt`)zQRg%GBkwnBtu`MkP0L0wMVfxl{bx?}E?O3Ug!t4AyvG?W|CZk@h8Xb?)&L$3kW2UY4 zY?q>zf1coaRYAWhynW4vz|YgSZ#4O7Paw{yDnuX3I^lCV;`d?=l_9z-g{QOLZt{3g zn8sXEz|Dve<|XO#Y#Im7h)G_ZaP1U+BQb{i?G!3u>8?;?6WU3`qydkD{Wr0GCDaYt z;{K{ngr(?4IU#fx_^Gpz=@U(bZ&8k!k)2ZY3B$k>y=?*2)YhdSt19neBtku&Q4SN7 zeWJnNoCqNi;p_u~Pf|&Jk-EZ>wGNAL9_6Gm((=(GbuS$lWG6fi2wx@90UuV9;b^3)gQOC>J zX84GFt*8H}u}EvA3(Bz=t%wP7Gvt#oZ`S{sp!i_?ocKVD!#D5SQS;NqAfLICM$R7V z2=;X+#gXtz6<2v{oe1tVVFcn^T7Xg{3%k;1kTz-3+aB!<-&Wc2iSda4jvE>!8)(Oa z1JJwnkLFU~TuJLlcvdC@B7B^dQKBW1gJ2=q0T9;r$Am&M7H+NNEc-qEGg2k+OQlL# z8bVOQ?@>w@L~np+flWu2tnt}id$FFbz+BDtYHQ&|F^7I(o*Gz}6b(l=!<@@q_C&uR z-2%VVx;-@2jKy7F3vSE>zLsPb{J8cqqlNa@#mcsZ*>RmP0U;v|7vrebGxJ{)^kH$x z9~gc6HwgD1@h2Swx{vtBit2sUm)i^b)bIz3&wwIAD1({j10^T6ECzaE^v;xD!fne& zcVSzU4uUjL^;bfZ0%;;g6-FP}{|lp0I3s5~goM&+uQO9*?62VlZkW!Xu;s!(BylU* zXd*)^YtGiAV-p=NE4Z!)9JaOk`tt=uP-oH+;v)Kz5hS0AH0y3CFuN6g26fhPuQmA- zl%9xQ=8<2gsQ8>ic4wOsx^d*8%M@>mAxueJnmlbM(a<9>ewLPu6sbvY-_VkU7ucVV ztCrAOV!qh~rDJmy{Sl~MvmGFDNgmo6AQV-~9SCk!g}Nu6^nF149~ZJJMWKRd>k{V} z+WaPs?{-G{4^p=jhNR0m(`A&zP3VRk&7pT`lVQlcZgqG;Lv9_9xQ7N1%51V5^Tl1k zBWYuEjXxrdN)1)M`c&zobxYZI#cW>Q#%Fp{I59LKZg4)leH}Yn3$9B=QKThWc}a%4 zVX^q9sFMEnXs)9$GsMNd7oIm@~B7X0zdJU8e zc-DwmNw)l46$Y-rur- ztACA-3r%+R$SQ}~(17c5!?xDa4<{SPP&Y8$I*{V49Xr~k2_A1#bId*k+_G>WI>`++ z;_P{ZA;lwDyp}c@rF*o@4F`4(>B}nh8g-xvCFt`&1n{?J1+{9~h=qiWEmY}$ifiWSB;Y=J9do;MX* zv>mCX2(D&Z4_tjdJYcNmU6NW!+|Q8q7*omGrgq-wFC+wg2lGugdcF^liTDjnL`s_& zm0w-7Bx}Io?xo=DWP_rC^@mO=#%J^%`t?rtUlt&6y)p|!XY*pnmEbGk5B4NQ&|qa# z?lGLb;QRRNIuE|SR8ws5Z$P3A0eFE@fp;=Y5-e9%6Ib!(h^Ib<`kN1`fFzv_9OVYFTFoqeM;o%*+*^Ju|O z9HCGG8YSP0Uf~2liw?hBn#gh%8zZ??ckM{9OSEdRA<4u(9|Yz*eN+|gX6O>hXabeu zLl?64aD%Su7tPE(u6;!ixE@R%Ixe|^;{k4r5qZ3C>`9csK>**poBrn(*a! ziB>w045UxR3JT#PO3dTPpwT6%P2{KAw$-eF2y4O5^uypu*!VyV$nuZOif^6gQlZ_* zcbwb$Zo_R(&jBBaU3g`fwaqn+JaWMf_a%#MR# zWQHBE6LoZ3D#5J}6R_`w;`-tiC4XOk%HPv=kQn&JSS1peggiCk1M~fgexvlWijsf5 zOv3tWi)xRV04V=QjBfCWh8aF-3igBPt@-iqM$&MY*IqFZ!MkPNE8QI2kblfp034lB z-?R*=vz3Yf5h?aOVc`Jp7V-eitnhoi56J;>ow6^wLFkoEcvXJUb=KQW$ zQ9l0q(G72#2ScK$qV5 zg_TX9u_RbQ`F}r!DlFrnpJq!)%17E)Ytr*WD0|P%#|q6PvkG!6cEpW_rhB= zS6u%YGXhyL#h;J}!LQYGw5ok&4N)zV{AZj~1|f_6nDV4H>xgxi0{y^YIDl~_|=pF)CX%ltDG<1R3G2GAnDVU+oCN!a#_bMDSKMQKqg3>a9xZ=-;(^a zK!+Uora(tGSeu>cqWnUAD9NMVcoJP^60jUO5jM+Ds|E6V^#mssD58=cac)v2ylaP^ zHherxRF5l$3wo8;x5bMNLQgQ)V8|Xz4@LQ$&8F2z%4qPkRNW@I;<~eV?RRLs!6oP# zim2qUy*_k`IzYRZI5!2kCL#458+3e4cVfOY2E}$99dj?QTnRtb*MFyIbeXxleDta# zuOw(wx#n|QX&!Ni$w#O_-3sNhe!wiRFubd0U4K~Vt5-nYRJYD(EBS)?iUsbR0DADo zk2akb-LCQTm7k|L=O{2wyng;-jgjbqJFg}oj6myxe~(*~BYC?27Ync%cgKeaLne86 zDsBn{hUjAW0-@ytJo-NOO*{S|kS`xV>9UrpqpFveNCglDQMpJT+jNzG@kkQLwKUPw z1E)D!uE^Kz{cU@`?rM$Cdc9`D)>FPx_%T)xrI%h%CaU=0{RYhRHpcZ~_ceYTW~O4W zp9Ds^ojtNfF1YzpL3IVd$^gJM@NMEXgXvQqFx{4A2!5`}rrTt|z+Az)L)5us}Lv9*Y52Pp&Pl6&% zexHwtachDRWCL1B$f3yW^dskY4juouVPN62PREeEZYK0t^leLQDyCr_W z-hVSrudaf0z;RlxJfkKQEz}@J3#;?2cg?^a(OkV6qcx%nLIb|&{V0vgg5Sl zH&}&X(KURc?!UzC);5cE*|^QJ93fC^zTnZc8gWLkDsJ67#L#&)FFXsWD+OvwHuK=D zSkBXo_L!KlANfcmoT&Y!fL1#?uF=iR4vpGwynYu=%bTS6CNNQO$AmY>A!8%W$^~PK&+YZhn(IdWY$Tjd zZ=j^_h{Ee}{Hmxw6a}VOVv2RGCXjH!jHSOxpi&-cP?q2u%3noX8D++F7*&VCdH;yK z9MF?!vJ2SkDyBeB7m(|c=?;PYo#DmR%GS*MIf!+4uLtN_6a|k|CyP10Vzu1Wx-$aw?GvpEi0_+IlP~=<>D`3?(o$}cd?fvkLG0Q~ZeCEoMRIU+;+(;JDQ**Pl2ny{i%R?C z={LogPaqAm@>N%A%~A=DI2>+4Xle6rK5+}SXHV24lajv>HS_<><#o!#-RJ$&LeR``*GVy5PYt4BW7DUK9DP|=}2a@qD&@6rkW6r8W8v<0}iU%{_ZM5+|ATo*6@P!H89$LEd? zD)&0t=aX<0YWyi(ta@VK$noInSjt1tQQjQBu~e6QuR#r597><_Ov%ywS@W1vwTTSH z@y)9OYGxMJ5xU9VI&&gOO3Q=%^)@yxu>Uk~%susHm)&Iu%J-CS{I<`eVMoxz zm^*g|JBn*u`R!J($xjE;dKKt%O2-}kR2{+x0%?=amtMCp>#p$JMywm|`dJg520R-j zc&sb3^3$8GLBIVg&oB<;4jtCy(VxVh3$WuoSCO7H2n}tUB6i*0slmd+tU$?Jiu7)3 z90snwP6?o;0@j`_1!6J|mY{3nGkuQX%r*sRL$A+eso+*u>i1n&KGu!I&h?Rx-Yyst zL2Tn6pdRY~%2Ad5T@-y3;6Q}6lQilbv5PLBd6qo*&6)m!Njj-GkHnTPm|J2<7FC$O zH0pU$ClNoRr828%5;7cIJtd0)PBLW`CW{U7(iU0K2-!+nf`s z+9}b03N@9ZYnH=-jip23&bs4n0rhY3KNNxNr)wu%^B~30n@Q1BWDlI*`Dv1S%J3vAzPipFKFT~>m&vrX1`y@ zeonVtewmEyI8d0jd=08#CbrWZG8P6Xg4jWq2%Fm-{TYxXyIkZvEw{ElxLZ~@LZXiE zi{st8{BOW@Y0|#dA0RRItibf<@#CKE)6*FaU_CK^EyahizB@b(4*Lno1hI!m^_lqH z$a1wcP#5{SrD}n_fHQe%3>@g$Y6QxC0UnyIwkC1wd|i$KBEJ&`zG{{%})E}`y! zVb|Qq+0_)`?*0-ae!Yggb&1^Gg}g{+KZvf1OaJvaVsSdKa5T*( zPp5o&rnCwF((w09VWKN(3>Qs>SHq5aRp`aHpw!wPs1>C>SHOLT;+CnZ6@v8kX@baE zhJe^#B~;E{1+(+&ky0DFaRxC%T>_Yi+4YZG>>VE!7tF)PL&B z;tiBw;ckERZA9qg=c)W>Moc}k@5?lN0mJ`y>_9=LAi`6muZ2}ADt znZIJ_qgd$k1_Kq}N<{H}a|;NR;=2i2&o?<#vvy^!)fKu(hP9u8%_3BAcDS zAX*tPrG^o9(Uhu2Hf748g|<2u`8!W+kPx|#-$0_ySI6)+D%+m-P>HnhZJ==t-y8U> zU%>BT{rz!*v@`uNXzsW_`kU(nv10lw;723q^03Yn-V(pPm`b`jG|L+b)zco+j`FEV zw7C*O76r`aaIHKFv@kPU8yPiVdjEc)#`OBo1ofOG%}FhH{KY0Y2sbSmSnxLl%_^?< zVe3s52g24#FVLiP7rncyy-%!Apg7`|4Pw!P(jggMyyL#l^$h4oj)XljdSh7btj!!r zx=HFl0jswk2R0lj{1~OlW*+N$ z8@`8+Pw?kl$q8;!fi+>7ZR9nPa5AQb=!<7^HS) zL^H@$Cr(jmgx-5FQ*KTeH&3+Z8R$@+<{r}|wV>@k| zkHmB{!x?6^k=&q9FyfDd5Ebuliv2mkP~D?8Xf{EHz8To9cf1-s(So-ur6P7BV97n~ z(8F#&`*P+<_PILVCl{4-NEUeOyQW99`SS|#b_N*n5Gp8m)Tq&8FIr z1lm>z#Rh#h_)T`Va+lM~C>)r|^Y`nM)NnKXs5<{<9Tj~jkI0jU#sF1R(5xJy=11BR zKWBv6d&-lpyLCbHn&}z-jSE?XNa$`K_i$>FE%#BKzXse0P(aHuQ!R4H>9=;Kf?Ja) ze@8rG<%yMvjen}wK$8iqe>@0Z!G1a+4Vo)0LHmFzovG%|49Zy?m#hANA>Q~5V6;bM#vJkB(8oGu$l-0V zA#PmWlVA5IsTrc^pLi@c*TZF!9x++a^DdSN#3muA3!acstWYhVfbs>(!O@3>E^F1S z^Br`U2WPw^KP2n$Nt$+ah8%+#gz!FQ20apwENI_&EgC57EAWLZLJ~CU)1TuLr9i-8 z%WA*gpM1VB<)ix6^F?Esxiy;L$*Ybze5{PMxU4sf3IqDsVCI7bfKkCNnicFQ9V#d# z@%bsHsNhZ9C#RP!Mm*pcd={ELG_luH zgDg0_v6$jBs=5HrG~v~WyXg2HYftUE&sEgYyotzVlT*KlKj`?V;omHr=p1EG^KqP~ zLshiaSN;9yUTl*Yqv_NZH;zhqkIE|nmLgwKj^KsKY7YzC&|8xp7R}vQhHKbNapa){ zpYol;g#@ihg!bpUUfA?AP@d8-JL4@io|NXZAC*dql9PIkl&}VLSH>|F$o|zOXS|5R zh*z8B2f9j;qLUOn`vx*zqDwRg<35Q z+25v0_-L=7iT&)@;-?z)#bT)uM|(U_sGZ>Uul4g|*Y8LgrmmsU9svm&6^wtoQ*ATD z*+S+kKfuoAWX0lQqx-!(ieCJv^b6xF7k3;J<<22$P&?|pm3x;oc;la@BI2Zo4*H$S zl+f!Y(K)qOe}w?Z(0bn)5l08ujeXOgs73ZhG6p{bSS>P;(~G0FHxo#9IP zzNhDlV!Ug&OqI)gYziFw3tCS>ex;nYWwVcuA3s@*8Ey?28xi~XfWF-H z*`b;tCQ5LuII5d6zy2kN{JjaR==E81nO*D+MY2~efnE;dei#W~yA}GfwFtPCoM#ESQk ze#OmDwE?AXNgOVS>~#yno>lg{3&L0*iPd0AvH`#r#MW~^1=*((i;^lR1-^{!ZM-=i z(8Py5cb=>Nm@by7G)}Q*UR{vV%jO!7bS}UGal3}?;zo=TDfTMP9;Caeqqd10$U>)n z$E{Ev#=e71+bKr$buuxO?r6O8hI(WvqKck0v|X%HK^*4(o-nJw^L{XQb|K7TW$Vo6gv*V-5S#PI|I;R$A1rY#%tWZ@5F@nl%b*p8mA$+E& zzh>#2yUpYL0dhtGqLT4jw)ySEvo9698T+kUdf=jD*=sii$*Zy_J%OyaBXJ8|t`%G$ z*sRr52e{;QQ)c57%wgARZ|@KzM1;!v-7N z>0y^T8WjoCmwY+gELgNsHEGaxB^dTN1Q)%F>nY`8WNo5^HZ5u2^32vvSo_=;wYghd zR37cGC%)bpucK3=RfB*Sa2=d`XrLw!P2Y3?x+OR3m}$M=xkcB7$FboomDxb`pZc}G zh2E`C#Aw>NMj`_aTPp@SSqG~|w;l;YrO^|*O1~~5ZeIJxVLq15^!};lc-*Gq8=vX7 z-+(fgK}}l90NxMdsnvAEv#iq&p9jZY+dGK*{Zn?FDR_&~ohvvRAcr z0-$~=vcgnl!7?e|aHZINsq^^(l(`%r9Go(|YDjOBLGc{~7CDcCn?PIB<2o8?5<(Am zKeRQxD)l>R;n1kHX0x|mSE%p*BHcstAa=(C!yrGO#XbYZ=0UnBKngxcfp+pJ3PO=^ zVkQ;v({#FnNk($%IV*_12dxf#un0WsTBA^uE5;wxB0b0EJ<_&&n`t4&G>q^$83s&0 zbdjjt@LnQOhKs;x4SW}Wy2T-KwzG$eIZqS#G|PWzV9*YpJhJ!cg=CMwou}Be7z4f5 zD0nvnk_Rne{t^|B5zKtuR()x4$O?*ky_iMXn;Hp;*&OYF>cQQ-1lw*%>;;E+BGJn> zyDt9BRmK@|hUn>B_cZzUL=RVEeZ9>c2m%mb=;Z1=5btrCt?+Vcp*(XaM9lN>ndhkx zh&1lC?hnFCtLL0?VwlXo=NIx3DPGQ8j+aFT8GMW{PDVIQTMILu^P8>XHI0Dd=}iyJI-Y#7(59F{9^j} z0E}I(zCz4}L`IAhO+4pU^#CH|H(}Jy5HXL8_v+Tp3@y8}2ovbBWPoMgWvq0=Zz@fP zY~?f8^v{2hxQcI3-&Jh>AYZ*BH6`#>kmi}D7k}8nANl^}KX7naurWtovJs)vE1a~13(G$hZ;scdu%}NSgpP8wHi}tW{$JHG7n9j8H1R>{okO60@_F1pC5|qLr zzIYs&cgTV`#wQW*_s|grYRM4PBm=NSh1>PzpCqYB#G^Y0&fy!^ymhdp|=FNCWVgZfdm@S&a%q@+sEqydNkHN}=%+j4-N+baBOi*z;L4MQQZbQf3C;kZI#ouLx{J#Uv)a*b0mE*eJs29W1o$7X-(q(kEGG-H($N@?%MTqKlmGmBnKCL3F&fE9a4h8h1co!}_T}R_jIH)riH$Be2tHkGO=FYOx zq~K?yMSfcPoq!7%_zUEFq^;OV{2k{(KyaKa(Ps`Q;er%bpVK%^P?fPpOQ99CD0uc-)) z1h9NlQ?&mH^e=8Uhz2$kkg2#3WXYp@_bjn6t1)xphkKUapp@rH$z@aW&5=Nf5LQ?3 z6vZ)>*>R>c%xz>Jg?xbrZk4;_gKN?M-C&EB9n&k#1!iCxWomt7mGMpT^^y9^gyquI z7cvpyM%kPG*Sf9xi%)0JTns-x_I6a6$pD6ix6=I+J+XVOV4v_7iEA<8BEd{S5u^0t zqq>Q=IS_17dyCL0Sg9)X8}QtWHYz+D3oIp5P!I{25B^El*kME~vG;7FZYK8I4THP; zI5@KzHe6WM$L*-;VLe`O^*=OScQ~70ye~?s+E7ZVL~X5ETVhib6;)Jiwbf4S)uQ&O zJ!5Ol+BHgTl^C&C?Funs?^-v%d!PH{dGcS9lk=YUJ>TzV9aJD-@0>Cs)wyx`6^OQ5 zWxW(cZG8m`b%&$nLBztreHSIc5Ati&Yos z9oRXGc-y7D^1ay9>zxD69mxW0JbHjXzH0pudJhL95M-DaJAXWRhd4E$s4x&j}c?s(kM9{l$^mC>#gI>$>V4duxp(3iT7m{7wWq z2LJPa%TX~`F?2OPwOLZXqUv-DsU*JOPdQk}C-C1YkVnkw_&tNm9J00LE#~uH;RO?! zHYzEr3PH9udZZj-G>3@+#37%31y>|2#RB1|Q?B5YYbH zICqF|&zF;O^~{;V;5&?aK?wA1%?E_gnC6d-Dgpi zCO)a#$!ax7`Sr3KJ~hn54En*Kz6OowTnjb6Xfz?Xpe~-#gfR$Fx4&o-ixxaOPf@5Gv-Vs3ET-@AJ4Z~O)%k1fF#W<}Y3 z<=42O8^8(f1}&!QTd$~%y*<)#M_-p@u)0XcO(>-CB92Xgu;<0Q0;B)99+$&-A+Lb0 zhlK_rG}5h*3#K8{QrfDHnA2C7o6DT~n+VR{ZOE&Oq0ckRk-+v~?7Ig-#%%`%FA$HR z)+5LfmO)p^Z9WfDyt)Fg(&Dz$IK1pdnUud_QG6y}24ih^V#`F%%}|Z3iVsf%_B-V6 z(En=zmO^kRWKYUi?_>ovMAeSkH5uRIeF_2Nf47%?SIc#_Wd0@2ry4u60hzS zo$X@^i5=aEnp^MbnB0tk$-B^Jz5aYz3DnRHI{k5~)MrGMB>m{i7e;Zx&Brg~(RNcK zZ$ZHL8RPT~V`?P=N^ml1r!P790M?Qq zhi4A{C}Ea(M}CQjL&~b?5L3Ls#Gi;sBpJ_Fi1eQ5L7NkvsSe%@4^%j$Cr8At!kcJu zd$M>s_M+HS!v|1dQSps#-<=2oYd-$xN5FFDODB}^l_`o9_T;W2@SU$|AsbOmWi(#B zat`Kuc#Lz~qeYZQ&&euh$oIcnod9RPD|6lW< z{~R8@>1P-(Y*&2b8I$==pTTzQ83^)q`x3DFf%>k~`Mpz?242W#3^sjwF30pnbYVb1m#!G^}Cii!7Eh&@`LV_ugs2Ud+95=>3fz6`1b@R!S1WfHRCFrXlxB zRGYB+CC5^c7RrGUrqQBvljU}^7-&83dupQmO| zk{*#lOxPOop)9@U1on5NIxBg|P!Pf|-fmrP=Nzbo_gTpN!${3)Q{CJPdGPx^tpKEU znLM$JBWvih2v?_J(>!>N-&~@^;3~HOCT6dRr&AtpHQH4@e zERtMBE9eem;g(d_9|pJ$MvM24G$$qM*yq;auT~-<&q`SOmlbKyc9YGN*M`FKaD40N zoGWG*oRNbp)3A*m2kJ^b86=nfYZrn%^Z2#aHO0hQ!tW9tmSr z*&SV}sn&O|G$dj>@v46cxdWRSv$Yz4WnV$$F!hdiFqHwvJnJvWJmfrBTVF=ScOg<; z4&KxcsNr%)$C$ixy6#(=!kAR=O2&~a zMUz^193+7!k+}HG8)}c|f5<<^zrZ$Tovba2oHJbD<&)w6w)C&5aeIuq%fp|M_HIyq zZ6aNlS0TvzH5DzGML?p5?S4k>2ihRv593d*xin|cUWJ4)e$2bt)Z>{%)X`WLp->R& zJ71iKi?Y52&%6C>I=%~^%&p;u>#|vIES){8Rnb>2JKW}A z#}0BfSeV@HOCbUtmqt##QdRbNP?h#)X^)shYugAkjP9nD0^(V~BFQef8bQmJNCc-N zhP1taQ)Syx=HrVq^^P*H<9iGuktf#%K+2uJtre~(I+jW>=;}@&B;K#<{SSso&A@$a z%?(oFHz;z^3px^r=c3n2mgj;OTWw+v{^`j1>QG9-72am-r3W*bThF~Cg#{<5PbZ|} zF?i+-0QBll2fx;#eXT_6^egCvc$eNRqv>+Mgt-jU;%j4YIrvdvpt+foC>^Tl5F@#B z%WKDaMnCzThsP8&3}8Q|M`IKKD+RF&|M`ik-_n`3{=$ycP!qf1t&BGANP6C`pi z?U3VA8uONsl>*XZ#tN0gfIaVt;gdL0!D%ONoA!f5uH*le-$@DNj|3R(-0zchkTY^TH;e>B}eNI6pe% z2M8h@$v2vS^-42NmcAbdj6>Y;of_i9@$6^UI? z?mhJ|08#>BYLwuwS)L0 z<0D1g^bAqPF}tV1PcCy!+uXNeWrL0AV(Ue*Ej73$`%3WKp_+OeTgG9*lk?xK!s0f4 zSD5QoEjrRjULm&7vOE&dV_78?B=wSuO!lf_N$NH^zf8i@j7A=IoKKZuMcc=!lq}uV zMnU$;`e&Y`OGfS^x_ZVxcD}6)F?;xY@i&OC?R~{Dul){gHX6|GdkfsR@3UL0-F@RWszvgy>f=f_Woar+u$nveQ6QPBQCUP-%t7yGq#9lsS z<4S*sEVFW>mNYHCc(djaJomMxV+()4NsBHy0a)ITy>NaHzm>IXEK6_vR&Ne=>_eHv8AYfEb~!ncRcD=*f2SvTtNhv2dd!6s`9ln|>8e<-@ z^L{h9r+&=A!t+t>$JKXz-uQho$lqwF>t5mthB`jjk?!I8Q+VsmhGy4=%-Pw>?3_QP z({$1!LR=~eDDh`G@rgr z&2re{J!V$0uVs_4b3ghC~^Q*BSS}g?0(Tc4l9JxOE&5`?a_j4NEUL2lkR4KW0|6 zcUN!dqVjWSFXhUGaYzCi)-BP0^(K7bf{N89Sh384&{QSt3wvMiG?wQ5Y-9B)%AS4c zR_*rYmLMmMjpxRyjF-+XJmfOW`jP>~klbC>&-xw%bctsPfJyEjx;atA$&GpSU*S1%>D;gTw{1tS+ zO4NEkOQftd3Jx`M#m~n8^j=^HlVc!Q=*x z3{H*I{3!o$<>62;CSp~PvyBM$Wb*rp^r>n4)1uix5`bDDGI#{rqIC!UfspvM@>+-m zM<)G-x=iBBdba!G^V!mN`qDp}k=Fhb25qHku0uBuYQRBp55Xk zW~fVg;Xm(DrB^l_T3<>n<{q(@%TbmpGAgwj`&o=Ww$J%|vf-il9ws%?oLBXO3oI6! z`}c_1VZtb_5VF)i3k!XCYi{;e%7-a}2p0G1sMpW%|O)18TBb&Z26#DO`?r$_>Q4ROR zFfCAHeqp~n0ToKwy>&pJU*ALE39cfx>Da^CA*2guWE!xx0NIyEamGEaurEyc(D~;I zu%ErP1m<($K4JGuaL?3!Yw-T9)#^$f{N~%&JvssR!_HPgd_l3cL1M4m;|A{j`WSaq-tW_olub-WJ z>832)ZNQ2I?C3s-M;tcz;R$=1nxNzSOju%g!Mv`Qdc2}bF1@cL=#7?(@VIAZNx#HH z@FOHLdLWEX3@+rIfH*vD<7k?Al<_Jy{r!Ez$j+q~4$tyW`rHt$XUi3Xd84PNXKW|@ zJib?~bnyIt{wmPAZ%3b{w}u~IwC*NynR|F!p!Uf^(Gk;cu=R_l@v1@t-`m(%K!07> zQt!ZyWIv{2EoIEH*fgKTr|?dB_-XiThtu|hU+sOSS7TVq?bGdf-|*VhiD%iK07P|S zqTZ)_#&rHp|5|9DSbTm}x`KGeb^qut4EC}3a)f9?0=%sAM@4t$!aqzxWc?bw9>e3^ zdB_;{))ubwHntmCgE)1<^r6+tpEqf%VqXw9ENt#y*PJ-yuNJ%6BFh%11R;L2xvkR# zx!nW1rid?s3OlJ^jE_YtaA^u=;#UB^jfD+uuB@XH*t~kpK#hw!rm=Uzy)u&4T+ipOWLMf~CGWPYD92V>OuU*tqsxje>x!u9Dr%qdtmER>>I?43~e}^0S zr^pK|;L#H}@$pM>fy*`VqML8$vL;k;C)LZJGoDT%yM7IRFsW2#=+-+#Yc8-A2zp8f z=ge5G2#Q(Mw))t%1GgRS%4^V1*ug$HXjvM5?EX*AUCt@K{S@r;lv+9(=*#o(E?`{m z%$|;i7m`eKEZ_B8&2(QN3-jxuQxNiFxYFWn-spOoCC%wvLgG|Jk#cXqVk(zdi{7;; zALR0U{d+>f%1L)^&6^q*iQF1{iH+&UJ4Rt-(x4x8waxhoZd_mKs_LFY&fWBjyWkU9 z2<)AN!`edS`NFL_>`Px*0~R|jAu=>;+tcmVsIQ$|wK&BGanbT%`Efl8f`@1(INlX# z*^8^?^0s*dl&?l<$*b8N06iKn_PT|3$XiY*rr=SOpAc#zaTXzB`Qp0he`50qBpPiF zvnV@Vxw*vBi4l0k?iNc}il+Fpxn{izHbFS)``3U@P#{C_VxI?@7&}e*|`6c&;t~4t_ z%+Mr;;PWaErD;t?B4dCWuFqJiMVx&I6xw`3ZNPF0AKP+Zd^q+Z|T(x4VyyMZ!vGkzzyRqtFQY8k)`c4nv)C<6G z-p)c~N~rU?r^=nro=W-HNomM8!P+~bHpbGBZD%;#B}W421Doeze!ybs@8gTs^}fm0 zGu9KE<(VU4S$}Ja7m`c0DHSSh9J<-a^*P_NTHJyZC$%knDFs)wKS{fAU%BVg2Sq$v zFtMxcloH_w2--$l7dFh0yi1TV-Ny*#qS3sK08@dRfW=4P#|ei@bv76IV*tOU-mlEi zy(wFC3xnzTon^3}=$9qzxJBg&d2m+Qnu%@a@^uFlW7sqh`G5`kRA zAcdkTtYuAh2m#bF^-DpW{}tw8Uh*dMe?;0(zppdCW{JCj)LAB`dfmIo3KoS(9L79c z$@Zmw)n2%2>tp@s*dW@p9)GVL7<#`#fCMhXAS#PXd=KaR>D$72bOGhWMHLnJ@9T%V zXRn3>f~;PSJ^*t_#xI_ywjO=G!L1#C72wD`;iQQNESmrRHBXF4wH>g`C-}UXg%W}- z99`KDVkBUh)`{1mJ^=(tPXzG$GwJsNt!Krx1YqJSWX#lyXjs01bg#WPB}Bc!th^=0 zifh(qTmT}HZkm4RpITinQM>golYx<5)2HRt__t&%MAj_12#EMiBc|>WbTjX-mE>*#dcdNT=_4$r~)rjuA9vLXQL&`E}Zvcc5wtu{E-ow3Y^w`Wa_W?v?FiufipWYN0C4~`@gO%&3K-`1u^xs zeLd8C8ssa`xMULU#uW4kAk%tw?_YUN0{*p?HzMVAkrldR3Uil{FOBhLA?&mIvCE%X zIA|w?he62&yNI?YFUpnL87_7%S0fw8Zq&~uAOu`UuyXcY-zfuUeU{x|qdER`Gv3x% zx_(pI2av`uuDT3*h+GvMuxD_(kf-7uJUi7fx_z*=icG9gTi^-&U%%x1oNQ)mwOg_^ z7YoE`k%br+FezTmqYf_4=fiy-yRok4knEH>R~zWN2Z&IGY^bqmR}e-uRscdA3)SJfhrbaNJ!-TiJL_y@6APKZzTAGe{cz7R%3mlo_H zfj(oARCp6zRtcXo4gZamjrWcMK=6e_}QbD*EdrO64OwoCU;Xj*o?01`6!X!odd z+=;jhUJNNFSzjfg;!Ol@7RzVH!X(gv#?jN+!hJ5<*}akf?tu;XUbtn8&wZ{^myjjs z922>6dVcfwcT2guF2`EHF|s#zt@><;_eCo@dK-dM$;+BArnqXUq@*WQBs|S7@tv zuVboG`I?z8X6%y&TVq~j`+BaG?6`)6tWAQVBHTKgWat%j7CKP_Wx=UmM^EtZb4csC zgy2{6)!iY79 zrLf>1dB3OYS{&I+bH~K?v3yd0>l?XkfmNNWat-r^g4o>ks$?9W1jwR@9-J}?n&wE$ zK{OL;Ohi(aQh>eC1s^RRPHlKTTL$TMeNL=N1%}FUd7PQQcrNHU+rf0vs_p#fI!;{> z9s%?JC3?*iuzd<$YLM7WA5hB92qzT|;RMVy{q>X7X+36vgxrEFS2V;9 zLc9>Q$X_mH(n z7%m(vTi7nFZ&?(o$4u9ldGw%I0_=GXCNEZ5VT8IFhr856hY5E9Gzx@`xaRViYOIRb@+u5ef5bkf2#| z+<~+yUmF!A6%9UtdSX>8o;Ul1kHRNGkd~SkMek4%deLc`mFi`!spWVA>(up$k=e!y zmmK`I;&vq|Ih11q_tfK6F%7HD_uAe#L~dOeUDhE-bN(FnYh|&S{m)19JYTmdSf?{Z7oQ?aI#H9HFZ29{cY~m_U-p zS%<5afsA~O(YzbF1b$pe9_XsAn_Q~)bu7jl(0VJdtGS_8S2-e&%&Utg*4~(z19yAxT=DJiG)*`*&li{U;m|DZTL*JK*+HzEP+0lK5{-f`_ z!I?IDRMe*CAW7y?rW>4L}qieK7s<~(%K-nXJ>R3}zBB+DUD@tOLOSuS-JfL@O@f(C@wqi&4Y z=e3)hyLARh`KR@V2_Rbb9OvT=Tf)76Y8N{@-+@M#FuMetN?I2Yv#m;5awT?15c0;9 zLh-ryGvDonh*nu`7dZQn`Mmr{dcFd-SgmF@hJ^_J^EM@m8;>u_kd^{6k$DFs@}8^G zRwZ88e)7(9Ih(o^u(rX)^9&>?sPyip;vJS= zOvRj&GDV#B2VD|e@%2#>$dm=ru=WrDDW-pgaqfr% z$7f;VYpB7jVbc)Jd@%Ugjqrt3l0t&w6X=^z36;{oP*Bc#d;Vk&VdYS(V}8*=hz7kE z^1RO*%kH}A$zc#f(g6G=7S}cw56@R$bI)e65v8v#ung0)`W?4Y-`^Qmuos_yz+6EF zhwSmrgWuUCuDE!*yoDcaGa$PBemOI%X~ik#TcB7xUU;-yeNDJu&ph$~&iP8t>otKs znUSkWL?p!e!eRGo*pjp4;dccUBLfBO;8@D}**pS(I0ft9hR2#Bgc=u7S>ERnv8=e& zR8dLLcHztaRN=519(Ehznazo@k5M(kIdx^+ICk(3CwOHoWWKPdhID!vzQoMWJ|xxu z(VQO}?Pgp3pk~7UK{UeUO>>!^iIQ1gMI15}o$c$iWBl-$wlS4W^zZH4dfw-Em~oHm z$Z1;l=bUj!aeI-E@yJ9sq-Rym;}-zMEIC!1HGM5EDgw7U8xb(r)IJHiiJo-?^3m+* z4pq=2jjurH^W(~oG5B&N0!nS9#!q1_@^&?#bp6)fXD)gKqQ=x?Ms&(=rLKPH>Vyk- zwL^5qnhA%Ey23E6{$ICeFP26G531httd5VlC1;qMS(hmGn~)3br%nIA7NF&?-xKI) zFQISsc0%tfRk=py&Cw|# zuj^3ee*E4Z22H+D)kS!O>}*a20gtpWzsh)8w$xLD%*$2HqDGA6bJ?&)9)G^3vPwGs zaRVR7VN{_QX8?&9fGYbW1DuskX<&znkO+KQxE5Y3WV1Ty|I0=~;Tu22W$OoU#&-z$ z!!A}Fk7eq~<`24=jk|buR-1iu@Vb$8-XVNetQ|)9x*aqjtbK6C=7oy6r6Oh^{o0V~ zGlnyaZr*CS@`<)D?1gmEYphQ9V{nRg?jZ#Wj>pA@a38w$9@23=U&k;E1_I3y#}j%v zej8s+0Um#dB?)sOkrULpG(5Hde@DGmEJQG-EFYDA!n#o;5v`t(XpZPVNz2!c(thz~ z2d#R5JNpH6`;>9>8MoMq!PjlvjojB^WpqTzyTEp)D~!V`P5+#Hu6INbuBK*e2qEA% z#~W^wD?2_iAve9Yg^$?b6rBL+%C7BsW$xCPsFg2t^mF1)Rq;d>>n)cQ>^Rw*!^P`u zgh*L1t%VzPW=Pm-1~O4o=KP~%{nl_ENx6qZCL#ftK%zZk?TgvvbX|k%cpM!G8LTD{s2n90{{sm)dR`k-Pvm`5EdV@-wv_}v^ zCjkH=OGn~ghqzVk^JQKBE!K=-*b~xdhP@Qa>v+!(8&K zPBNvUaRF$nPp-cfecA4D1yeMa&*xB&jd+yQ%w*vzpAx1Y%|r}92`(kQxm@!ehwLws z%W%s1@n(?ZkL4*UD%#l*JFhzAF_o+IpNZNm63IgTwM&gNtiM5w7DHO|)LU*VXzUl+ z`v?u1v)io7z<)mRd>-Nc*eg?XOlleD_9X6Y&H)@gMo8%?hI}PFzk-cPhCXVx=hR-| z?AHM=!d6y*;Basi9b1^k=6Oe+9B#Zn__+PkT6n{o6uULbNG_l0;03>q{a>7YBihY) zJLmt71Icdp_!U6E^h_%Uu|9a)V_k78G2jZ`325xNb&g0`uQI>%l<`Pc4x1se7li+Q z03mXtLTV!t3q*=qTQSD$f4Z2$9lo7z)xwxy++aExctG+c^z4S6zTFUA1|pxc6R`J^8`wNn#>FAlsp)d$n-$BI8d;S*G9Tkg%I|uHNm* zjaw2bm;O*9PbB~qB-CEKfHX8=Glm5QhID_~Jk2 z1&_8S3xGm{{mt^ud3^BtOFP$>a*^CvGE_y+bnJmrxgca&LoCoUkt^8}LmE3=iw zQ~!Vd&UEl=dVI~9MILIuU`!@1>2#n1fNHJED;sd62xGDL=Uc%!V(QiqbL=$#qhu6{A zGY=YLGciC;0JP`#+$b$skt>${3OTjtEC_$f(*TFF;#_n97t5V-DG+^*Cj4+9=ro#1RGR^f*iJM{vgUFim142}!9_KYeYtL=+Gq zKTdUBgM%IF(|S5e+PO>2*#U;c0y+sKyS~B5WX+c+1Pu&JQ4*%_bo;QSi;(Zke+NPb zU0#9N4GR7$eEXzE(WTFBOq#j?r)r=Jdb;8;QW$=k#1 zO5Q~K{p7s`-WfVK3JV?eAAe9Q;1mE`7U})ytUR2zG~Fs%umfoRl?5jG+q>(QUtBR_X^XMEyNL{Z!!WT#rLzpU2&2Pc3d>>*&rmk;+a>2a8~J*M^K znxfORp8w8}2+86R$Iu|~K5E%-^R4|Sz63>o+IPM2p%pq_{>IBF)j6yE8cu*nSk+7z z7CWoK{k`x$_Pd%P60}|VCug&AwK&~b$OaB~0Y)O=Pk-id1D|1P>FI+D9;5YXkKev< zoaTE78Co6r6OvC8haBw&}<5@mg?Wo&*YNWGZRVK4jw)_e1{ znbPfi`EzCO;7|$NRw~{s=L;iGABziKC|7#d@x6X}?IV1wa&@53{sx@{&r^mXA>hwa zTbH}aFeWg5INWbzO16m6t(dY&eKgZYBsNo>xFA$s_S2 zxL9jy*|8Bdb#T@$|8?bq(o2f?!)C1BaU8O;c}bSgi*8|Uom$Ixr3g5(Fktm7p(MOdz>`T$nu;SOel@Hol`4?Ku=xWD@nem%9WQ`$ zV;BFXT<`Y%Z44X98&MY_S>5c1g~B|TtVTEq>n=;N%g#A9jEnVD2SFSRrW2j4r@8lb zKK@Y2oo|hyrYczJ0R7AqYu=j3g2=KdzS3C$^hHZl~J1;h?8AS>FUGIO+_ z!~N843p7quwdpgKGqe2886mY2<3Gk$~%` zyzNnfZS3Pe$H|pPo5#oXy(`oFsEm~$a(x{U`$kp(E-_%GNz5@|loq%0dvNF(q30G8 z=5XKJj(N{}m*NL(TfcJ@;uGEeWJag4J6P&0%~goh@^t7J z3RCI-{!8MG5z-=p+)af*0<}VgB)zZaW{= zhXgp){Qe!n5!M=o7tk-`Qk=F*IzhB4b#$cHrsCTlnzpPBN7ReWTMJ+hNTfDyOpWc z_D8}|O-9(^C3dDqZZDgQdo-jZloVo}r=m@?yVak#0NnABJVr!io1Z43$_ph$bcd^) zfDD!oM*g=T_hMvB6QDnwd&lz6Jp8)>(yLzSi)#~KW5DdLe>kCM(Lee3O<8;i?V;jYE=em zv`VH8=S_6fXbfE+5dxBjpA&`YJr@#xh(&&#qM0DQ{w2FSya$U#f2C&>NOYL@bdA&Q z-4}HFboHHpx9|Vw2WFnO4)ZP}nz4tLYzJySA(8rVx8cK==bR zW4X)e|HGzVJ3m5b)9!QLV!sGBrn?Y@Boe$a@}IkY{ccTkeE?ei#(~;zDab;8FmgV= z093U_sY`fkK>PPpA%^bmDe`s%SPWCK9l}ynyLpj0s;_(FR}ne3UtEk4g~a^GX9*K4 zAQJO>M|Jf@z^3qXelT8#O*&i^q-7t}!xqqryo~J{-sy%RPDNKsroAsBVz&sNgzn>#wxCtEF`tW3}lA zJMMc-#uG9z3|-R{Qefin6rLJI4V{^ry(h|vhk0>_>@9h*GE%!phE17?SL8077YZvs z%dc9uoD6WAKVrE9S682v>0cF>;^=!`;$BC;D?2nNFg2G*F7V^H0*lPr=!Nj2&X0UE z?kAjKWQeRJRr#t`8H7ld41JHSLu4A>R2B0dW0zin>*w&}I1b+WXBa_2D>IT-FyLgdt@H^btGvzf8uA z&XtLfD6(7$85FNwziC%jQ$vs$e@`pAF5Xat<!U^2wkhJHb|7i^2jq*3NK;o~rftnPnq<-rl!Dz`2=;&a8 z?rF1Gas?-lcw(}{3`g%!*3g_Q!3Tz1)q($txm~r+!F)eC097o_VRLlK(l21?I$rQ9 zZC*kwiu%YI%qF`g7^_1iAu_(=Jp-oSSE?Br(%%?%VI}#xWO9Ag>V$6E|1G)Oau_Qa zh}1fri;>M;&Eg-kQ^Apal3)YWOgvnu03yQd(D?cFfh-;kveoHl!WW~Ylpc^L3|lT(ZD_&ff+Xwfk0cxR>fxdt^sh^!Ku$2$+| zS_^^{!MWXQjx#z0osp{&w=$fe5IdXQe&O82v<*BmI3vPF|K;^BhwE>G$|QEZf(Xen zo`Ecqe`~0@$vOcn2c0-VXyqOS-miLB82TnfphlFCdyZ@&Ss{)`^dB!0HwQfT`7TMe z_=Sj4fZ7?kr!0=r?mQ3f?Ug0SB-8VqYsL?R&y@>9 z>+|5(Uqa&X(N0;kV~yDYMSBW%TsxpBgfn=^QJvdG+J3ZV9uF5_ul?EZ&l!lxDwJe*Q!o-l-~a4C1Y=#?L^@`4l%bO7z6)p?UR~pVI;{Y$=nFS-e-!ZdD=-ftJsP( zP+H&s{>+NYx|lCpmSB^CGz76oM!oasi_1#k;0ggCyb3d6U$+hvdbhztyTXvA+C9>J z%vD;!V#nT`;`OE-n9U~gHR7F{4;w@v;kzh_BhNd*&6Rr=mT)~vX*Yq#?T5EfFw?YC z!?*QKgyDU3wY5(0id1g?w50|*XynB8$|}2^xnG&R{NeQ8qpIrFwOWxLU#&Y7CMePT zSOF{bZ@nNgizPCPopEvb7O}bI9XFbH~r3CV?&eu??=@zuI*5h*{#rsN&UNQ`tO(xq7YDf};O za7|gx0_no$h>Gt6DUrq;nxlgkv#AXaAWIdtH)5T&PBRQe0vRV}iwkU16p*PaG*aer z<0ruf+spHf@z$@m(B-c$8WK|j@LXXnbhmDI24m;=u;tv8kKFGn0lm-gPw2^Vn>Qo# z4{ZfRti!BqHaOGfn!nR!^U5ySGlEkRo8MrK_I_kK5znNPWwO0}4!Iy~`zM5EPJ_1Q zH2}S=%zjxwc(yCVA6mfQosm?tW{O$*mt;Bvebe^HBsGL7F-4<(AZPZLK=ug z^o#=4=bZmt;sOcTF}r_KQRK!{*Hy2v!-cVn!q5qP{jhUSm?SAPuJ==n7Lz#u-BFX< zSJ4?prBwIz&X81?+XU9gYdN)byexG_^zi>=)rPxP@QTJ_O5~kIx3T@+Cm$ar)XL7y zxNE&fX>|n#RQl6VQ0CGUNIDh$xt^Xr7#yFiu7)CS78lL| zHo95fex9MjuI*n529r>RYI5L#io9EfE0UW;qUS4{2(ugaI~^kgyHMmEQ(Z8(mxkJq0*_w zWo?2u;Op(GbMeuZRe}9ue!tY4Ew5`rqB}F`*415Ifese@>|S5-vmjI!>X=ypI~Rgt2oj}A2f57(LMmTw zs=B=~@jjjR)I`2)m&Ybq{JX0Tqh**2>0Mune|q@s^_MVZKuH753qr1KOO;hrUf9ag zjW&rNelG^1rWsnmlk#C15?G}gn^eEjb2px3F89Y>6gv0fy0C6f{v3>2n;UQdn(Xx< zj)w#@XOzNTIb{ct32LPGN@uSofgjJ2*Fddt$u@F5tdf*9{^$2L*LaN{>yW*VLT(=@ zaE2w4cxJ_E>wC9dhQP{|Vfcu7i=(2;ZA6Q}(1a|GYi(3j5OTqNvTTRawLLMUHE9a( z($%9NPJb_%l|?7mJh$Hqe0}czNad)Gue;Hk6GgvfPlarYpRnxk+E_83l52( zAI({4qo(#9UnW!pKF1;j^jU*J49aZjyUv-_sSOsKw14ok=bMZl_8|?Nsc`+ZURcGu z&WLkSxmd5oLwT6a4X6v7#6n05*D{b7+lbEcc8k)b=%tb=s0EZ@hQq!fdH%wc{IVGO zz2LcV{Q-{gG-I-@D>*yVR~C2{mD3@?S?`ka=v+k|B167TU@Y(;y#arlXPcup$q)Oy zpvwZvu8Fhr*lEg+Um@V4fyNLH+|6*xC1@d;@ZoA@7z3|?I`5yl7dRK*b)m?>y*v4llvyC#a{jWJSdGt<>MF>);)mAG9kf=Dx z<8e3sKqKG5c6wFj;KD+Y4noo*56*})8lL#@YQPZ!WVJBA4)wsk+t+-u!*o{=OXS2W z_>&@XCIHot+GK7|2i#k_qp3>^=`mmTiosNbUYg>$0^k)7qv9>anI!a>!)B2*j;7ej zruwK}cecQT$DwS2A`jOWPW1{yW&$rk$Vt)9AU`W1Qcm1S%`7RLURu3sWX7n+$Wpv` zB_Vjng?sS{zDQq;i%t1|YP!y_rkbX$R1pYGdZ+<{NbiR#y@w70f}lumhK@)TQA!L1 z6i5glAkvYDbOog(3ZV-~5hOI}O{)6kd9UlskNi1%W;T1yWbc`sxrg!@8YkH`bQ%6# zD_eN=o`^Zq>*6&mU0PJnbv!6g!dI8hc(e2fPHbrVA8 zHS#MrmhM@_B=j-~QNfLq_cY?IsI!(lj0BCcZ>(+V z(GMO-Q@U5gA`YHiNa@5eLdq-Tg10|KxW^jXAqCt=LW*13h1naT{}x{rf%dFO9zDd% z5r9O6HPbX{lgfjW2j!CV;^+ss&?`XWbXoO-AWqMSZtG|{lJCu+TP1{U`fpoW@ZCfz zeM$m16<1-7wWK6 zdW(F&W6{C=GHN~~*$n1SU&X5RZPf22xQA##p;nigaZgRM3X{I0|p)}zM#6oXR}0S zfQ6*~#8PpAVX*XvXT!qT2QJB`lz^QGWC8 zK-195#b?>CgfAJ5!j90VWQsQUM0Z(Tth$tB|C(U@YN!pLu1PDNcUY2U5bd7p5Atb~%^b)nJ`6hiTZ` zjZO=(4FS;|Y~6ZHsU$U4NiMmGz~W?l_nhQ*iN8f*hVoU?tX%e?+oy$#tH(p#3nw^9 z=fEsViAZu^%u%6qcFWPdT%9QqBZifhbwESgyYq{mXw{pg9s&ZRcDlE>#3`o{(kMPn zZQ#>+$TzCH%0DppeYLEub7B;MXQmU`nwPk9rtqsU*_nE~hxE?NJEBP*9v5Vl0rrgM zQpbQBZ^^-L1&d9twT!&HxYM2Q8g!9YK}8cjlI0q0iuL(fXbUsOYJs(33)izYI^LZJ zJ|EPXWoY+j-%i{IZx^BVX+#?Z!mP4x!bJT7&(+5_8Oc@!5re*oM=-*9SOD%c;!VK{ z$pmmj6!n~S%ZSOc_TvEor|B}LhhGI=3>Iu9Eq&E9Y~NR=bNwJ8srdk=A*aFDGUF1I zY`p=Y{oWZ!8)PVnT(>qkJN{w#(CMbjOzrR;*2Lt8s5Ii+H8B?)aLLk3@F09@`zk(P zcbf3sZTUI{7c=ZM8m@KMwA7c)=0r*ZU!^hI=a1tp}8 zU@PDE&K^oO+#6jq_#ce&w$R8@ie5-2;rz;{rmIx2MMvKAP2blin3TX?JR; zVyNR9`&Sw+q$9gu!Q}0R6}fVZi3uO3=@N$z0^uP7IZ>+)SFpkpu>PLU*^jwB# zHR!c*t57UW+cB=2>Br+%rkY3E8{Hr1T{18ST)qc?Cy+jgUbp9- zh;0ZZ+csAuin$7;Z{GJHmvXh=rGvpM{Ueme z+8)>5;)m~ktO=8Vg`ZK1&sZ9! zv|o>LIijqwvkS=P|2B0Ps2MQE=7BP4&+wHM$`wRGHH1P!gj>wiD)^y<*JC@Il)pf~ zOo0e(<|F167mpykvEg*<@h9-m%q&a8_(Fs45E%!7_%+hB-lH%21!C zDh2UTsBHBl?*sVb&7%}E$^cl+)PMnAOi9+Z{+XnMtp^VT9x`ie&5MDG;kdFyUIxXF>e`C1l?>@Lpm9D6krCZM<(|$u`kXn>w6}C z@g?u0mblMl)59UR#Giz+8X(l`V?n%*{QV0pqz+1gE4{2r*k<+dR@R?T$tiI6^n)=e z4P_(2)T7b2h6r@F!c``=xbLCnSj}b)7LwxUkQ*4RLTae3z&)ZSax9_Wr4x5)IX;kK zMa<|Q#xPl!3K*phiBO@qo}FcbN1GIRCsIt@YQy!)X8*?LHpSg`yb z_N_{iE!aii$%d}B+Puc$<>G;H>cte2R zFqOlVPAaH-G9cC%elAQ7JodEYWF(a;S;{sD6*20Cd?cfeQpsv~<*k9=yRvZ+NtoM&O zbxtI=d8QFiU27V_Rz9Pe89KbZOjX!Ewq%cN zeuiCA=r&r@c7#g7BT6Ul_k`ZMXcYt4ioxDA`XOGHao?q=-0(l+!_>D4wGEbt z`O4?>h+7CuAPu`4-|fE0WwcYUi<`V&&CI0G9}rYtgy=B0 z`->bY_~HJ?BZoA7mA2UV*^7t6tJds`S%dnAqb2 zLWG9-2Yb*<-Gmx=sjJk}RKz}V0$8&Yf!FPjX25#j+>!O`! zf2H=hFkL=`BRyjDaoIsEGqAy!hFnro2`VT5$6VC`)z~yJR`7$~tIkZB*js`7m4S05 zvV1zgsH3wDF(n55I=kz~9pSb7wZV8s$oubYXLejpDDG*Iom{f572ItCxgHQ?U2pvB|nqvVD<WJjrDXmPAyw{GlpZY`GlR2wyKeKs{kEZcW z8C}ZvwkosLLxhUtVIXes72cee;AUG+^Mpd(Ru-2A`qGhsY@83vkixt{gk8_P;_5)O zwXl%~=*LyYk}-BhfOzT-C(elU3-ncnRKOe`ghb#Hg?^fMuG6GI?#Hrie@A0(NtptZ!$d4$j_uuNsT6q{^;k#n{tSUi}HqxJ` zjLgBr$wUY(F zK7x*!UE6fnTh^kLD*4oUBuW49SEC_}S_;1eU9x@ZZT?Ah+O{?zOV4y}V6q75TPG2E za@7;IBo_Pax6IVwUVU zfoBltJI9wdiSlGS7_32~hZd}uDzvB*F?|W)pEuX_O2^(<6F97v=b{!CXazRok9w7! z*Ts7E+NRTH@};xzsYeX|tZ>l4?T;wQd}#ZZ%mm{1$#D}RlaVV7m!CY{oVcHfk!UQe zs>2Eg^_e(OXD%sH{{Z2j*qHDTWxP|3(A-$+Q#aRd_3uBb&-o9V51YtR zbax$!7h*&dPu3+#rrKT2$mn9E`>7AL$*&zh7hs$gszBN+z)2T49e~)jtl`#yrp>hN z>3^o>t!PG!_a_G`QfkJ{r~stNO^`+g&NC_g1b}Hlo+XbkgmBZ>LUv|&~%I$ zC!2ey@;RQ)H)X%Pd_Xz~p)TFrWTKR)`hY+K5cZKWq9ZG@`Q$*CAk-D!!C0+ln1Sh} z37;cpKZL$QuW=2zL(S~Hc@@8)&VtAfM2XQAstj#=hTwyr6$>=tH~7z z{0VU6aysQ@S`xmb>#YjEAi~ln5T?(@x8xd#GDWzB%YJY=`K$MU&4V@pZvr}1TzrII z6PaFltTNIXL>Tcg-sOOLYgfK0V~$0pWB&AvZh8kpNDJtk+hi@&DTmWVfj@m4LBMql ze6r^LX(E4{tJNFr4CXHT*?M{L-$vP#MbuLr#y_YLmTH9oXjD4bHdJ7o(0#;vku}8h zhMqtVOy66N`Ur}N-)3hjubx2^1yxmUwKPzMWfpyKwnb!R)d2Hj| zN|kOOK0nDFsu7W_xh&7D8V7F}>8`Ku9R8){JHAERwVm?>9hI69>pvkc zZ|H(Jo~&BSisMO9cp*b?(Q+E+n`sp+MX{F|YH6&wLZK9WD}B@S%v3cQ1KEu>_R(YkInjiB-fIwfEae1vcyaox%q&Jm~(S(%M<(fdv! zVLjHJ>hnf~d@dT8Mtv^EX@6vE>6gYorBXJM1wKt%2?xSp@b}y~5mJ4| zNR>O^`S3`X6x*p%JVt9y6qzJN$-A@qhQed;?F^>5m`WZvCVtYY4*sh+N^o-^YeZ+w zop-F7bA^2V*s^Yr0TT#8w`(7~QK89mN25X=vHEcA7;@>tRIk-p^>LkJ&zhiNjNGEj zDEi{sH23Mqq1EDO^_7O(Axe=tS-wL4*%RmbnuUcGy8V=6yV&#)ps z-W+zCvcC^E1@*ng+~U)R9WOLVRIWO-lC^G*kC&Sg5q#?3sM*-riikN{n}1Y({8VF6xqli=MfB169W;t#A z{1pm{P!QbLVuH0>twW&WNAPMrPm=5DqU9qsaN~^%n7nI!TjY+l88og(Ljt8m;g1j;gB6=hny z&H^*skgCr93n_N|oAuqc`6_2w_2ZaoFUd8sGxnAjM^3eRyEb5x0on^&i&?I7XmH5o zJYLhWfLZ-169xhU3!mrL?=jSl<8+9ecQBu`xBaRoSDl;9eh&n|y;jUky*BJ+8r|rW zS1A+#f8B2qf92z?W zuD$<{_eLPrsj>PifU=*&HTtN5X$;Hf@2yWVce6t$knW;E8?m~Hn4}nd3F%g4C0+(- zQ-(H`&Xw|AHzNr~zz#o?Feeg7)F@KlBBe@6-pNs`X0F1c={^3G!U(uGgR$os%@j&S zl}?IE8d;>YDaqe*#8QKFE9N->f$P~g*CP*dD1Usy?&yoX?cQVkl$YUXT&d9nKO$L`zxiZ)ctLPPRzekB+0&h%77Rn)I0f!u)hSA|M>Jg z_8J@?hRa{U zKv%zzoYDcaYwhDg`*sT8bOeQYggaCh#$&YJzV`ZgsJ%T9w^cZ?L=?<)`2+jyAFQeV z_k(T4Ht)Ypml72ry7v94s!25$eb<>2$hPe@AUF5;#+`;^EfREK%V=-uN{Ciy_@0T~^`5w_{g)n(-%=YnRy|v)T1gmX&Ynf5kECbjY-C zGr|PyBA45PYWLQ@xo-P2qKQ%ODs@icPv@^cpRa!2bbDIk(bGahWKa-n$%Xs)dSQLt zdog_UW?uk&p~qH^wZ%Io({k*%Ve(gB;hyhpzK2^fOO~gKe`ON?%~N5b`e2ZRkpW~U z_fF-s3EQ?fO-OIF?wO>1^qWe&SZXY94oo28eq!=)i9>}DJ@_%GN$r1lAgLC%h?9)< zrCNO+$4YL=zN}(|V4GF3*y}m?R&Z&~)NLkA>?RuTzf*X1_+j_9SGPiYhcA`T?CpNi z8&9WzO1f(;1R2bK-`eWzZ`@T{i>J$OB2J1Vu3;>9B_?WOO72%>-HvIZqZTp}!uiL@ z0%LprOGyOo)H}Qa=EM`EMV)qX=lokUL<6$upGXpT%h&r$wvf@79zqxzp}bBkU@mDP z&MOG%df)h{&Hm~~>V*My75PBsDLOSiL)fUkt$01*%-#q7)2rbJk0EG3>~%_(!r%ie zBV=`Y_~Wmwrq3be^9`G(n)Is zCWv>;+Fk0e6DP9pzWy-UE##PG-`8%^_*SLA6*);U<;evJ!|Co#S}W8di9lb7J;_oS zc|d&p<-s;RWc7o7_JLAm?=P~7q>HT3!@RX{u(~OJM15~S22d(V6I-==({oVw1!tVA zv+DYSz|!&2QkXDOEgfa1#}7xGu^l5QfEXtZo@)E98!PzfOH<7DK`msDv0D11^NBtj z;k6peuLOg&q=HYC}k4t^OND-XxwD>mM8xEi(L`_~PiS#ky9j$`su@P}lL_djZqCOvswg z)vhN8Zd4fQNK}*Jv9DdunGIl3z0p%Qyo3{7hG=~J$%7D7FtqYk^aSnMzcQg?7r0^` zrnD_LCL4;KPdnIkG{RK8!q6Und}-rBIn%NKmHK~Ak|%P$lMR0{{@!8i$5`)% zBF5b1IQ($LpV=Qvo(<2*Fh-+Jh&=RPJP7sDW|!soy1c@C;ZK#4*Ir4gz0W&GN7HuV z{L|w<+=;99x{C@OLHaBmsoE=;2tsF@-2qQu%r^fEvpymt6>|ly(G9K2r|ItASSpNzZ+m8SIcNj5 z)(3yyT~lZENd;HBMzd;=z@4Lml8)0lnh|G6zEOo{c%SB@23NRPYd1R6v7GH^_j1 z0S;F9`iTO-vJDM=B8+G~vU&l@I`_BIS?|%;xn9zDO#%0n0{HZAfk``E(CE15;Qn%T zSMMgBP=DoDqF8O{Rb%2kd4F~>Z#Gk$Rp)-VL!2vWN%2HsiWj4sZ7X~qo`Ct}9pUL`mGZGhH^R4#-Iy4WJ+*^syW!Fjo1gc6?SXNLt>MNI)52P00s5 z*;Yzp1_((&wD3uzWbxLS$*hel*_W{U6`ln#LwwIUx+OJH?pQ5iM;n<1u|PzdJjDN+ zpg!F&9yWj_;mL-z5up=pcKS;gnnf};qOe{SUA}SWPiPz7gc%sEU@}?z{mlTDjVGJJ zWBJRy^x=8qR-6Dq#7Oz8F0MMzR;%rML>5XME~3zGd_VCg<;Pa*|74YrdV<&=J*rH{ zh~McCDsbfl?r_5cDyGA;4km;u% `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)); diff --git a/tools/test-laser-level.cjs b/tools/test-laser-level.cjs new file mode 100644 index 00000000..9e41355a --- /dev/null +++ b/tools/test-laser-level.cjs @@ -0,0 +1,229 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs'); + +const level = JSON.parse(fs.readFileSync(path.join(__dirname, '../assets/custom/Json/level9001.json'), 'utf8')); +const info = level.LEVEL_INFO[0]; +const { nearestLaserHit } = loadModule('assets/Script/LaserRules.ts'); +const directions = { right: [1, 0], left: [-1, 0], up: [0, 1], down: [0, -1] }; +const cc = { v2: (x, y) => ({ x, y }), instantiate: () => { + const component = { special: 0, longAndShort: [], init(_data, x, y, direction) { + Object.assign(this, { x, y, direction }); + } }; + const wall = { getComponent: () => component }; + return { children: [null, wall], getChildByName: () => wall, setPosition() {} }; +} }; +const MapController = loadMembers('assets/Script/Map.ts', ['getWllDiraction', 'createWall', 'shrinkVine'], { cc }); +const Block = loadMembers('assets/Script/Block.ts', ['initBlocks'], { cc }); + +// Execute the game's wall direction and prefab creation methods, in createMap's x/y order. +function gameSlots() { + const slots = []; + const map = Object.assign(new MapController(), { + mapWidth: info.map[0], mapHeight: info.map[1], wallNum: 0, + Wall_Prefab: Array.from({ length: 12 }), node: {}, + leftDoors: [], rightDoors: [], topDoors: [], bottomDoors: [], + openWall: [], freezeWall: [], lockWall: [], longAndShortWall: [], + setDoorInfo(wall) { slots.push({ num: this.wallNum, ...wall.getComponent('Wall') }); }, + }); + map.mapBlocksWall = Array.from({ length: map.mapWidth }, (_, x) => + Array.from({ length: map.mapHeight }, (_, y) => { + const boundary = x === 0 || y === 0 || x === map.mapWidth - 1 || y === map.mapHeight - 1 + || info.gap.some(point => point.x === x && point.y === y); + const component = { posX: x, posY: y, setDiraction(direction) { this.direction = direction; } }; + return { x, y, opacity: boundary ? 254 : 255, getComponent: () => component }; + })); + for (const column of map.mapBlocksWall) for (const cell of column) { + if (cell.opacity !== 254) continue; + const direction = map.getWllDiraction('wall', { x: cell.x, y: cell.y }); + if (direction != null) map.createWall(direction, cell); + } + return slots; +} +const slots = gameSlots(); +function cells(piece) { + const block = Object.assign(new Block(), { block_Info: piece }); + block.initBlocks(); + const offsets = piece.block === 23 ? [{ x: 0, y: 0 }] : block.allBlocks; + const x = piece.position.x / 120 + (info.map[0] - 2) / 2; + const y = piece.position.y / 120 + info.map[1] / 2; + return offsets.map(offset => ({ x: x + offset.x, y: y + offset.y })); +} +function rect(point) { + return { type: 'rect', minX: point.x - 0.5, maxX: point.x + 0.5, + minY: point.y - 0.5, maxY: point.y + 0.5 }; +} +function ray(laser) { + return ['emitter', 'receiver'].map(role => { + const slot = slots[laser[role].num], direction = directions[slot.direction]; + return { x: slot.x + direction[0] / 2, y: slot.y + direction[1] / 2 }; + }); +} +// Grid fixture for the authored layout; runtime collider collection has separate tests. +function blockers(pieces = level.BLOCK_INFO[0], terrain = info.risefall) { + return [ + ...pieces.filter(piece => piece.type !== 22).flatMap(piece => + cells(piece).map(cell => ({ owner: piece.id, ...rect(cell) }))), + ...terrain.filter(cell => cell.vine > 0).map(cell => ({ owner: `vine:${cell.x},${cell.y}`, ...rect(cell) })), + ]; +} +function firstHit(laser, objects = blockers()) { + const hit = nearestLaserHit(...ray(laser), objects); + return hit ? objects[hit.colliderIndex].owner : null; +} + +test('level9001 uses actual complete straight-wall slots and independent 1/2/3-cell gates', () => { + assert.equal(info.id, '9001'); + assert.equal(slots.length, 32); + assert.equal(new Set(info.lasers.map(laser => laser.id)).size, 3); + assert.deepEqual(info.lasers.map(laser => laser.color), [7, 7, 6]); + const used = new Set(); + const expectedHeads = [[0, 3, 9, 3], [0, 6, 9, 6], [4, 0, 4, 9]]; + info.lasers.forEach((laser, index) => { + const emitter = slots[laser.emitter.num], receiver = slots[laser.receiver.num]; + assert.deepEqual([emitter.x, emitter.y, receiver.x, receiver.y], expectedHeads[index]); + const forward = directions[emitter.direction], back = directions[receiver.direction]; + assert.ok(forward && back, 'heads must use complete straight walls'); + assert.deepEqual(forward.map((value, axis) => value + back[axis]), [0, 0]); + const [start, end] = ray(laser); + assert.equal((end.x - start.x) * forward[1] - (end.y - start.y) * forward[0], 0); + assert.ok((end.x - start.x) * forward[0] + (end.y - start.y) * forward[1] > 0); + const gate = level.WALL_INFO[0].filter(wall => wall.laserId === laser.id); + assert.equal(gate.length, index + 1); + assert.equal(gate[0].length, gate.length); + gate.forEach((wall, offset) => { + assert.equal(wall.length, offset ? 0 : gate.length); + assert.equal(wall.special, 0); + assert.equal(wall.color, gate[0].color); + assert.equal(wall.id, gate[0].id); + const first = slots[gate[0].num], slot = slots[wall.num]; + assert.equal(slot.direction, first.direction); + assert.ok(directions[slot.direction], 'gate segments must use complete straight walls'); + assert.equal(slot.x, first.x + (['up', 'down'].includes(slot.direction) ? offset : 0)); + assert.equal(slot.y, first.y + (['left', 'right'].includes(slot.direction) ? offset : 0)); + }); + for (const num of [laser.emitter.num, laser.receiver.num, ...gate.map(wall => wall.num)]) { + assert.ok(!used.has(num), `wall slot ${num} overlaps`); + used.add(num); + } + }); + assert.deepEqual(level.WALL_INFO[0].filter(wall => wall.length).map(wall => wall.color), [10, 8, 5]); +}); + +test('all blocks and terrain are inside the board without overlapping, and target widths fit their gates', () => { + const occupied = new Set(), ids = new Set(); + for (const piece of level.BLOCK_INFO[0]) { + assert.ok(!ids.has(piece.id)); ids.add(piece.id); + for (const point of cells(piece)) { + assert.ok(Number.isInteger(point.x) && Number.isInteger(point.y)); + assert.ok(point.x >= 1 && point.x <= 8 && point.y >= 1 && point.y <= 8); + const key = `${point.x},${point.y}`; + assert.ok(!occupied.has(key), `overlapping block at ${key}`); occupied.add(key); + } + if (piece.type === 22 || piece.block === 23) continue; + const gate = level.WALL_INFO[0].find(wall => wall.length && wall.color === piece.color); + assert.ok(gate, `target ${piece.id} has no color-matched gate`); + const axis = ['up', 'down'].includes(slots[gate.num].direction) ? 'x' : 'y'; + const values = cells(piece).map(point => point[axis]); + assert.ok(Math.max(...values) - Math.min(...values) + 1 <= gate.length); + } + for (const point of info.risefall) { + const key = `${point.x},${point.y}`; + assert.ok(!occupied.has(key), `terrain overlaps at ${key}`); occupied.add(key); + } + assert.deepEqual(info.risefall.filter(point => point.vine).map(point => point.vine), [2, 1]); +}); + +test('initial green beams hit the draggable block and vine; the crossing cyan beam passes both single-color tiles', () => { + assert.deepEqual(info.lasers.map(laser => firstHit(laser)), [90010, 'vine:6,6', null]); + const cyan = info.lasers[2]; + const mobile = cells(level.BLOCK_INFO[0].find(piece => piece.type === 22))[0]; + const fixed = info.risefall.find(point => !point.vine); + // Both transparent objects really lie on the beam, rather than accidentally missing it. + for (const point of [mobile, fixed]) assert.ok(nearestLaserHit(...ray(cyan), [rect(point)])); + const [greenStart, greenEnd] = ray(info.lasers[0]), [cyanStart, cyanEnd] = ray(cyan); + assert.ok(cyanStart.x > greenStart.x && cyanStart.x < greenEnd.x); + assert.ok(greenStart.y > cyanStart.y && greenStart.y < cyanEnd.y); +}); + +test('moving the red blocker clears only its own green group; a fixed obstacle still blocks a ray', () => { + const pieces = structuredClone(level.BLOCK_INFO[0]); + pieces.find(piece => piece.id === 90010).position.y += 120; + assert.deepEqual(info.lasers.map(laser => firstHit(laser, blockers(pieces))), [null, 'vine:6,6', null]); + const fixed = blockers().find(object => object.owner === 90050); + assert.ok(nearestLaserHit({ x: 0.5, y: 2 }, { x: 8.5, y: 2 }, [fixed])); +}); + +test('a simple red exit route stays clear and the actual vine shrink releases the second green beam', () => { + const pieces = structuredClone(level.BLOCK_INFO[0]); + const red = pieces.find(piece => piece.id === 90010); + const obstacles = pieces.filter(piece => piece.id !== red.id).flatMap(cells).concat(info.risefall); + for (const point of [{ x: 2, y: 4 }, { x: 2, y: 5 }, { x: 2, y: 6 }, { x: 2, y: 7 }, { x: 1, y: 7 }]) { + assert.ok(!obstacles.some(other => other.x === point.x && other.y === point.y)); + red.position.x = (point.x - 4) * 120; red.position.y = (point.y - 5) * 120; + assert.equal(firstHit(info.lasers[0], blockers(pieces)), null); + } + const remaining = pieces.filter(piece => piece.id !== red.id); + const terrain = structuredClone(info.risefall); + const removed = []; + const map = Object.assign(new MapController(), { + vineBlock: [{ segments: terrain.filter(point => point.vine) }], + releaseVineSegment(segment) { removed.push(segment); }, queueVineShrinkAnimation() {}, + }); + map.shrinkVine(); + assert.equal(removed.length, 2); + assert.deepEqual(info.lasers.map(laser => firstHit(laser, blockers(remaining, terrain.filter(point => !removed.includes(point))))), + [null, null, null]); + // The remaining yellow block can move down one cell; the cyan block can move up one cell. + for (const [id, dy] of [[90020, -120], [90030, 120]]) { + const piece = remaining.find(item => item.id === id); + piece.position.y += dy; + const others = remaining.filter(item => item.id !== id).flatMap(cells); + assert.ok(cells(piece).every(point => !others.some(other => other.x === point.x && other.y === point.y))); + } +}); + +test('the authored JSON passes runtime validation and runtime collection preserves initial reception', () => { + const system = { collide_arr: [] }; + const Runtime = loadMembers('assets/Script/LaserSystem.ts', ['validate', 'collectColliders', 'colliderShape'], { + cc: { ...cc, isValid: node => !!node }, LQCollideSystem: system, + LQCollideShape: { Rect: 1, Circle: 2, Polygon: 3 }, + DIRECTIONS: { + right: { x: 1, y: 0, opposite: 'left' }, left: { x: -1, y: 0, opposite: 'right' }, + up: { x: 0, y: 1, opposite: 'down' }, down: { x: 0, y: -1, opposite: 'up' }, + }, + }); + const map = { node: {}, riseFallBlock: [], mapBlocksWall: Array.from({ length: 10 }, () => + Array.from({ length: 10 }, () => ({ opacity: 250 }))) }; + const runtime = Object.assign(new Runtime(), { map }); + const runtimeSlots = slots.map(slot => ({ ...slot, posX: slot.x, posY: slot.y, + wall_Info: level.WALL_INFO[0].find(wall => wall.num === slot.num), + node: { parent: { getChildByName: name => name === 'laser' ? {} : null } }, + })); + assert.equal(runtime.validate(runtimeSlots, info.lasers).length, 3); + const parent = block => ({ parent: map.node, + getComponent: name => name === 'Block' ? block : null, getChildByName: () => null }); + const collider = (point, owner, name = 'icon') => { + system.collide_arr.push({ enabled: true, is_enable: true, data_string: '', collide_shape: 1, + offset: { x: 0, y: 0 }, size: { width: 1, height: 1 }, + node: { parent: owner, name, activeInHierarchy: true, + getComponent: () => null, getChildByName: () => null, + convertToWorldSpaceAR: offset => ({ x: point.x + offset.x, y: point.y + offset.y }), + }, + }); + }; + for (const piece of level.BLOCK_INFO[0]) { + const owner = parent({ type: piece.type, over: false }); + for (const point of cells(piece)) collider(point, owner); + } + for (const point of info.risefall) { + const owner = parent(null); + if (!point.vine) map.riseFallBlock.push(owner); + collider(point, owner, 'rise'); + } + const shapes = runtime.collectColliders().map(item => item.shape); + assert.equal(shapes.length, 9, 'single-color mobile and fixed tiles must be excluded; vine and fixed obstacle remain'); + assert.deepEqual(info.lasers.map(laser => !!nearestLaserHit(...ray(laser), shapes)), [true, true, false]); +}); diff --git a/tools/test-laser-rules.cjs b/tools/test-laser-rules.cjs new file mode 100644 index 00000000..111fb648 --- /dev/null +++ b/tools/test-laser-rules.cjs @@ -0,0 +1,123 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const ts = require('typescript'); + +const exported = {}; +const source = fs.readFileSync(path.join(__dirname, '../assets/Script/LaserRules.ts'), 'utf8'); +vm.runInNewContext(ts.transpileModule(source, { + compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS }, +}).outputText, { exports: exported }); +const { nearestLaserHit } = exported; +const point = (x, y) => ({ x, y }); +const rect = (minX, minY, maxX, maxY) => ({ type: 'rect', minX, minY, maxX, maxY }); +const polygon = coordinates => ({ type: 'polygon', points: coordinates.map(([x, y]) => point(x, y)) }); +const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`); + +for (const [direction, start, end, expected] of [ + ['right', point(0, 5), point(10, 5), point(4, 5)], + ['left', point(10, 5), point(0, 5), point(6, 5)], + ['up', point(5, 0), point(5, 10), point(5, 4)], + ['down', point(5, 10), point(5, 0), point(5, 6)], +]) { + test(`${direction} beam ends at the nearest rectangle surface`, () => { + const hit = nearestLaserHit(start, end, [rect(4, 4, 6, 6)]); + close(hit.ratio, 0.4); + close(hit.distance, 4); + close(hit.point.x, expected.x); + close(hit.point.y, expected.y); + }); +} + +test('nearest blocker wins regardless of collider ordering', () => { + const hit = nearestLaserHit(point(0, 0), point(10, 0), [ + rect(8, -1, 9, 1), + { type: 'circle', center: point(5, 0), radius: 1 }, + polygon([[2, -1], [3, -1], [3, 1], [2, 1]]), + ]); + assert.equal(hit.colliderIndex, 2); + close(hit.distance, 2); +}); + +test('fractional dragging positions unblock immediately, without grid snapping', () => { + const start = point(0, 0), end = point(10, 0); + const moving = rect(4.125, -0.5, 5.125, 0.5); + close(nearestLaserHit(start, end, [moving]).distance, 4.125); + moving.minY += 0.501; + moving.maxY += 0.501; + assert.equal(nearestLaserHit(start, end, [moving]), null); + moving.minY -= 0.002; + moving.maxY -= 0.002; + close(nearestLaserHit(start, end, [moving]).distance, 4.125); +}); + +test('a beam in the empty part of a concave shape is not blocked by its bounds', () => { + const lShape = polygon([[2, 0], [6, 0], [6, 1], [3, 1], [3, 4], [2, 4]]); + assert.equal(nearestLaserHit(point(4, 3), point(7, 3), [lShape]), null); + const hit = nearestLaserHit(point(7, 3), point(0, 3), [lShape]); + close(hit.point.x, 3); + close(hit.distance, 4); + lShape.points.reverse(); + close(nearestLaserHit(point(7, 3), point(0, 3), [lShape]).distance, 4); +}); + +test('circle surface, tangent, and a clear line are distinguished', () => { + const circle = { type: 'circle', center: point(5, 0), radius: 2 }; + close(nearestLaserHit(point(0, 0), point(10, 0), [circle]).distance, 3); + close(nearestLaserHit(point(0, 2), point(10, 2), [circle]).distance, 5); + assert.equal(nearestLaserHit(point(0, 2.001), point(10, 2.001), [circle]), null); +}); + +test('an emitter inside any solid has a zero-distance hit', () => { + const solids = [ + rect(-1, -1, 1, 1), + { type: 'circle', center: point(0, 0), radius: 1 }, + polygon([[-1, -1], [1, -1], [1, 1], [-1, 1]]), + ]; + for (const solid of solids) { + const hit = nearestLaserHit(point(0, 0), point(10, 0), [solid]); + assert.equal(hit.ratio, 0); + assert.equal(hit.distance, 0); + } +}); + +test('touching a solid boundary blocks, including a collinear polygon edge', () => { + for (const solid of [rect(3, 0, 5, 2), polygon([[3, 0], [5, 0], [5, 2], [3, 2]])]) { + close(nearestLaserHit(point(0, 0), point(10, 0), [solid]).distance, 3); + close(nearestLaserHit(point(10, 0), point(0, 0), [solid]).distance, 5); + assert.equal(nearestLaserHit(point(3, 1), point(0, 1), [solid]).ratio, 0); + } +}); + +test('objects behind the emitter or beyond the receiver do not block', () => { + const solids = [ + rect(-3, -1, -2, 1), rect(11, -1, 12, 1), + { type: 'circle', center: point(13, 0), radius: 2 }, + polygon([[11, -1], [12, -1], [12, 1], [11, 1]]), + ]; + assert.equal(nearestLaserHit(point(0, 0), point(10, 0), solids), null); + close(nearestLaserHit(point(0, 0), point(10, 0), [rect(10, -1, 11, 1)]).ratio, 1); +}); + +test('rotated polygon surfaces use their edges, not their axis-aligned bounds', () => { + const diamond = polygon([[5, 0], [7, 2], [5, 4], [3, 2]]); + close(nearestLaserHit(point(0, 1), point(10, 1), [diamond]).distance, 4); + const diagonal = nearestLaserHit(point(0, 0), point(10, 10), [rect(3, 3, 4, 4)]); + close(diagonal.ratio, 0.3); + close(diagonal.distance, Math.sqrt(18)); +}); + +test('crossing beams are queried independently against the same solids', () => { + const blockers = [rect(7, 4, 8, 6)]; + close(nearestLaserHit(point(0, 5), point(10, 5), blockers).distance, 7); + assert.equal(nearestLaserHit(point(5, 0), point(5, 10), blockers), null); + close(nearestLaserHit(point(0, 5), point(10, 5), blockers).distance, 7); +}); + +test('empty colliders and zero-length lines have defined results', () => { + assert.equal(nearestLaserHit(point(0, 0), point(10, 0), []), null); + assert.equal(nearestLaserHit(point(0, 0), point(0, 0), [rect(1, 1, 2, 2)]), null); + assert.equal(nearestLaserHit(point(0, 0), point(0, 0), [rect(-1, -1, 1, 1)]).distance, 0); +}); diff --git a/tools/test-laser-system.cjs b/tools/test-laser-system.cjs new file mode 100644 index 00000000..532977d6 --- /dev/null +++ b/tools/test-laser-system.cjs @@ -0,0 +1,460 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs'); + +const v2 = (x = 0, y = 0) => ({ x, y, + sub(other) { return v2(this.x - other.x, this.y - other.y); }, + mag() { return Math.hypot(this.x, this.y); }, +}); +class Node { + constructor(name = '') { + this.name = name; + this.children = []; + this.components = {}; + this.active = true; + this.valid = true; + this.x = this.y = this.angle = 0; + this.scale = 1; + this.width = this.height = 0; + this.anchorX = this.anchorY = 0.5; + } + set parent(value) { + if (this._parent) this._parent.children.splice(this._parent.children.indexOf(this), 1); + this._parent = value; + if (value) value.children.push(this); + } + get parent() { return this._parent; } + get activeInHierarchy() { return this.active && (!this.parent || this.parent.activeInHierarchy); } + getChildByName(name) { return this.children.find(child => child.name === name) || null; } + getComponent(type) { return typeof type === 'string' ? this.components[type] || null : this.view; } + setPosition(x, y) { + this.x = typeof x === 'object' ? x.x : x; + this.y = typeof x === 'object' ? x.y : y; + } + getBoundingBox() { + return { xMin: this.x - this.anchorX * this.width, xMax: this.x + (1 - this.anchorX) * this.width, + yMin: this.y - this.anchorY * this.height, yMax: this.y + (1 - this.anchorY) * this.height }; + } + convertToWorldSpaceAR(point) { + const radians = this.angle * Math.PI / 180; + const local = v2(this.x + this.scale * (point.x * Math.cos(radians) - point.y * Math.sin(radians)), + this.y + this.scale * (point.x * Math.sin(radians) + point.y * Math.cos(radians))); + return this.parent ? this.parent.convertToWorldSpaceAR(local) : local; + } + convertToNodeSpaceAR(point) { + const local = this.parent ? this.parent.convertToNodeSpaceAR(point) : point; + const dx = (local.x - this.x) / this.scale, dy = (local.y - this.y) / this.scale; + const radians = -this.angle * Math.PI / 180; + return v2(dx * Math.cos(radians) - dy * Math.sin(radians), dx * Math.sin(radians) + dy * Math.cos(radians)); + } + destroy() { this.valid = false; } +} +class View { + constructor(node) { this.node = node; this.gateCalls = []; } + initHead(role, color, direction, inset = 0, retraction = 0) { Object.assign(this, { role, color, direction, inset, retraction }); } + initGate(length, direction, color) { Object.assign(this, { length, direction, color }); } + initBeam(color, direction, startOffset = 0) { Object.assign(this, { color, direction, startOffset }); } + getBeamOrigin() { return this.node.convertToWorldSpaceAR(v2()); } + getVisibleBeamOrigin() { + const [dx, dy] = { right: [1, 0], left: [-1, 0], up: [0, 1], down: [0, -1] }[this.direction]; + return this.node.convertToWorldSpaceAR(v2(dx * (this.inset - this.retraction), dy * (this.inset - this.retraction))); + } + setGateOpen(open, immediate) { + if (this.beforeAnimation) this.beforeAnimation(open); + this.gateCalls.push({ open, immediate }); + } + setBeam(length, connected) { this.beam = { length, connected }; } +} +const cc = { + Node, v2, isValid: node => !!node && node.valid !== false, + fx: { AudioManager: { _instance: { playEffect() {} } } }, + instantiate(prefab) { + if (typeof prefab === 'function') return prefab(); + const node = new Node('laser'); + node.view = new View(node); + return node; + }, +}; +const LQCollideShape = { Rect: 1, Circle: 2, Polygon: 3 }; +const LQCollideSystem = { collide_arr: [] }; +const rules = loadModule('assets/Script/LaserRules.ts'); +const LaserSystem = loadModule('assets/Script/LaserSystem.ts', { + './lq_collide_system/lq_collide': { LQCollide: class {} }, + './lq_collide_system/lq_collide_system': { LQCollideSystem }, + './lq_base/data/lq_const': { LQCollideShape }, + './LaserRules': rules, + './prop/LaserView': { default: View }, +}, { cc }).default; +const MapController = loadMembers('assets/Script/Map.ts', + ['passWall', 'lateUpdate', 'createWall', 'changeLockWall', 'downDoor'], { cc, setTimeout: callback => callback() }); +const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`); + +function child(parent, name, x = 0, y = 0) { + const node = new Node(name); + node.parent = parent; + node.setPosition(x, y); + return node; +} +function fixture(length = 2) { + LQCollideSystem.collide_arr = []; + const map = { node: new Node('Map'), mapBlocksWall: [], riseFallBlock: [] }; + const slots = [], configs = []; + function wall(x, y, direction, info = null) { + const holder = child(map.node, direction, x * 100, y * 100); + const node = child(holder, 'wall'); + node.width = node.height = 60; + child(holder, 'laser'); + child(holder, 'length').active = false; + const component = { node, posX: x, posY: y, direction, wall_Info: info, + color: info ? info.color : 0, colorArray: [], special: 0 }; + node.components.Wall = component; + if (!map.mapBlocksWall[x]) map.mapBlocksWall[x] = []; + map.mapBlocksWall[x][y] = { opacity: 250 }; + slots.push(component); + return component; + } + function group(id, row, gateLength = length, color = 1) { + const emitter = slots.length; + wall(0, row, 'right'); + const receiver = slots.length; + wall(10, row, 'left'); + for (let i = 0; i < gateLength; i++) { + wall(2 + i, row + 1, 'down', { laserId: id, color: 3, length: i ? 0 : gateLength, special: 0 }); + } + configs.push({ id, color, emitter: { num: emitter }, receiver: { num: receiver } }); + } + group('first', 2); + return { map, slots, configs, wall, group, create: () => new LaserSystem(map, slots, configs, {}) }; +} +function collider(parent, x, y, options = {}) { + const node = child(parent, options.name || 'edge', x, y); + const value = { node, is_enable: true, enabled: true, data_string: '', + collide_shape: LQCollideShape.Rect, offset: v2(), size: { width: 100, height: 100 }, ...options }; + LQCollideSystem.collide_arr.push(value); + return value; +} +function block(parent, type = 0) { + const node = child(parent, 'block'); + node.components.Block = { type, over: false }; + return node; +} + +test('only solid active colliders block; both monochrome types stay transparent and vines stay solid', () => { + const { map, create } = fixture(); + const system = create(); + const ordinary = collider(block(map.node), 400, 200); + const vineCell = child(map.node, 'vineCell'); + const vine = collider(vineCell, 600, 200, { name: 'rise' }); + collider(block(map.node, 22), 400, 200); + const riseCell = child(map.node, 'monochromeCell'); + map.riseFallBlock.push(riseCell); + collider(riseCell, 400, 200, { name: 'rise' }); + collider(block(map.node), 400, 200, { data_string: '-1' }); + collider(block(map.node), 400, 200, { data_string: '-2' }); + collider(block(map.node), 400, 200, { is_enable: false }); + collider(block(map.node), 400, 200, { enabled: false }); + collider(block(map.node), 400, 200).node.active = false; + const hiddenBlock = block(map.node); + hiddenBlock.active = false; + collider(hiddenBlock, 400, 200); + collider(block(map.node), 400, 200).node.valid = false; + const exiting = block(map.node); + exiting.components.Block.over = true; + collider(exiting, 400, 200); + collider(new Node('another map'), 400, 200); + const collected = system.collectColliders(); + assert.equal(collected.length, 2); + close(collected[0].shape.minX, ordinary.node.x - 50); + close(collected[1].shape.minX, vine.node.x - 50); + ordinary.is_enable = false; + system.update(); + assert.equal(system.groups[0].connected, false, 'the remaining vine must block'); + vine.is_enable = false; + system.update(); + assert.equal(system.groups[0].connected, true, 'logically removed vine must stop blocking'); +}); + +test('moving before the collision cache refresh updates beam length and gate state immediately', () => { + const { map, create } = fixture(); + const system = create(); + const moving = collider(block(map.node), 400, 200); + Object.defineProperty(moving, 'world_rect', { get() { throw new Error('stale cache must not be read'); } }); + system.update(); + close(system.groups[0].beam.beam.length, 327); + moving.node.x = 425.125; + system.update(); + close(system.groups[0].beam.beam.length, 352.125); + moving.node.y = 250.001; + system.update(); + assert.equal(system.groups[0].connected, true); + close(system.groups[0].beam.beam.length, 952); +}); + +test('all gate cells change before animation, including an immediate reversal during the previous tween', () => { + const { map, create } = fixture(3); + const system = create(), group = system.groups[0]; + group.gate.gateCalls.length = 0; + group.gate.beforeAnimation = open => assert.ok(group.gates.every(wall => wall.open === open)); + const moving = collider(block(map.node), 400, 200); + system.update(); + assert.equal(group.gate.gateCalls.length, 1); + assert.equal(group.gate.gateCalls[0].open, false); + assert.equal(group.gate.gateCalls[0].immediate, false); + moving.node.y += 60; + system.update(); + assert.equal(group.gate.gateCalls[1].open, true); + assert.equal(group.gate.gateCalls.length, 2); + system.update(); + assert.equal(group.gate.gateCalls.length, 2, 'stable reception must not restart an animation'); +}); + +test('a logically exited block is transparent while its visual node remains active', () => { + const { map, create } = fixture(); + const system = create(), obstacle = block(map.node); + collider(obstacle, 400, 200); + system.update(); + assert.equal(system.groups[0].connected, false); + obstacle.components.Block.over = true; + system.update(); + assert.equal(obstacle.activeInHierarchy, true); + assert.equal(system.groups[0].connected, true); +}); + +test('same-color groups are independent and each beam ends at its nearest solid', () => { + const setup = fixture(1); + setup.group('second', 6, 1, 1); + const system = setup.create(); + const farther = collider(block(setup.map.node), 700, 200); + const nearer = collider(block(setup.map.node), 300, 200); + system.update(); + assert.equal(system.groups[0].connected, false); + close(system.groups[0].beam.beam.length, 227); + assert.equal(system.groups[1].connected, true); + close(system.groups[1].beam.beam.length, 952); + nearer.node.y = 600; + system.update(); + close(system.groups[0].beam.beam.length, 627); + close(system.groups[1].beam.beam.length, 227); + farther.node.y = 400; + system.update(); + assert.equal(system.groups[0].connected, true); + assert.equal(system.groups[1].connected, false); +}); + +test('the source and target wall colliders do not obstruct their own optical path', () => { + const setup = fixture(); + const system = setup.create(); + collider(setup.slots[0].node.parent, 0, 0); + collider(setup.slots[1].node.parent, 0, 0); + system.update(); + assert.equal(system.groups[0].connected, true); +}); + +for (const [direction, opposite, dx, dy, mouth, length] of [ + ['right', 'left', 1, 0, 23, 952], ['left', 'right', -1, 0, 25, 952], + ['up', 'down', 0, 1, 30, 953], ['down', 'up', 0, -1, 17, 953], +]) { + test(`${direction} visible origins move inward while hidden-start blockers still close the logical gate`, () => { + const { map, slots, create } = fixture(); + [direction, opposite].forEach((facing, index) => { + const wall = slots[index]; + wall.direction = facing; + wall.posX = 10 + index * dx * 10; + wall.posY = 10 + index * dy * 10; + if (!map.mapBlocksWall[wall.posX]) map.mapBlocksWall[wall.posX] = []; + map.mapBlocksWall[wall.posX][wall.posY] = { opacity: 250 }; + wall.node.parent.setPosition(index * dx * 1000, index * dy * 1000); + }); + const system = create(), group = system.groups[0]; + system.update(); + close(group.source.getBeamOrigin().x, 0); + close(group.source.getBeamOrigin().y, 0); + close(group.target.getBeamOrigin().x, dx * 1000); + close(group.target.getBeamOrigin().y, dy * 1000); + close(group.beam.node.x, dx * mouth); + close(group.beam.node.y, dy * mouth); + close(group.beam.node.x + dx * group.beam.startOffset, dx * 31); + close(group.beam.node.y + dy * group.beam.startOffset, dy * 31); + close(group.beam.beam.length, length); + assert.equal(system.layer.zIndex, 10, 'do not raise the whole beam over blocks'); + assert.equal(group.connected, true); + collider(block(map.node), dx * 10, dy * 10, { size: { width: 10, height: 10 } }); + system.update(); + assert.equal(group.connected, false, 'collision still starts at the original wall socket'); + assert.ok(group.gates.every(wall => wall.open === false)); + assert.equal(group.beam.beam.length, 0, 'a hit behind the visible start must not make a reversed beam'); + }); +} + +test('head inset uses the wall face without moving the shared socket or gate attachment', () => { + const setup = fixture(), wall = setup.slots[0].node; + wall.setPosition(6.333, -73.465); + wall.anchorX = wall.anchorY = 0; + wall.width = 61; + wall.height = 146; + const socket = wall.parent.getChildByName('laser'); + socket.x = 36.695; + const system = setup.create(), group = system.groups[0]; + close(group.source.inset, 31.638); + close(group.source.getBeamOrigin().x, 36.695); + close(group.source.getVisibleBeamOrigin().x, 60.333); + close(group.beam.node.x + group.beam.startOffset, 68.333); + close(socket.x, 36.695); + close(group.gate.node.x, 0); + close(group.gate.node.parent.x, 0); +}); + +test('map scaling moves only the visible head coordinates and preserves beam length in map space', () => { + const setup = fixture(); + setup.map.node.scale = 0.5; + const system = setup.create(), group = system.groups[0]; + close(group.source.getVisibleBeamOrigin().x, 11.5); + close(group.target.getVisibleBeamOrigin().x, 487.5); + close(group.beam.node.x, 23); + close(group.beam.node.x + group.beam.startOffset, 31); + close(group.beam.beam.length, 952); + assert.equal(group.connected, true); +}); + +test('LQ pre-scaled dimensions and polygon points are not scaled a second time', () => { + const { map, create } = fixture(); + const system = create(); + map.node.scale = 0.5; + const rectangle = collider(block(map.node), 8, 4, { offset: v2(2, 1), size: { width: 3, height: 2 } }); + const shape = system.colliderShape(rectangle); + close(shape.minX, 3.5); + close(shape.maxX, 6.5); + close(shape.minY, 1.5); + const circle = collider(block(map.node), 8, 4, { collide_shape: LQCollideShape.Circle, radius: 3 }); + close(system.colliderShape(circle).radius, 3); + const polygon = collider(block(map.node), 8, 4, { + collide_shape: LQCollideShape.Polygon, polygon_points: [v2(2, 0), v2(0, 2), v2(-2, 0)], + }); + polygon.node.angle = 90; + const points = system.colliderShape(polygon).points; + close(points[0].x, 4); + close(points[0].y, 4); + close(points[1].x, 2); + close(points[1].y, 2); +}); + +test('individual collision strips and corner circles preserve the empty part of a compound shape', () => { + const { map, create } = fixture(); + const system = create(), compound = block(map.node); + collider(compound, 3, 5, { size: { width: 2, height: 10 } }); + collider(compound, 6, 1, { size: { width: 8, height: 2 } }); + collider(compound, 9, 9, { collide_shape: LQCollideShape.Circle, radius: 1 }); + const shapes = system.collectColliders().map(item => item.shape); + assert.equal(shapes.length, 3); + assert.equal(rules.nearestLaserHit(v2(6, 6), v2(9, 6), shapes), null); + close(rules.nearestLaserHit(v2(6, 6), v2(0, 6), shapes).distance, 2); +}); + +for (const [name, change, error] of [ + ['missing receiver', setup => delete setup.configs[0].receiver, /完整直墙/], + ['same-facing endpoints', setup => setup.slots[1].direction = 'right', /相向/], + ['misaligned endpoints', setup => setup.slots[1].posY++, /相向/], + ['corner wall', setup => setup.map.mapBlocksWall[0][2].opacity = 251, /完整直墙/], + ['occupied head slot', setup => setup.slots[0].wall_Info = { color: 1 }, /已被占用/], + ['missing gate segment', setup => setup.slots.pop(), /完整的/], + ['noncontiguous gate', setup => setup.slots[3].posX++, /连续同色/], + ['mixed gate colors', setup => setup.slots[3].wall_Info.color++, /连续同色/], + ['other gate mechanics', setup => setup.slots[2].wall_Info.toggle = 1, /连续同色/], + ['orphan gate', setup => setup.configs.length = 0, /缺少对应组/], +]) { + test(`invalid laser configuration rejects ${name}`, () => { + const setup = fixture(); + // Keep a valid map cell at adjacent positions for coordinate-mutating cases. + setup.map.mapBlocksWall[10][3] = { opacity: 250 }; + setup.map.mapBlocksWall[4] = [{}, {}, {}, { opacity: 250 }]; + change(setup); + assert.throws(setup.create, error); + }); +} + +test('passWall refreshes reception before checking every segment and preserves color restrictions', () => { + const setup = fixture(); + const system = setup.create(); + const map = Object.assign(new MapController(), { laserSystem: system, checkColor() {} }); + const piece = block(setup.map.node); + piece.components.Block.color = 3; + const gates = system.groups[0].gates.map(wall => wall.node); + const obstacle = collider(block(setup.map.node), 500, 200); + assert.equal(map.passWall(true, gates, piece), false); + obstacle.node.y += 60; + assert.equal(map.passWall(true, gates, piece), true); + piece.components.Block.color = 2; + assert.equal(map.passWall(true, gates, piece), false); + piece.components.Block.oldRainbow = true; + assert.equal(map.passWall(true, gates, piece), true); + obstacle.node.y -= 60; + assert.equal(map.passWall(true, gates, piece), false, 'rainbow cannot bypass a disconnected laser gate'); +}); + +test('lateUpdate refreshes lasers and legacy gates continue to work with no laser system', () => { + let updates = 0; + const map = Object.assign(new MapController(), { laserSystem: { update() { updates++; } }, checkColor() {} }); + map.lateUpdate(); + assert.equal(updates, 1); + map.laserSystem = null; + map.lateUpdate(); + const setup = fixture(1), wall = setup.slots[2]; + delete wall.wall_Info.laserId; + const piece = block(setup.map.node); + piece.components.Block.color = wall.color; + wall.open = false; + assert.equal(map.passWall(true, [wall.node], piece), true); + wall.special = 2; + assert.equal(map.passWall(true, [wall.node], piece), false); + wall.open = true; + assert.equal(map.passWall(true, [wall.node], piece), true); +}); + +test('creating laser gates leaves them outside the global switch-door toggle list', () => { + const created = []; + const prefab = () => { + const holder = new Node('right'), wall = child(holder, 'wall'); + wall.components.Wall = { special: 0, longAndShort: [], open: true, + init() {}, changeLock() { this.open = !this.open; } }; + created.push(wall.components.Wall); + return holder; + }; + const map = Object.assign(new MapController(), { + node: new Node('Map'), Wall_Prefab: Array(12).fill(prefab), wallNum: 0, + leftDoors: [], openWall: [], freezeWall: [], lockWall: [], longAndShortWall: [], + setDoorInfo(wall) { + const component = wall.getComponent('Wall'); + component.special = this.wallNum === 0 ? 0 : 2; + component.wall_Info = this.wallNum === 0 ? { laserId: 'first', special: 0 } : { special: 2 }; + }, + }); + const cell = new Node('cell'); + cell.components.MapBlock = { posX: 0, posY: 0, setDiraction() {} }; + map.createWall('right', cell); + map.createWall('right', cell); + assert.equal(map.openWall.length, 1); + map.changeLockWall(); + assert.equal(created[0].open, true, 'laser gate must not be toggled by ordinary block exits'); + assert.equal(created[1].open, false, 'the existing switch door still toggles'); +}); + +test('door hints skip a closed laser gate and preserve ordinary door color hints', () => { + const setup = fixture(1), laserWall = setup.slots[2]; + const ordinaryWall = setup.wall(5, 5, 'down', { special: 0, length: 1, color: 3 }); + const calls = []; + laserWall.downDoor = () => calls.push('laser'); + ordinaryWall.downDoor = () => calls.push('ordinary'); + const map = Object.assign(new MapController(), { + wallArray: [laserWall.node.parent, ordinaryWall.node.parent], + }); + laserWall.open = false; + map.downDoor(3, 0); + assert.deepEqual(calls, ['ordinary']); + calls.length = 0; + laserWall.open = true; + map.downDoor(3, 0); + assert.deepEqual(calls, ['laser', 'ordinary']); + calls.length = 0; + map.downDoor(2, 0); + assert.deepEqual(calls, []); +});