export class GameObjManager { map; opts = { styles: { default: { textColor: "FFFFFF", backgroundColor: "303030", borderColor: "000000", actionColor: "000000", textOpacity: 1, backgroundOpacity: 0.75, borderOpacity: 1, borderWidth: 8, font: "monospace", fontSize: 24, fontWidth: 16, }, defaultFocused: { textColor: "FFFFFF", backgroundColor: "505030", borderColor: "808000", actionColor: "000000", textOpacity: 1, backgroundOpacity: 0.75, borderOpacity: 1, borderWidth: 8, font: "monospace", fontSize: 24, fontWidth: 16, }, }, }; mainmanager; floorObject; roomObjects = new Map([]); constructor(map, opts = {}) { this.map = map; this.opts = { ...this.opts, ...opts, }; } __getTopStyle(styles = [], focused = false) { const defaultFocused = { ...this.opts.styles.default, ...this.opts.styles.defaultFocused, }; if (focused) { return styles.reduce((lastStyle, style) => { return { ...lastStyle, ...this.opts.styles[style], ...this.opts.styles[style + "Focused"], }; }, defaultFocused); } else { return styles.reduce((lastStyle, style) => { return { ...lastStyle, ...this.opts.styles[style], }; }, this.opts.styles.default); } } __getTopFloorStyle(styles = []) { const defaultFloor = { ...this.opts.styles.default, ...this.opts.styles.default_floor, }; return styles.reduce((lastStyle, style) => { return { ...lastStyle, ...this.opts.styles[style], }; }, defaultFloor); } generateFloor() { if (this.floorObject) this.floorObject.destroy(); const currentFloor = this.mainmanager.getCurrentFocusObject("floor"); // Generate regular polygon for area() if (currentFloor.polys && currentFloor.poly == null) currentFloor.poly = currentFloor.polys.flat(1); const polygon = new this.map.kp.Polygon( currentFloor.poly.map(([x, y]) => this.map.kp.vec2(x, y)) ); const bounds = polygon.bbox(); this.map.camBounds = bounds; const curStyle = this.__getTopFloorStyle(currentFloor.styles); this.floorObject = this.map.kp.make([ this.map.kp.polygon(polygon.pts, { triangulate: true, }), this.map.kp.opacity(0), this.map.kp.pos(), this.map.kp.z(4), ]); let polysgon; if (currentFloor.polys) polysgon = currentFloor.polys.map( (poly) => new this.map.kp.Polygon(poly.map(([x, y]) => this.map.kp.vec2(x, y))) ); this.floorObject.onDraw(() => { const camScale = 1 / this.map.kp.camScale().y; if (polysgon) { polysgon.forEach((poly) => { this.map.kp.drawPolygon({ pts: poly.pts, pos: this.map.kp.vec2(0), triangulate: true, color: this.map.kp.Color.fromHex(curStyle.backgroundColor), opacity: curStyle.backgroundOpacity, }); }); } else { this.map.kp.drawPolygon({ pts: polygon.pts, pos: this.map.kp.vec2(0), triangulate: true, color: this.map.kp.Color.fromHex(curStyle.backgroundColor), opacity: curStyle.backgroundOpacity, }); } this.map.kp.drawPolygon({ pts: polygon.pts, pos: this.map.kp.vec2(0), fill: false, triangulate: true, outline: { color: this.map.kp.Color.fromHex(curStyle.borderColor), opacity: curStyle.borderOpacity, width: curStyle.borderWidth * camScale, }, }); let text = currentFloor.name; if (curStyle.textTransform == "uppercase") { text = text.toUpperCase(); } else if (curStyle.textTransform == "lowercase") { text = text.toLowerCase(); } this.map.kp.drawText({ text, size: curStyle.fontSize * camScale, font: curStyle.font, pos: this.map.kp.vec2(-4 * camScale, -8 * camScale), color: this.map.kp.Color.fromHex(curStyle.textColor), opacity: curStyle.textOpacity, anchor: "botleft", }); }); this.map.kp.add(this.floorObject); } zoomToBounds(boundingBox, pos) { // Get screen size const width = this.map.kp.width(); const height = this.map.kp.height(); // Get bbox size const bBoxWidth = boundingBox.width * 1.25; const bBoxHeight = boundingBox.height * 1.25; // Compare bounds const scaledWidth = width / bBoxWidth; const scaledHeight = height / bBoxHeight; // Whichever one is biggest const scale = Math.min(scaledWidth, scaledHeight); let newPos = boundingBox.center(); if (pos) newPos = newPos.add(pos); this.map.zoomToAbs( Math.log2(scale), // log scale newPos ); } zoomToFloor() { const objectBounds = this.floorObject.renderArea().bbox(); this.zoomToBounds(objectBounds); } zoomToRoom(id) { const selectedObject = this.roomObjects.get(id); if (selectedObject == null) return; const objectBounds = selectedObject.renderArea().bbox(); this.zoomToBounds(objectBounds, selectedObject.pos); } generateRooms() { if (this.roomObjects.size > 0) { this.roomObjects.forEach((x) => x.destroy()); this.roomObjects.clear(); } const currentRooms = this.mainmanager.getAllFocusObject("room"); currentRooms.forEach((room) => { // Generate regular polygon for area() if (room.polys && room.poly == null) room.poly = room.polys.flat(1); const polygon = new this.map.kp.Polygon( room.poly.map(([x, y]) => this.map.kp.vec2(x, y)) ); const polygonBbox = polygon.bbox(); const polygonCenter = polygonBbox.center(); polygon.pts = polygon.pts.map((point) => point.sub(polygonCenter)); let roomFocused = this.mainmanager.getCurrentFocus("room") == room.id; let curStyle = this.__getTopStyle(room.styles, roomFocused); const obj = this.map.kp.make([ this.map.kp.polygon(polygon.pts, { triangulate: true, }), this.map.kp.opacity(0), this.map.kp.area(), this.map.kp.pos(polygonCenter), this.map.kp.z(room.empty ? 5 : 6), { clickForgiveness: 5, startClickPosition: null, }, ]); this.roomObjects.set(room.id, obj); let polysgon; if (room.polys) polysgon = room.polys.map( (poly) => new this.map.kp.Polygon( poly.map(([x, y]) => this.map.kp.vec2(x, y).sub(polygonCenter)) ) ); let labelPos = this.map.kp.vec2(0); let labelWidth = polygonBbox.width; if (room.label?.pos) labelPos = this.map.kp.vec2(room.label.pos).sub(polygonCenter); if (room.label?.width) labelWidth = room.label.width; obj.onDraw(() => { // Check if room is within bounds const camScale = 1 / this.map.kp.camScale().y; const camBounds = this.map.kp.vec2( this.map.kp.width() * camScale, this.map.kp.height() * camScale ); const camPos = this.map.kp.camPos().sub(camBounds.scale(0.5)); const rect = new this.map.kp.Rect(camPos, camBounds.x, camBounds.y); if (!rect.collides(polygonBbox)) return; const curRoomFocused = this.mainmanager.getCurrentFocus("room") == room.id; if (curRoomFocused != roomFocused) { curStyle = this.__getTopStyle(room.styles, curRoomFocused); roomFocused = curRoomFocused; } if (roomFocused) { obj.z = 7; } else { obj.z = 6; } if (polysgon) { polysgon.forEach((poly) => { this.map.kp.drawPolygon({ pts: poly.pts, pos: this.map.kp.vec2(0), triangulate: true, color: this.map.kp.Color.fromHex(curStyle.backgroundColor), opacity: curStyle.backgroundOpacity, }); }); } else { this.map.kp.drawPolygon({ pts: polygon.pts, pos: this.map.kp.vec2(0), triangulate: true, color: this.map.kp.Color.fromHex(curStyle.backgroundColor), opacity: curStyle.backgroundOpacity, }); } if (!room.empty) { let opacity = 0; if (obj.isHovering()) { opacity = 0.25; } if (this.map.kp.isMousePressed() && obj.isHovering()) { obj.startClickPosition = this.map.kp.mousePos(); } if (this.map.kp.isMouseDown() && obj.isHovering()) { opacity = 0.5; } if (this.map.kp.isMouseReleased() && obj.isHovering()) { const endClickPosition = this.map.kp.mousePos(); if ( obj.startClickPosition && obj.startClickPosition.dist(endClickPosition) < obj.clickForgiveness ) { window.location.hash = room.id; } this.map.clearMouseMode(); } if (polysgon) { polysgon.forEach((poly) => { this.map.kp.drawPolygon({ pts: poly.pts, pos: this.map.kp.vec2(0), triangulate: true, color: this.map.kp.Color.fromHex(curStyle.actionColor), opacity, }); }); } else { this.map.kp.drawPolygon({ pts: polygon.pts, pos: this.map.kp.vec2(0), triangulate: true, color: this.map.kp.Color.fromHex(curStyle.actionColor), opacity, }); } } this.map.kp.drawPolygon({ pts: polygon.pts, pos: this.map.kp.vec2(0), fill: false, triangulate: true, outline: { color: this.map.kp.Color.fromHex(curStyle.borderColor), opacity: curStyle.borderOpacity, width: curStyle.borderWidth * camScale, }, }); // Easy optimization: Don't draw empty room names if (room.name != "") { if ( room.shortName != "" && curStyle.fontWidth * camScale * room.name.length > labelWidth ) { if ( curStyle.shortNameCollapse && curStyle.fontWidth * camScale * room.shortName.length > labelWidth ) return; let text = room.shortName + (curStyle.textCut ?? "…"); if (curStyle.textTransform == "uppercase") { text = text.toUpperCase(); } else if (curStyle.textTransform == "lowercase") { text = text.toLowerCase(); } this.map.kp.drawText({ text, size: curStyle.shortNameMinScale ? Math.min(curStyle.fontSize, curStyle.fontSize * camScale) : curStyle.fontSize * camScale, font: curStyle.font, pos: labelPos, color: this.map.kp.Color.fromHex(curStyle.textColor), opacity: curStyle.textOpacity, align: "center", anchor: "center", }); } else { if ( curStyle.shortNameCollapse && curStyle.fontWidth * camScale * room.name.length > labelWidth ) return; let text = room.name; if (curStyle.textTransform == "uppercase") { text = text.toUpperCase(); } else if (curStyle.textTransform == "lowercase") { text = text.toLowerCase(); } this.map.kp.drawText({ text, size: curStyle.shortNameMinScale ? Math.min(labelWidth, curStyle.fontSize * camScale) : curStyle.fontSize * camScale, font: curStyle.font, pos: labelPos, color: this.map.kp.Color.fromHex(curStyle.textColor), opacity: curStyle.textOpacity, align: "center", anchor: "center", }); } } }); this.floorObject.add(obj); }); } }