// ==UserScript==
// @name Cube Engine Uptaded New Options
// @version 9.0.2
// @description The ultimate enhancement for your Drawaria.online experience. Redefining possibilities!
// @namespace drawaria.modded.fullspec
// @homepage https://drawaria.online/profile/?uid=63196790-c7da-11ec-8266-c399f90709b7
// @author ≺ᴄᴜʙᴇ³≻ And YouTubeDrawaria
// @match https://drawaria.online/
// @match https://drawaria.online/test
// @match https://drawaria.online/room/*
// @icon https://drawaria.online/avatar/cache/e53693c0-18b1-11ec-b633-b7649fa52d3f.jpg
// @grant none
// @license GNU GPLv3
// @run-at document-end
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==
(function () {
(function CodeMaid(callback) {
class TypeChecker {
constructor() {}
isArray(value) {
return this.isA("Array", value);
}
isObject(value) {
return !this.isUndefined(value) && value !== null && this.isA("Object", value);
}
isString(value) {
return this.isA("String", value);
}
isNumber(value) {
return this.isA("Number", value);
}
isFunction(value) {
return this.isA("Function", value);
}
isAsyncFunction(value) {
return this.isA("AsyncFunction", value);
}
isGeneratorFunction(value) {
return this.isA("GeneratorFunction", value);
}
isTypedArray(value) {
return (
this.isA("Float32Array", value) ||
this.isA("Float64Array", value) ||
this.isA("Int16Array", value) ||
this.isA("Int32Array", value) ||
this.isA("Int8Array", value) ||
this.isA("Uint16Array", value) ||
this.isA("Uint32Array", value) ||
this.isA("Uint8Array", value) ||
this.isA("Uint8ClampedArray", value)
);
}
isA(typeName, value) {
return this.getType(value) === "[object " + typeName + "]";
}
isError(value) {
if (!value) {
return false;
}
if (value instanceof Error) {
return true;
}
return typeof value.stack === "string" && typeof value.message === "string";
}
isUndefined(obj) {
return obj === void 0;
}
getType(value) {
return Object.prototype.toString.apply(value);
}
}
class DOMCreate {
#validate;
constructor() {
this.#validate = new TypeChecker();
}
exportNodeTree(node = document.createElement("div")) {
let referenceTolocalThis = this;
let json = {
nodeName: node.nodeName,
attributes: {},
children: [],
};
Array.from(node.attributes).forEach(function (attribute) {
json.attributes[attribute.name] = attribute.value;
});
if (node.children.length <= 0) {
json.children.push(node.textContent.replaceAll("\t", ""));
return json;
}
Array.from(node.children).forEach(function (childNode) {
json.children.push(referenceTolocalThis.exportNodeTree(childNode));
});
return json;
}
importNodeTree(json = { nodeName: "", attributes: {}, children: [] }) {
let referenceTolocalThis = this;
if (referenceTolocalThis.#validate.isString(json)) {
return this.TextNode(json);
}
let node = this.Tree(json.nodeName, json.attributes);
json.children.forEach(function (child) {
node.appendChild(referenceTolocalThis.importNodeTree(child));
});
return node;
}
Element() {
return document.createElement.apply(document, arguments);
}
TextNode() {
return document.createTextNode.apply(document, arguments);
}
Tree(type, attrs, childrenArrayOrVarArgs) {
const el = this.Element(type);
let children;
if (this.#validate.isArray(childrenArrayOrVarArgs)) {
children = childrenArrayOrVarArgs;
} else {
children = [];
for (let i = 2; i < arguments.length; i++) {
children.push(arguments[i]);
}
}
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (typeof child === "string") {
el.appendChild(this.TextNode(child));
} else {
if (child) {
el.appendChild(child);
}
}
}
for (const attr in attrs) {
if (attr == "className") {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
el.appendAll = function (...nodes) {
nodes.forEach((node) => {
el.appendChild(node);
});
};
return el;
}
}
class CookieManager {
constructor() {}
set(name, value = "") {
document.cookie =
name + "=" + value + "; expires=" + new Date("01/01/2024").toUTCString().replace("GMT", "UTC") + "; path=/";
}
get(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(";");
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
clear(name) {
document.cookie = name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
}
}
class DocumentCleaner {
document;
constructor() {
this.document = new DOMCreate();
}
scripts(remove = true) {
try {
let array = document.querySelectorAll('script[src]:not([data-codemaid="ignore"])');
array.forEach((script) => {
if (script.src != "") document.head.appendChild(script);
});
} catch (error) {
console.error(error);
}
try {
let unifiedScript = this.document.Tree("script");
let scripts = document.querySelectorAll('script:not([src]):not([data-codemaid="ignore"])');
let unifiedScriptContent = "";
scripts.forEach((script) => {
let content = script.textContent; //.replaceAll(/\s/g, '');
unifiedScriptContent += `try{${content}}catch(e){console.warn(e);}`;
script.remove();
});
unifiedScript.textContent = unifiedScriptContent;
if (!remove) document.head.appendChild(unifiedScript);
} catch (error) {
console.error(error);
}
}
styles(remove = false) {
try {
let unifiedStyles = this.document.Tree("style");
unifiedStyles.textContet = "";
let styles = document.querySelectorAll('style:not([data-codemaid="ignore"])');
styles.forEach((style) => {
unifiedStyles.textContent += style.textContent;
style.remove();
});
if (!remove) document.head.appendChild(unifiedStyles);
} catch (error) {
console.error(error);
}
}
embeds() {
try {
let array = document.querySelectorAll("iframe");
array.forEach((iframe) => {
iframe.remove();
});
} catch (error) {
console.error(error);
}
}
}
class CustomGenerator {
constructor() {}
uuidv4() {
return crypto.randomUUID();
}
}
globalThis.typecheck = new TypeChecker();
globalThis.cookies = new CookieManager();
globalThis.domMake = new DOMCreate();
globalThis.domClear = new DocumentCleaner();
globalThis.generate = new CustomGenerator();
if (window.location.pathname === "/") window.location.assign("/test");
})();
(function CubicEngine() {
domMake.Button = function (content) {
let btn = domMake.Tree("button", { class: "btn btn-outline-secondary" });
btn.innerHTML = content;
return btn;
};
domMake.Row = function () {
return domMake.Tree("div", { class: "_row" });
};
domMake.IconList = function () {
return domMake.Tree("div", { class: "icon-list" });
};
const sockets = [];
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (...args) {
let socket = this;
if (sockets.indexOf(socket) === -1) {
sockets.push(socket);
}
socket.addEventListener("close", function () {
const pos = sockets.indexOf(socket);
if (~pos) sockets.splice(pos, 1);
});
return originalSend.call(socket, ...args);
};
const identifier = "🧊";
class Stylizer {
constructor() {
this.element = domMake.Tree("style", { "data-codemaid": "ignore" }, []);
document.head.appendChild(this.element);
this.initialize();
}
initialize() {
this.addRules([
`body * {margin: 0; padding: 0; box-sizing: border-box; line-height: normal;}`,
`#${identifier} {--CE-bg_color: var(--light); --CE-color: var(--dark); line-height: 2rem; font-size: 1rem;}`,
`#${identifier}>details {position:relative; overflow:visible; z-index: 999; background-color: var(--CE-bg_color); border: var(--CE-color) 1px solid; border-radius: .25rem;}`,
`#${identifier} details>summary::marker {content:"📘";}`,
`#${identifier} details[open]>summary::marker {content:"📖";}`,
`#${identifier} details details {margin: 1px 0; border-top: var(--CE-color) 1px solid;}`,
`#${identifier} input.toggle[name][hidden]:not(:checked) + * {display: none !important;}`,
`#${identifier} header>.icon {margin: 1px;}`,
`#${identifier} header>.icon.active {color: var(--success);}`,
`#${identifier} header>.icon:not(.active) {color:var(--danger); opacity:.6;}`,
`#${identifier} header:not(:has([title='Unselect'] + *)) > [title='Unselect'] {display:none;}`,
`#${identifier} .btn {padding: 0;}`,
`#${identifier} .icon-list {display: flex; flex-flow: wrap;}`,
`#${identifier} .nowrap {overflow-x: scroll; padding-bottom: 12px; flex-flow: nowrap;}`,
`#${identifier} .icon {display: flex; flex: 0 0 auto; max-width: 1.6rem; min-width: 1.6rem; height: 1.6rem; border-radius: .25rem; border: 1px solid var(--CE-color); aspect-ratio: 1/1;}`,
`#${identifier} .icon > * {margin: auto; text-align: center; max-height: 100%; max-width: 100%;}`,
`#${identifier} .itext {text-align: center; -webkit-appearance: none; -moz-appearance: textfield;}`,
`#${identifier} ._row {display: flex; width: 100%;}`,
`#${identifier} ._row > * {width: 100%;}`,
`hr {margin: 5px 0;}`,
`.playerlist-row::after {content: attr(data-playerid); position: relative; float: right; top: -20px;}`,
`[hidden] {display: none !important;}`,
`.noselect {-webkit-touch-callout: none; -webkit-user-select: none; -moz-user-select: none; user-select: none;}`,
]);
}
addRules(rules = []) {
let reference = this;
rules.forEach(function (rule) {
reference.addRule(rule);
});
}
addRule(rule) {
let TextNode = domMake.TextNode(rule);
this.element.appendChild(TextNode);
}
}
class ModBase {
static globalListOfExtensions = [];
static localListOfExtensions = [];
static Styles = new Stylizer();
static register = function (extension) {
extension.localListOfExtensions = [];
ModBase.globalListOfExtensions.push(extension);
return ModBase;
};
static bind = function (extension, target) {
let parent;
if (typecheck.isFunction(target)) parent = target;
else if (typecheck.isString(target))
parent = ModBase.globalListOfExtensions.find((entry) => entry.name === target);
else if (typecheck.isObject(target)) parent = target.constructor;
else {
console.log(typecheck.getType(target));
}
if (!parent) return new Error(`${parent}`);
parent.localListOfExtensions.push(extension);
parent.autostart = true;
return parent;
};
static findGlobal = function (extensionName) {
return ModBase.globalListOfExtensions.find((entry) => entry.name === extensionName);
};
#id;
#name;
#icon;
htmlElements;
children;
parent;
constructor(name, icon) {
this.#id = generate.uuidv4();
this.#name = this.constructor.name;
this.#icon = "📦";
this.children = [];
this.htmlElements = {};
this.#onStartup();
this.setName(name || this.#name);
this.setIcon(icon || this.#icon);
}
#onStartup() {
this.#loadInterface();
if (this.constructor.autostart)
this.constructor.localListOfExtensions.forEach((extension) => {
this.loadExtension(extension);
});
}
#loadInterface() {
this.htmlElements.details = domMake.Tree("details", {
class: "noselect",
open: false, // Changed from true to false to make it closed by default
"data-reference": this.constructor.name,
});
this.htmlElements.summary = domMake.Tree("summary");
this.htmlElements.header = domMake.Tree("header", { class: "icon-list" });
this.htmlElements.section = domMake.Tree("section");
this.htmlElements.children = domMake.Tree("section");
this.htmlElements.details.appendChild(this.htmlElements.summary);
this.htmlElements.details.appendChild(this.htmlElements.header);
this.htmlElements.details.appendChild(this.htmlElements.section);
this.htmlElements.details.appendChild(this.htmlElements.children);
this.htmlElements.input = domMake.Tree(
"input",
{ type: "radio", id: this.#id, name: "QBit", class: "toggle", hidden: true, title: this.#name },
[this.#name]
);
this.htmlElements.label = domMake.Tree("label", { for: this.#id, class: "icon" });
{
const input = this.htmlElements.input;
const label = this.htmlElements.label;
input.addEventListener("change", (event) => {
this.parent?.children.forEach((child) => {
child.htmlElements.label.classList.remove("active");
});
label.classList[input.checked ? "add" : "remove"]("active");
});
label.classList[input.checked ? "add" : "remove"]("active");
}
{
const resetImageSelectionLabel = domMake.Tree("div", { class: "icon", title: "Unselect" }, [
domMake.Tree("i", { class: "fas fa-chevron-left" }),
]);
resetImageSelectionLabel.addEventListener("click", () => {
this.children.forEach((child) => {
child.htmlElements.label.classList.remove("active");
child.htmlElements.input.checked = !1;
});
});
this.htmlElements.header.appendChild(resetImageSelectionLabel);
}
}
loadExtension(extension, referenceHandler) {
let activeExtension = new extension();
activeExtension.parent = this;
activeExtension.htmlElements.input.name = this.getName();
if (referenceHandler) referenceHandler(activeExtension);
else this.children.push(activeExtension);
if (!extension.siblings) extension.siblings = [];
extension.siblings.push(activeExtension);
if (extension.isFavorite) {
activeExtension.htmlElements.input.click();
if (activeExtension.enable) activeExtension.enable();
}
this.htmlElements.header.appendChild(activeExtension.htmlElements.label);
this.htmlElements.children.appendChild(activeExtension.htmlElements.input);
this.htmlElements.children.appendChild(activeExtension.htmlElements.details);
return activeExtension;
}
notify(level, message) {
if (typeof message != "string") {
try {
message = JSON.stringify(message);
} catch (error) {
throw error;
}
}
let color = "";
if ([5, "error"].includes(level)) {
color = "#dc3545";
} else if ([4, "warning"].includes(level)) {
color = "#ffc107";
} else if ([3, "info"].includes(level)) {
color = "#17a2b8";
} else if ([2, "success"].includes(level)) {
color = "#28a745";
} else if ([1, "log"].includes(level)) {
color = "#6c757d";
} else if ([0, "debug"].includes(level)) {
color = "purple";
}
console.log(`%c${this.#name}: ${message}`, `color: ${color}`);
let chatmessage = domMake.Tree(
"div",
{ class: `chatmessage systemchatmessage5`, "data-ts": Date.now(), style: `color: ${color}` },
[`${this.#name}: ${message}`]
);
let loggingContainer = document.getElementById("chatbox_messages");
if (!loggingContainer) loggingContainer = document.body;
loggingContainer.appendChild(chatmessage);
}
findGlobal(extensionName) {
return this.referenceToBase.findGlobal(extensionName);
}
findLocal(extensionName) {
return this.children.filter((child) => child.constructor.name === extensionName);
}
setName(name) {
if (!name) return;
this.#name = name;
this.htmlElements.label.title = name;
this.htmlElements.summary.childNodes.forEach((child) => child.remove());
if (typecheck.isString(name)) {
if (name.startsWith("<")) return (this.htmlElements.summary.innerHTML = name);
name = domMake.TextNode(name);
}
this.htmlElements.summary.appendChild(name);
}
getName() {
return this.#name;
}
setIcon(icon) {
if (!icon) return;
this.#icon = icon;
this.htmlElements.label.childNodes.forEach((child) => child.remove());
if (typecheck.isString(icon)) {
if (icon.startsWith("<")) return (this.htmlElements.label.innerHTML = icon);
icon = domMake.TextNode(icon);
}
this.htmlElements.label.appendChild(icon);
}
getIcon() {
return this.#icon;
}
get referenceToBase() {
return this.constructor.dummy1;
}
get referenceToMaster() {
return this.constructor.dummy2;
}
_EXP_destroy(youSure = false) {
if (!youSure) return;
this.children.forEach((child) => {
child._EXP_destroy(youSure);
delete [child];
});
this.children = null;
let pos = this.parent.children.indexOf(this);
if (~pos) {
this.parent.children.splice(pos, 1);
}
this.htmlElements.children.remove();
this.htmlElements.section.remove();
this.htmlElements.header.remove();
this.htmlElements.summary.remove();
this.htmlElements.details.remove();
this.htmlElements.input.remove();
this.htmlElements.label.remove();
this.htmlElements = null;
let pos2 = this.constructor.siblings.indexOf(this);
if (~pos2) {
this.constructor.siblings.splice(pos2, 1);
}
}
}
class CubeEngine extends ModBase {
static dummy1 = ModBase.register(this);
constructor() {
super("CubeEngine");
}
}
class Await {
static dummy1 = ModBase.register(this);
#interval;
#handler;
#callback;
constructor(callback, interval) {
this.#interval = interval;
this.#callback = callback;
}
call() {
const localThis = this;
clearTimeout(this.#handler);
this.#handler = setTimeout(function () {
localThis.#callback();
}, this.#interval);
}
}
globalThis[arguments[0]] = ModBase;
return function (when = "load") {
setTimeout(() => {
const ModMenu = new CubeEngine();
ModMenu.htmlElements.details.open = false; // This line is now redundant as it's set in ModBase
const target = document.getElementById("accountbox");
const container = domMake.Tree("div", { id: identifier, style: "height: 1.6rem; flex: 0 0 auto;" });
container.appendChild(ModMenu.htmlElements.details);
target.after(container);
target.after(domMake.Tree("hr"));
globalThis["CubeEngine"] = ModMenu;
globalThis["sockets"] = sockets;
domClear.embeds();
domClear.scripts();
domClear.styles();
}, 200);
};
})("QBit")();
// ... (final del módulo CubicEngine, p.ej. })("QBit")(); )
// --- Global readiness promises for external libraries ---
// Estas promesas se resolverán cuando la librería respectiva esté completamente cargada e inicializada.
// Su colocación es CRÍTICA: Deben estar en el scope más externo de tu userscript
// pero después de la inicialización de CubicEngine para que 'QBit' y 'globalThis' estén disponibles.
// NOTA: image-js ha sido ELIMINADO de aquí y del código del módulo ImageAnalyzer.
let _cvReadyPromise = new Promise(resolve => {
// OpenCV.js requiere `cv.onRuntimeInitialized` para asegurar que WebAssembly se ha cargado.
if (typeof cv !== 'undefined') {
if (cv.Mat) { // Si ya está listo (poco probable tan temprano), resolvemos.
resolve();
console.log("Cube Engine: OpenCV.js ya estaba listo al inicio.");
} else {
cv.onRuntimeInitialized = () => {
resolve();
console.log("Cube Engine: OpenCV.js onRuntimeInitialized disparado. Librería lista.");
};
}
}
});
// --- End Global readiness promises ---
// --- START NEW MODULE: ImageAnalyzer (CORREGIDO) ---
// (Pega el código completo del módulo ImageAnalyzer aquí)
// --- END NEW MODULE: ImageAnalyzer ---
// --- START NEW MODULE: ShapeDetector (CORREGIDO) ---
// (Pega el código completo del módulo ShapeDetector aquí)
// --- END NEW MODULE: ShapeDetector ---
(function BotClient() {
const QBit = globalThis[arguments[0]];
function parseServerUrl(any) {
var prefix = String(any).length == 1 ? `sv${any}.` : "";
return `wss://${prefix}drawaria.online/socket.io/?sid1=undefined&hostname=drawaria.online&EIO=3&transport=websocket`;
}
function parseRoomId(any) {
return String(any).match(/([a-f0-9.-]+?)$/gi)[0];
}
function parseSocketIOEvent(prefix_length, event_data) {
try {
return JSON.parse(event_data.slice(prefix_length));
} catch (error) {}
}
function parseAvatarURL(arr = []) {
return `https://drawaria.online/avatar/cache/${arr.length > 0 ? arr.join(".") : "default"}.jpg`;
}
// CLASE BOTCLIENT MODIFICADA PARA HEREDAR DE QBit
class BotClient extends QBit { // <--- ESTA LÍNEA FUE MODIFICADA/DESCOMENTADA
static dummy1 = QBit.register(this);
// constructor(name = '', avatar = []) {
constructor(name = "JavaScript", avatar = ["86e33830-86ea-11ec-8553-bff27824cf71"]) {
super(name, `<img src="${parseAvatarURL(avatar)}">`); // Asegura que se llama al constructor de la clase padre
this.name = name; // Asegura que el nombre del bot se almacene para 'notify'
this.avatar = avatar;
this.attributes = { spawned: false, rounded: false, status: false };
this.url = "";
this.socket = null;
this.interval_id = 0;
this.interval_ms = 25000;
this.room = {
id: null,
config: null,
type: 2,
players: [],
};
this.customObservers = [
{
event: "mc_roomplayerschange",
callback: (data) => {
this.room.players = data[2];
},
},
];
}
getReadyState() {
const localThis = this;
if (!localThis.socket) return false;
return localThis.socket.readyState == localThis.socket.OPEN;
}
connect(url) {
const localThis = this;
// if (localThis.getReadyState()) localThis.disconnect();
if (localThis.getReadyState()) return;
if (!url) return localThis.enterRoom(document.querySelector("#invurl").value);
localThis.socket = new WebSocket(parseServerUrl(url));
localThis.socket.addEventListener("open", function (event) {
localThis.interval_id = setInterval(function () {
if (!localThis.getReadyState()) return clearInterval(localThis.interval_id);
localThis.send(2);
}, localThis.interval_ms);
});
localThis.socket.addEventListener("message", function (message_event) {
var prefix = String(message_event.data).match(/(^\d+)/gi)[0] || "";
if (prefix == "40") {
localThis.send(emits.startplay(localThis.room, localThis.name, localThis.avatar));
}
var data = parseSocketIOEvent(prefix.length, message_event.data) || [];
if (data && data.length == 1) {
if (data[0].players) localThis.room.players = data[0].players;
}
if (data && data.length > 1) {
var event = data.shift();
localThis.customObservers.forEach((listener) => {
if (listener.event === event) if (listener.callback) listener.callback(data);
});
}
});
}
disconnect() {
if (!this.getReadyState()) return;
this.socket.close();
}
reconnect() {
this.send(41);
this.send(40);
}
enterRoom(roomid) {
this.room.id = parseRoomId(roomid);
if (!this.getReadyState()) this.connect(this.room.id.includes(".") ? this.room.id.slice(-1) : "");
this.reconnect();
}
addEventListener(eventname, callback) {
this.customObservers.push({ event: eventname, callback });
}
send(data) {
if (!this.getReadyState()) return /*console.warn(data)*/;
this.socket.send(data);
}
emit(event, ...data) {
// data = data.length > 0 ? data : null;
var emitter = emits[event];
if (emitter) this.send(emitter(...data));
}
}
const emits = {
chatmsg: function (message) {
// 42["chatmsg","a"]
let data = ["chatmsg", message];
return `${42}${JSON.stringify(data)}`;
},
passturn: function () {
// 42["passturn"]
let data = ["passturn"];
return `${42}${JSON.stringify(data)}`;
},
pgdrawvote: function (playerid) {
// 42["pgdrawvote",2,0]
let data = ["pgdrawvote", playerid, 0];
return `${42}${JSON.stringify(data)}`;
},
pgswtichroom: function () {
// 42["pgswtichroom"]
let data = ["pgswtichroom"];
return `${42}${JSON.stringify(data)}`;
},
playerafk: function () {
// 42["playerafk"]
let data = ["playerafk"];
return `${42}${JSON.stringify(data)}`;
},
playerrated: function () {
// 42["playerrated"]
let data = ["playerrated"];
return `${42}${JSON.stringify(data)}`;
},
sendgesture: function (gestureid) {
// 42["sendgesture",16]
let data = ["sendgesture", gestureid];
return `${42}${JSON.stringify(data)}`;
},
sendvote: function () {
// 42["sendvote"]
let data = ["sendvote"];
return `${42}${JSON.stringify(data)}`;
},
sendvotekick: function (playerid) {
// 42["sendvotekick",93]
let data = ["sendvotekick", playerid];
return `${42}${JSON.stringify(data)}`;
},
wordselected: function (wordid) {
// 42["wordselected",0]
let data = ["sendvotekick", wordid];
return `${42}${JSON.stringify(data)}`;
},
activateitem: function (itemid, isactive) {
let data = ["clientcmd", 12, [itemid, isactive]];
return `${42}${JSON.stringify(data)}`;
},
buyitem: function (itemid) {
let data = ["clientcmd", 11, [itemid]];
return `${42}${JSON.stringify(data)}`;
},
canvasobj_changeattr: function (itemid, target, value) {
// target = zindex || shared
let data = ["clientcmd", 234, [itemid, target, value]];
return `${42}${JSON.stringify(data)}`;
},
canvasobj_getobjects: function () {
let data = ["clientcmd", 233];
return `${42}${JSON.stringify(data)}`;
},
canvasobj_remove: function (itemid) {
let data = ["clientcmd", 232, [itemid]];
return `${42}${JSON.stringify(data)}`;
},
canvasobj_setposition: function (itemid, positionX, positionY, speed) {
let data = ["clientcmd", 230, [itemid, 100 / positionX, 100 / positionY, { movespeed: speed }]];
return `${42}${JSON.stringify(data)}`;
},
canvasobj_setrotation: function (itemid, rotation) {
let data = ["clientcmd", 231, [itemid, rotation]];
return `${42}${JSON.stringify(data)}`;
},
customvoting_setvote: function (value) {
let data = ["clientcmd", 301, [value]];
return `${42}${JSON.stringify(data)}`;
},
getfpid: function (value) {
let data = ["clientcmd", 901, [value]];
return `${42}${JSON.stringify(data)}`;
},
getinventory: function () {
let data = ["clientcmd", 10, [true]];
return `${42}${JSON.stringify(data)}`;
},
getspawnsstate: function () {
let data = ["clientcmd", 102];
return `${42}${JSON.stringify(data)}`;
},
moveavatar: function (positionX, positionY) {
let data = [
"clientcmd",
103,
[1e4 * Math.floor((positionX / 100) * 1e4) + Math.floor((positionY / 100) * 1e4), false],
];
return `${42}${JSON.stringify(data)}`;
},
setavatarprop: function () {
let data = ["clientcmd", 115];
return `${42}${JSON.stringify(data)}`;
},
setstatusflag: function (flagid, isactive) {
let data = ["clientcmd", 3, [flagid, isactive]];
return `${42}${JSON.stringify(data)}`;
},
settoken: function (playerid, tokenid) {
let data = ["clientcmd", 2, [playerid, tokenid]];
return `${42}${JSON.stringify(data)}`;
},
snapchatmessage: function (playerid, value) {
let data = ["clientcmd", 330, [playerid, value]];
return `${42}${JSON.stringify(data)}`;
},
spawnavatar: function () {
let data = ["clientcmd", 101];
return `${42}${JSON.stringify(data)}`;
},
startrollbackvoting: function () {
let data = ["clientcmd", 320];
return `${42}${JSON.stringify(data)}`;
},
trackforwardvoting: function () {
let data = ["clientcmd", 321];
return `${42}${JSON.stringify(data)}`;
},
startplay: function (room, name, avatar) {
let data = `${420}${JSON.stringify([
"startplay",
name,
room.type,
"en",
room.id,
null,
[null, "https://drawaria.online/", 1000, 1000, [null, avatar[0], avatar[1]], null],
])}`;
return data;
},
votetrack: function (trackid) {
let data = ["clientcmd", 1, [trackid]];
return `${42}${JSON.stringify(data)}`;
},
requestcanvas: function (playerid) {
let data = ["clientnotify", playerid, 10001];
return `${42}${JSON.stringify(data)}`;
},
respondcanvas: function (playerid, base64) {
let data = ["clientnotify", playerid, 10002, [base64]];
return `${42}${JSON.stringify(data)}`;
},
galleryupload: function (playerid, imageid) {
let data = ["clientnotify", playerid, 11, [imageid]];
return `${42}${JSON.stringify(data)}`;
},
warning: function (playerid, type) {
let data = ["clientnotify", playerid, 100, [type]];
return `${42}${JSON.stringify(data)}`;
},
mute: function (playerid, targetname, mute = 0) {
let data = ["clientnotify", playerid, 1, [mute, targetname]];
return `${42}${JSON.stringify(data)}`;
},
hide: function (playerid, targetname, hide = 0) {
let data = ["clientnotify", playerid, 3, [hide, targetname]];
return `${42}${JSON.stringify(data)}`;
},
report: function (playerid, reason, targetname) {
let data = ["clientnotify", playerid, 2, [targetname, reason]];
return `${42}${JSON.stringify(data)}`;
},
line: function (playerid, lastx, lasty, x, y, isactive, size, color, ispixel) {
let data = [
"drawcmd",
0,
[lastx / 100, lasty / 100, x / 100, y / 100, isactive, -size, color, playerid, ispixel],
];
return `${42}${JSON.stringify(data)}`;
},
erase: function (playerid, lastx, lasty, x, y, isactive, size, color) {
let data = ["drawcmd", 1, [lastx / 100, lasty / 100, x / 100, y / 100, isactive, -size, color, playerid]];
return `${42}${JSON.stringify(data)}`;
},
flood: function (x, y, color, size, r, g, b, a) {
// 42["drawcmd",2,[x, y,color,{"0":r,"1":g,"2":b,"3":a},size]]
let data = ["drawcmd", 2, [x / 100, y / 100, color, { 0: r, 1: g, 2: b, 3: a }, size]];
return `${42}${JSON.stringify(data)}`;
},
undo: function (playerid) {
// 42["drawcmd",3,[playerid]]
let data = ["drawcmd", 3, [playerid]];
return `${42}${JSON.stringify(data)}`;
},
clear: function () {
// 42["drawcmd",4,[]]
let data = ["drawcmd", 4, []];
return `${42}${JSON.stringify(data)}`;
},
noop: function () {
// 42["drawcmd",5,[0.44882022129015975,0.3157894736842105,0.44882022129015975,0.3157894736842105,true,-12,"#000000",playerid]]
},
};
const events = {
bc_announcement: function (data) {
//
},
bc_chatmessage: function (data) {
// 42["bc_chatmessage",3,"playername","a"]
},
bc_clearcanvasobj: function (data) {
//
},
bc_clientnotify: function (data) {
// 42["bc_clientnotify",playerid,"playername",code,null]
},
bc_createcanvasobj: function (data) {
// 42["bc_createcanvasobj","1",[3,63001,0.5,0.5,0,1,null,"1",true]]
},
bc_customvoting_abort: function (data) {
//
},
bc_customvoting_error: function (data) {
// 42["bc_customvoting_error","rollbackcanvas"]
},
bc_customvoting_results: function (data) {
// 42["bc_customvoting_results",[2],true,0]
},
bc_customvoting_start: function (data) {
// 42["bc_customvoting_start",{"type":321,"secs":20,"acceptratios":[0.51],"pgdrawallow":true,"voteoptions":["YES","NO"]},1]
},
bc_customvoting_vote: function (data) {
// 42["bc_customvoting_vote",1,0,[2,1,[1]]]
},
bc_exp: function (data) {
// 42["bc_exp",29,4357]
},
bc_extannouncement: function (data) {
//
},
bc_freedrawsession_reset: function (data) {
// 42["bc_freedrawsession_reset",-1,{"votingtype":2,"currentvotes":0,"neededvotes":2,"votingtimeout":null}null]
},
bc_gesture: function (data) {
// 42["bc_gesture",3,31]
},
bc_musicbox_play: function (data) {
// 42["bc_musicbox_play",[30394,1,"37678185",252386,1661295694733,"Sony Masterworks - Smooth Criminal","2cellos/smooth-criminal"]]
},
bc_musicbox_vote: function (data) {
// 42["bc_musicbox_vote",[[30394,1]],3,30394]
},
bc_pgdrawallow_results: function (data) {
// 42["bc_pgdrawallow_results",2,true,true]
},
bc_pgdrawallow_startvoting: function (data) {
// 42["bc_pgdrawallow_startvoting",2,1,false]
},
bc_pgdrawallow_vote: function (data) {
// 42["bc_pgdrawallow_vote",2,1,0,false,[1,0]]
},
bc_playerafk: function (data) {
// 42["bc_playerafk",28,"Jinx"]
},
bc_playerrated: function (data) {
// 42["bc_playerrated",1,29,"lil cute girl",28,"Jinx",[1]]
},
bc_removecanvasobj: function (data) {
// 42["bc_removecanvasobj",3,"1",null]
},
bc_resetplayername: function (data) {
//
},
bc_round_results: function (data) {
// 42["bc_round_results",[[5,"Jinx",15,61937,3,"63196790-c7da-11ec-8266-c399f90709b7",0],[4,"ツ♡thick mojo ♡ツ",15,65464,3,"018cdc20-47a4-11ec-b5b5-6bdacecdd51e",1]]]
},
bc_setavatarprop: function (data) {
// 42["bc_setavatarprop",3]
},
bc_setobjattr: function (data) {
// 42["bc_setobjattr","1","shared",false]
},
bc_setstatusflag: function (data) {
// 42["bc_setstatusflag",3,3,true]
},
bc_spawnavatar: function (data) {
// 42["bc_spawnavatar",3,true]
},
bc_startinground: function (data) {
// 42["bc_startinground",200000,[],{"votingtype":0,"currentvotes":0,"neededvotes":2,"votingtimeout":null}]
},
bc_token: function (data) {
// 42["bc_token",1,3,0]
},
bc_turn_abort: function (data) {
// 42["bc_turn_abort","pass","lil cute girl","2c276aa0-dc5e-11ec-9fd3-c3a00b129da4","hammer",null]
},
bc_turn_fastout: function (data) {
// 42["bc_turn_fastout",15000]
},
bc_turn_results: function (data) {
// 42["bc_turn_results",[[1,"Jinx",2,2,"63196790-c7da-11ec-8266-c399f90709b7",0,0],[2,"vale",3,3,"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.xxxxxxxxxxxxx",9248]],"cavern"]
},
bc_turn_waitplayers: function (data) {
// 42["bc_turn_waitplayers",true,-1,6]
},
bc_uc_freedrawsession_changedroom: function (data) {
// console.log(data[2], data[3])
// 42["bc_uc_freedrawsession_changedroom",[list of drawlines not !important]]
},
bc_uc_freedrawsession_start: function (data) {
//
},
bc_votekick: function (data) {
// 42["bc_votekick","Jinx",22,true]
},
bc_votingtimeout: function (data) {
// 42["bc_votingtimeout",{"votingtype":2,"currentvotes":0,"neededvotes":2,"votingtimeout":null}]
},
bcmc_playervote: function (data) {
// 42["bcmc_playervote","playername",{"votingtype":3,"currentvotes":1,"neededvotes":2,"votingtimeout":1661296731309}]
},
bcuc_getfpid: function (data) {
// 42["bcuc_getfpid"]
// 42["clientcmd",901,[{"visitorId":"a8923f0870050d4a4e771cd26679ab6e"}]]
},
bcuc_itemactivated: function (data) {
// 42["bcuc_itemactivated",3,63001,[2,[0.5,0.5],0,1,null],1]
},
bcuc_itemactivationabort: function (data) {
//
},
bcuc_moderatormsg: function (data) {
// 42["bcuc_moderatormsg","Kick Player",true]
},
bcuc_snapchatmessage: function (data) {
// 42["uc_snapshot","1671740010120.1.28028"]
// https://drawaria.online/snapshot/save
},
uc_avatarspawninfo: function (data) {
// 42["uc_avatarspawninfo","9a2ab5b2-b81e-4690-9af7-475d870d6e20",[[38,75059625,0]]]
},
uc_buyitemerror: function (data) {
//
},
uc_canvasobjs: function (data) {
// 42["uc_canvasobjs","9a2ab5b2-b81e-4690-9af7-475d870d6e20",{}]
},
uc_chatmuted: function (data) {
// 42["uc_chatmuted"]
},
uc_coins: function (data) {
// 42["uc_coins",-50,43]
},
uc_inventoryitems: function (data) {
// 42["uc_inventoryitems",[[100,99,null],[63000,null,null],[86000,null,null]],false,false] list
},
uc_resetavatar: function (data) {
//
},
uc_serverserstart: function (data) {
//
},
uc_snapshot: function (data) {
//
},
uc_tokenerror: function (data) {
// 42["uc_tokenerror",2]
},
uc_turn_begindraw: function (data) {
// 42["uc_turn_begindraw",90000,"arrow"]
},
uc_turn_selectword: function (data) {
// 42["uc_turn_selectword",11000,["vase","cellar","rain"],1,7,false]
},
uc_turn_selectword_refreshlist: function (data) {
// 42["uc_turn_selectword_refreshlist",["crayons","trade","team"]]
},
uc_turn_wordguessedlocalThis: function (data) {
// 42["uc_turn_wordguessedlocalThis","stage",3,[[2,3,3,53938],[1,2,2,0]]]
},
};
globalThis["_io"] = { events, emits };
})("QBit");
// YOUTUBEDRAWARIA
// ... (Tu código existente hasta aquí, incluyendo las promesas globales y otros módulos) ...
// --- START NEW MODULE: MagicTools (COMBINADO - SOLUCIÓN DEFINITIVA BOTÓN "COLOR RÁPIDO") ---
(function MagicToolsModule() {
const QBit = globalThis[arguments[0]];
// All Known Elements for Hide/Show Menus (copied from HideShowMenusModule)
const allKnownElements = [
{ selector: '#canvas', label: 'Canvas' },
{ selector: '#leftbar', label: 'Left Sidebar' },
{ selector: '#rightbar', label: 'Right Sidebar' },
{ selector: '#playerlist', label: 'Player List' },
{ selector: '#cyclestatus', label: 'Cycle Status' },
{ selector: '#votingbox', label: 'Voting Box' },
{ selector: '#passturnbutton', label: 'Pass Turn Button' },
{ selector: '.timer', label: 'Round Timer' },
{ selector: '#roomcontrols', label: 'Room Controls' },
{ selector: '#infotext', label: 'Info Text' },
{ selector: '#gesturespickerselector', label: 'Gestures Picker' },
{ selector: '#chatbox_messages', label: 'Chat Messages' },
{ selector: '#drawcontrols', label: 'Draw Controls' },
{ selector: '#turnresults', label: 'Turn Results' },
{ selector: '#roundresults', label: 'Round Results' },
{ selector: '#snapmessage_container', label: 'Snap Message Container' },
{ selector: '#accountbox', label: 'Account Box' },
{ selector: '#customvotingbox', label: 'Custom Voting Box' },
{ selector: '#showextmenu', label: 'Show Ext Menu Button' },
{ selector: '#playgroundroom_next', label: 'Playground Next Button' },
{ selector: '#homebutton', label: 'Home Button' },
{ selector: '.invbox', label: 'Invitation Box' },
{ selector: '#howtoplaydialog', label: 'How to Play' },
{ selector: '#newroomdialog', label: 'New Room Options' },
{ selector: '#musictracks', label: 'Music Tracks' },
{ selector: '#inventorydlg', label: 'Inventory' },
{ selector: '#extmenu', label: 'Extra Menu' },
{ selector: '#pressureconfigdlg', label: 'Pressure Settings' },
{ selector: '#palettechooser', label: 'Palette Chooser' },
{ selector: '#wordchooser', label: 'Word Chooser' },
{ selector: '#targetword', label: 'Target Word Info' },
{ selector: '#invitedlg', label: 'Invite Dialog' },
{ selector: '#reportdlg', label: 'Report Dialog' },
{ selector: '#turtabortedmsg', label: 'Turn Aborted Msg' },
{ selector: '#drawsettings', label: 'Draw Settings' },
{ selector: '.modal-header', label: 'Header (Any)' },
{ selector: '.modal-body', label: 'Body (Any)' },
{ selector: '.modal-footer', label: 'Footer (Any)' },
{ selector: '.form-group', label: 'Form Group (Any)' },
{ selector: '.table', label: 'Table (Any)' },
{ selector: '.spinner-border', label: 'Spinner/Loading Icon (Any)' },
];
QBit.Styles.addRules([
`#${QBit.identifier} .magic-tools-section {
border: 1px solid var(--CE-color);
border-radius: .25rem;
padding: 5px;
margin-bottom: 10px;
background-color: var(--CE-bg_color);
}`,
`#${QBit.identifier} .magic-tools-section-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--dark-blue-title);
text-align: center;
}`,
`#${QBit.identifier} .magic-tools-toggle-button { /* Base style for all toggle buttons */
background-color: var(--secondary);
color: var(--dark);
}`,
`#${QBit.identifier} .magic-tools-toggle-button.active { /* Active state for toggle buttons */
background-color: var(--info);
color: white;
}`,
`#${QBit.identifier} .magic-tools-controls-row > * {
flex: 1;
}`,
`#${QBit.identifier} .magic-tools-selector {
border: 1px solid var(--input-border-blue);
background-color: var(--input-bg);
color: var(--dark-text);
padding: 5px;
width: 100%;
box-sizing: border-box;
margin-bottom: 5px;
}`
// OLD CSS related to .magic-tool-icon-toggle and .custom-checkbox-indicator has been removed
]);
class MagicTools extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
// BiggerBrush / BetterBrush
#biggerBrushActive = false;
#betterBrushActive = false;
#drawwidthrangeSlider; // For BiggerBrush
#betterBrushVisibilityObserver; // For BetterBrush popuplist
#rapidColorChangeButton; // Reference to the new button for Rapid Color Change
// BiggerStencil
#biggerStencilActive = false;
#biggerStencilAccessibilityObserver;
// Hide/Show Menus
#hideShowElementSelector;
#hideShowMenusObserver;
constructor() {
super("Herramientas Mágicas", '<i class="fas fa-magic"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#setupDrawControlsObservers(); // For BiggerBrush, BetterBrush, BiggerStencil
this.#setupHideShowMenusObserver(); // For Hide/Show Menus
this.#populateHideShowSelector(); // Initial population
}
#loadInterface() {
const container = domMake.Tree("div");
// --- Section: Herramientas de Pincel ---
const brushToolsSection = domMake.Tree("div", { class: "magic-tools-section" });
brushToolsSection.appendChild(domMake.Tree("div", { class: "magic-tools-section-title" }, ["Herramientas de Pincel"]));
// BiggerBrush Toggle
const biggerBrushRow = domMake.Row();
const biggerBrushButton = domMake.Button('<i class="fas fa-brush"></i> Pincel<br>Grande');
biggerBrushButton.classList.add("magic-tools-toggle-button");
biggerBrushButton.addEventListener("click", () => this.#toggleBiggerBrush(biggerBrushButton));
biggerBrushRow.appendChild(biggerBrushButton);
brushToolsSection.appendChild(biggerBrushRow);
// BetterBrush Toggle
const betterBrushRow = domMake.Row();
const betterBrushButton = domMake.Button('<i class="fas fa-magic"></i> Pincel<br>Mejorado');
betterBrushButton.classList.add("magic-tools-toggle-button");
betterBrushButton.addEventListener("click", () => this.#toggleBetterBrush(betterBrushButton));
betterBrushRow.appendChild(betterBrushButton);
brushToolsSection.appendChild(betterBrushRow);
// NEW: Rapid Color Change button in its own row, directly below BetterBrush
const rapidColorChangeRow = domMake.Row();
this.#rapidColorChangeButton = domMake.Button('<i class="fas fa-adjust"></i> Color<br>Rápido'); // Use <br> for newline
this.#rapidColorChangeButton.classList.add("magic-tools-toggle-button");
this.#rapidColorChangeButton.addEventListener("click", () => this.#toggleRapidColorChange(this.#rapidColorChangeButton));
rapidColorChangeRow.appendChild(this.#rapidColorChangeButton);
brushToolsSection.appendChild(rapidColorChangeRow);
// BiggerStencil Toggle
const biggerStencilRow = domMake.Row();
const biggerStencilButton = domMake.Button('<i class="fas fa-object-group"></i> Plantillas<br>Grandes');
biggerStencilButton.classList.add("magic-tools-toggle-button");
biggerStencilButton.addEventListener("click", () => this.#toggleBiggerStencil(biggerStencilButton));
biggerStencilRow.appendChild(biggerStencilButton);
brushToolsSection.appendChild(biggerStencilRow);
container.appendChild(brushToolsSection);
// --- Section: Exportar Chat ---
const exportChatSection = domMake.Tree("div", { class: "magic-tools-section" });
exportChatSection.appendChild(domMake.Tree("div", { class: "magic-tools-section-title" }, ["Exportar Chat"]));
const exportChatRow = domMake.Row();
const exportChatButton = domMake.Button('<i class="fas fa-file-export"></i> Exportar<br>Chat (TXT)');
exportChatButton.title = "Exporta todos los mensajes del chat a un archivo de texto.";
exportChatButton.addEventListener("click", () => this.#exportChatMessages());
exportChatRow.appendChild(exportChatButton);
exportChatSection.appendChild(exportChatRow);
container.appendChild(exportChatSection);
// --- Section: Ocultar/Mostrar Menús ---
const hideShowMenusSection = domMake.Tree("div", { class: "magic-tools-section" });
hideShowMenusSection.appendChild(domMake.Tree("div", { class: "magic-tools-section-title" }, ["Ocultar/Mostrar Menús"]));
this.#hideShowElementSelector = domMake.Tree("select", { id: "magic-tools-element-selector", class: "magic-tools-selector" });
hideShowMenusSection.appendChild(this.#hideShowElementSelector);
const hideShowButtonRow = domMake.Row({ class: "action-buttons" });
const showButton = domMake.Button("Mostrar");
showButton.addEventListener('click', () => this.#toggleElementVisibility(false));
const hideButton = domMake.Button("Ocultar");
hideButton.addEventListener('click', () => this.#toggleElementVisibility(true));
hideShowButtonRow.appendAll(showButton, hideButton);
hideShowMenusSection.appendChild(hideShowButtonRow);
container.appendChild(hideShowMenusSection);
this.htmlElements.section.appendChild(container);
}
// --- BiggerBrush Logic ---
#toggleBiggerBrush(button) {
this.#biggerBrushActive = !this.#biggerBrushActive;
button.classList.toggle("active", this.#biggerBrushActive);
if (!this.drawwidthrangeSlider) {
this.drawwidthrangeSlider = document.querySelector("#drawwidthrange");
if (!this.drawwidthrangeSlider) {
this.notify("error", "Slider de ancho de dibujo no encontrado.");
this.#biggerBrushActive = false;
button.classList.remove("active");
return;
}
}
if (this.#biggerBrushActive) {
document.querySelectorAll(".drawcontrols-button").forEach((n) => {
n.classList.remove("drawcontrols-disabled");
});
button.innerHTML = '<i class="fas fa-paint-brush"></i> Pincel Grande<br>Activo';
this.drawwidthrangeSlider.parentElement.previousElementSibling.lastElementChild.click();
this.drawwidthrangeSlider.parentElement.style.display = "flex";
this.drawwidthrangeSlider.max = 48;
this.drawwidthrangeSlider.min = -2000;
this.notify("success", "Pincel Grande activado.");
} else {
button.innerHTML = '<i class="fas fa-paint-brush"></i> Pincel<br>Grande';
this.drawwidthrangeSlider.max = 45;
this.drawwidthrangeSlider.min = -100;
this.notify("warning", "Pincel Grande desactivado.");
}
}
// --- BetterBrush Logic ---
#toggleBetterBrush(button) {
this.#betterBrushActive = !this.#betterBrushActive;
button.classList.toggle("active", this.#betterBrushActive);
button.innerHTML = this.#betterBrushActive
? '<i class="fas fa-magic"></i> Pincel Mejorado<br>Activo'
: '<i class="fas fa-magic"></i> Pincel<br>Mejorado';
this.notify("info", `Pincel Mejorado ${this.#betterBrushActive ? 'activado' : 'desactivado'}.`);
}
// --- Rapid Color Change Logic ---
#toggleRapidColorChange(button) {
const colorflowSpeedInput = document.querySelector('input[data-localprop="colorflow"]');
const colorflowTypeSelect = document.querySelector('select[data-localprop="colorautochange"]');
const settingsContainer = document.querySelector(".drawcontrols-settingscontainer:has([data-localprop='colorautochange'])");
if (!colorflowSpeedInput || !colorflowTypeSelect || !settingsContainer) {
this.notify("warning", "Controles de flujo de color no encontrados. Asegúrate de que los controles de dibujo están visibles.");
return;
}
const isActive = button.classList.contains("active");
const newActiveState = !isActive;
button.classList.toggle("active", newActiveState);
button.innerHTML = newActiveState
? '<i class="fas fa-adjust"></i> Color Rápido<br>Activo'
: '<i class="fas fa-adjust"></i> Color<br>Rápido';
if (newActiveState) {
colorflowTypeSelect.value = "2";
colorflowSpeedInput.max = 10;
colorflowSpeedInput.value = 10;
this.notify("info", "Cambio Rápido de Color activado.");
} else {
colorflowTypeSelect.value = "0";
colorflowSpeedInput.max = 1;
colorflowSpeedInput.value = 0;
this.notify("info", "Cambio Rápido de Color desactivado.");
}
settingsContainer.dispatchEvent(new CustomEvent("change"));
}
// --- BiggerStencil Logic ---
#toggleBiggerStencil(button) {
this.#biggerStencilActive = !this.#biggerStencilActive;
button.classList.toggle("active", this.#biggerStencilActive);
button.innerHTML = this.#biggerStencilActive
? '<i class="fas fa-object-group"></i> Plantillas Grandes<br>Activo'
: '<i class="fas fa-object-group"></i> Plantillas<br>Grandes';
this.notify("info", `Plantillas Grandes ${this.#biggerStencilActive ? 'activado' : 'desactivado'}.`);
const targetStencilButton = document.querySelector(".fa-parachute-box")?.parentElement;
if (!targetStencilButton) {
this.notify("warning", "Botón de Plantillas no encontrado.");
return;
}
if (this.#biggerStencilActive) {
if (targetStencilButton.disabled) {
targetStencilButton.disabled = "";
}
}
}
#setupDrawControlsObservers() {
const betterBrushTarget = document.querySelector(".drawcontrols-popuplist");
if (betterBrushTarget) {
this.#betterBrushVisibilityObserver = new MutationObserver((mutations) => {
if (this.#betterBrushActive) {
if (mutations[0].target.style.display !== "none") {
mutations[0].target.querySelectorAll("div").forEach((n) => {
n.removeAttribute("style");
});
}
}
});
this.#betterBrushVisibilityObserver.observe(betterBrushTarget, { attributes: true, attributeFilter: ['style'] });
} else {
this.notify("warning", "Contenedor de pop-up de pincel no encontrado para BetterBrush.");
}
const biggerStencilTarget = document.querySelector(".fa-parachute-box")?.parentElement;
if (biggerStencilTarget) {
this.#biggerStencilAccessibilityObserver = new MutationObserver((mutations) => {
if (this.#biggerStencilActive) {
if (mutations[0].target.disabled) {
mutations[0].target.disabled = "";
}
}
});
this.#biggerStencilAccessibilityObserver.observe(biggerStencilTarget, { attributes: true, attributeFilter: ['disabled'] });
} else {
this.notify("warning", "Botón de esténcil no encontrado para BiggerStencil.");
}
this.drawwidthrangeSlider = document.querySelector("#drawwidthrange");
if (!this.drawwidthrangeSlider) {
this.notify("warning", "Slider de ancho de dibujo no encontrado para BiggerBrush.");
}
}
// --- Export Chat Logic ---
#exportChatMessages() {
const chatbox = document.getElementById('chatbox_messages');
if (!chatbox) {
this.notify("warning", "Contenedor de chat no encontrado.");
return;
}
const messages = chatbox.querySelectorAll('div.chatmessage');
let exportedMessages = [];
messages.forEach(message => {
let timestamp = message.dataset.ts ? new Date(parseInt(message.dataset.ts)).toLocaleTimeString() : 'N/A';
if (message.classList.contains('systemchatmessage') || message.classList.contains('systemchatmessage5')) {
exportedMessages.push(`[${timestamp}] [Sistema] ${message.textContent.trim().replace(/^System: /, '')}`);
} else if (message.classList.contains('playerchatmessage-highlightable') || message.classList.contains('chatmessage')) {
const playerName = message.querySelector('.playerchatmessage-name')?.textContent?.trim() || 'Desconocido';
const playerMessage = message.querySelector('.playerchatmessage-text')?.textContent?.trim() || '';
exportedMessages.push(`[${timestamp}] ${playerName}: ${playerMessage}`);
}
});
if (exportedMessages.length === 0) {
this.notify("info", "No hay mensajes en el chat para exportar.");
return;
}
const blob = new Blob([exportedMessages.join('\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `drawaria_chat_${new Date().toISOString().slice(0, 10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.notify("success", "Chat exportado exitosamente.");
}
// --- Hide/Show Menus Logic ---
#populateHideShowSelector() {
const currentSelectedValue = this.#hideShowElementSelector.value;
this.#hideShowElementSelector.innerHTML = '';
const addedSelectors = new Set();
const placeholderOption = domMake.Tree('option', { value: '' }, ['-- Selecciona un elemento --']);
this.#hideShowElementSelector.appendChild(placeholderOption);
allKnownElements.forEach(item => {
try {
if (document.querySelector(item.selector) && !addedSelectors.has(item.selector)) {
const option = domMake.Tree('option', { value: item.selector }, [item.label]);
this.#hideShowElementSelector.appendChild(option);
addedSelectors.add(item.selector);
}
} catch (e) {
console.warn(`[MagicTools - HideShow] Selector inválido: ${item.selector}. Error: ${e.message}`);
}
});
if (currentSelectedValue && Array.from(this.#hideShowElementSelector.options).some(opt => opt.value === currentSelectedValue)) {
this.#hideShowElementSelector.value = currentSelectedValue;
} else {
this.#hideShowElementSelector.value = '';
}
}
#toggleElementVisibility(hide) {
const selectedValue = this.#hideShowElementSelector.value;
if (!selectedValue) {
this.notify("warning", "No hay elemento seleccionado.");
return;
}
try {
document.querySelectorAll(selectedValue).forEach(el => {
if (hide) {
if (el.style.display && el.style.display !== 'none') {
el.dataset.originalDisplay = el.style.display;
}
el.style.display = 'none';
el.style.visibility = 'hidden';
if (selectedValue.includes('.modal-backdrop')) {
el.remove();
}
this.notify("info", `Ocultando: ${selectedValue}`);
} else {
if (el.dataset.originalDisplay) {
el.style.display = el.dataset.originalDisplay;
delete el.dataset.originalDisplay;
} else {
el.style.display = '';
}
el.style.visibility = '';
this.notify("info", `Mostrando: ${selectedValue}`);
}
});
} catch (e) {
this.notify("error", `Error al ${hide ? 'ocultar' : 'mostrar'} el elemento ${selectedValue}: ${e.message}`);
}
}
#setupHideShowMenusObserver() {
if (this.#hideShowMenusObserver) {
this.#hideShowMenusObserver.disconnect();
}
this.#hideShowMenusObserver = new MutationObserver(() => {
clearTimeout(this.#hideShowMenusObserver._timer);
this.#hideShowMenusObserver._timer = setTimeout(() => {
this.#populateHideShowSelector();
}, 500);
});
this.#hideShowMenusObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
}
}
})("QBit");
// --- END NEW MODULE: MagicTools ---
// ... (Tu código existente continúa con los demás módulos) ...
(function GhostCanvas() {
const QBit = globalThis[arguments[0]];
const Await = QBit.findGlobal("Await");
QBit.Styles.addRule(
".ghostimage { position:fixed; top:50%; left:50%; opacity:.6; box-shadow: 0 0 1px 1px cornflowerblue inset; }"
);
function getBoundingClientRect(htmlElement) {
let { top, right, bottom, left, width, height, x, y } = htmlElement.getBoundingClientRect();
top = Number(top).toFixed();
right = Number(right).toFixed();
bottom = Number(bottom).toFixed();
left = Number(left).toFixed();
width = Number(width).toFixed();
height = Number(height).toFixed();
x = Number(x).toFixed();
y = Number(y).toFixed();
return { top, right, bottom, left, width, height, x, y };
}
function makeDragable(draggableElement, update) {
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
draggableElement.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
draggableElement.style.top = Number(draggableElement.offsetTop - pos2).toFixed() + "px";
draggableElement.style.left = Number(draggableElement.offsetLeft - pos1).toFixed() + "px";
update();
}
function closeDragElement() {
/* stop moving when mouse button is released:*/
document.onmouseup = null;
document.onmousemove = null;
}
}
const radios = [];
class GhostCanvas extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
static isFavorite = true;
GameCanvas;
DrawCanvas;
DrawCanvasContext;
DrawCanvasRect;
loadedImages;
drawingManager;
constructor() {
super("GhostCanvas", '<i class="fas fa-images"></i>');
this.GameCanvas = document.body.querySelector("canvas#canvas");
this.DrawCanvas = document.createElement("canvas");
this.DrawCanvasRect = {};
this.loadedImages = [];
this.DrawCanvasContext = this.DrawCanvas.getContext("2d");
this.drawingManager = new TaskManager(this);
this.#onStartup();
this.resetAllSettings();
}
#onStartup() {
this.#loadInterface();
this.DrawCanvas.width = this.GameCanvas.width;
this.DrawCanvas.height = this.GameCanvas.height;
this.DrawCanvas.style =
"position:fixed; opacity:.6; box-shadow: 0 0 1px 1px firebrick inset; pointer-events: none;";
const onResize = new Await(this.alignDrawCanvas.bind(this), 500);
window.addEventListener("resize", (event) => {
onResize.call();
});
this.htmlElements.input.addEventListener("change", (event) => {
if (this.htmlElements.input.checked) this.alignDrawCanvas();
});
}
#loadInterface() {
this.#row1();
this.#row2();
this.#row3();
this.#row4();
this.#row5();
}
#row1() {
const row = domMake.Row();
{
const resetSettingsButton = domMake.Button("Reset");
const showCanvasInput = domMake.Tree("input", { type: "checkbox", title: "Toggle Canvas", class: "icon" });
const clearCanvasButton = domMake.Button("Clear");
resetSettingsButton.title = "Reset Settings";
clearCanvasButton.title = "Clear GameCanvas";
resetSettingsButton.addEventListener("click", (event) => {
this.resetAllSettings();
});
showCanvasInput.addEventListener("change", () => {
this.DrawCanvas.style.display = showCanvasInput.checked ? "block" : "none";
});
clearCanvasButton.addEventListener("click", (event) => {
let data = ["drawcmd", 0, [0.5, 0.5, 0.5, 0.5, !0, -2000, "#FFFFFF", -1, !1]];
this.findGlobal("BotClientInterface").siblings[0].bot.send(`${42}${JSON.stringify(data)}`);
});
document.body.appendChild(this.DrawCanvas);
row.appendAll(resetSettingsButton, showCanvasInput, clearCanvasButton);
}
this.htmlElements.section.appendChild(row);
}
#row2() {
const row = domMake.Row();
{
const loadPixelDataButton = domMake.Button("Load");
const pixelsLeftToDraw = domMake.Tree("input", {
type: "text",
readonly: true,
style: "text-align: center;",
value: "0",
});
const clearPixelListButton = domMake.Button("Clear");
this.htmlElements.pixelsLeftToDraw = pixelsLeftToDraw;
loadPixelDataButton.title = "Load Pixels to draw";
clearPixelListButton.title = "Clear Pixels to draw";
loadPixelDataButton.addEventListener("click", (event) => {
this.saveCanvas();
});
clearPixelListButton.addEventListener("click", (event) => {
this.setPixelList([]);
});
row.appendAll(loadPixelDataButton, pixelsLeftToDraw, clearPixelListButton);
}
this.htmlElements.section.appendChild(row);
}
#row3() {
const row = domMake.Row();
{
const startDrawingButton = domMake.Button("Start");
const stopDrawingButton = domMake.Button("Stop");
startDrawingButton.addEventListener("click", (event) => {
this.drawingManager.startDrawing();
});
stopDrawingButton.addEventListener("click", (event) => {
this.drawingManager.stopDrawing();
});
row.appendAll(startDrawingButton, stopDrawingButton);
}
this.htmlElements.section.appendChild(row);
}
#row4() {
const row = domMake.Row();
{
const brushSizeInput = domMake.Tree("input", { type: "number", min: 2, value: 2, max: 200, step: 1 });
const singleColorModeInput = domMake.Tree("input", { type: "checkbox", class: "icon" });
const brushColorInput = domMake.Tree("input", { type: "text", value: "blue" });
brushSizeInput.addEventListener("change", (event) => {
this.drawingManager.brushSize = Number(brushSizeInput.value);
});
singleColorModeInput.addEventListener("change", (event) => {
this.drawingManager.singleColor = Boolean(singleColorModeInput.checked);
});
brushColorInput.addEventListener("change", (event) => {
this.drawingManager.brushColor = brushColorInput;
});
row.appendAll(brushSizeInput, singleColorModeInput, brushColorInput);
}
this.htmlElements.section.appendChild(row);
}
#row5() {
const row = domMake.IconList();
{
const id = generate.uuidv4();
const chooseGhostlyImageInput = domMake.Tree("input", { type: "file", id: id, hidden: true });
const chooseGhostlyImageLabel = domMake.Tree("label", { for: id, class: "icon", title: "Add Image" }, [
domMake.Tree("i", { class: "fas fa-plus" }),
]);
const localThis = this;
function onChange() {
if (!this.files || !this.files[0]) return;
const myFileReader = new FileReader();
myFileReader.addEventListener("load", (event) => {
const base64 = event.target.result.replace("image/gif", "image/png");
localThis.createGhostImage(base64, row);
});
myFileReader.readAsDataURL(this.files[0]);
}
chooseGhostlyImageInput.addEventListener("change", onChange);
row.appendAll(chooseGhostlyImageLabel, chooseGhostlyImageInput);
}
{
const resetImageSelectionLabel = domMake.Tree("div", { class: "icon", title: "Unselect" }, [
domMake.Tree("i", { class: "fas fa-chevron-left" }),
]);
resetImageSelectionLabel.addEventListener("click", () => {
document.body.querySelectorAll('input[name="ghostimage"]').forEach((node) => {
node.checked = false;
});
});
row.appendChild(resetImageSelectionLabel);
}
this.htmlElements.section.appendChild(row);
}
createGhostImage(imageSource, row) {
this.alignDrawCanvas();
const image = this.loadExtension(GhostImage, (reference) => {
this.loadedImages.push(reference);
});
row.appendChild(image.htmlElements.label);
image.setImageSource(imageSource);
}
clearCanvas() {
this.DrawCanvasContext.clearRect(0, 0, this.DrawCanvas.width, this.DrawCanvas.height);
}
saveCanvas() {
this.getAllPixels();
}
resetAllSettings() {
this.clearCanvas();
this.loadedImages.forEach((image, index) => {
setTimeout(() => {
image.reduceToAtoms();
}, 10 * index);
});
this.drawingManager.stopDrawing();
this.setPixelList([]);
this.alignDrawCanvas(true);
}
alignDrawCanvas() {
if (arguments[0]) {
this.DrawCanvas.width = this.GameCanvas.width;
this.DrawCanvas.height = this.GameCanvas.height;
}
const GameCanvasRect = getBoundingClientRect(this.GameCanvas);
this.DrawCanvas.style.top = `${GameCanvasRect.top}px`;
this.DrawCanvas.style.left = `${GameCanvasRect.left}px`;
this.DrawCanvas.style.width = `${GameCanvasRect.width}px`;
this.DrawCanvas.style.height = `${GameCanvasRect.height}px`;
const DrawCanvasRect = getBoundingClientRect(this.DrawCanvas);
if (DrawCanvasRect.width <= 0 || DrawCanvasRect.height <= 0)
return Object.assign(this.DrawCanvasRect, DrawCanvasRect);
// DrawCanvasRect.alignModifierX = Number(this.DrawCanvas.width / DrawCanvasRect.width).toFixed(2);
// DrawCanvasRect.alignModifierY = Number(this.DrawCanvas.height / DrawCanvasRect.height).toFixed(2);
DrawCanvasRect.drawModifierX = 100 / DrawCanvasRect.width;
DrawCanvasRect.drawModifierY = 100 / DrawCanvasRect.height;
Object.assign(this.DrawCanvasRect, DrawCanvasRect);
}
getAllPixels() {
const image = this.DrawCanvasContext.getImageData(
0,
0,
this.DrawCanvasContext.canvas.width,
this.DrawCanvasContext.canvas.height
);
const pixels = [];
for (let index = 0; index < image.data.length; index += 4) {
// const x = (index * 0.25) % image.width;
// const y = Math.floor((index * 0.25) / image.width);
const x = (index * 0.25) % image.width;
const y = Math.floor((index * 0.25) / image.width);
const r = image.data[index + 0];
const g = image.data[index + 1];
const b = image.data[index + 2];
const a = image.data[index + 3];
// const color = rgbaArrayToHex([r, g, b, a]);
const color = [r, g, b, a];
pixels.push({ x1: x, y1: y, x2: x, y2: y, color });
}
this.setPixelList(pixels);
}
getNoneTransparentPixels() {
this.getAllPixels();
const newPixelArray = this.drawingManager.pixelList.filter((pixel) => {
return pixel.color !== "#000000";
// return /^#0[0-8]0[0-8]0[0-8]$/g.test(pixel.color);
});
this.setPixelList(newPixelArray);
}
setPixelList(pixelArray) {
this.drawingManager.pixelList = pixelArray;
this.htmlElements.pixelsLeftToDraw.value = pixelArray.length;
}
}
class GhostImage extends QBit {
image;
rect;
constructor() {
super("GhostImage", '<i class="fas fa-image-polaroid"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.image = domMake.Tree("img", { class: "ghostimage" });
this.image.addEventListener("mousedown", (event) => {
this.htmlElements.label.click();
});
this.htmlElements.input.type = "radio";
this.htmlElements.input.name = "ghostimage";
radios.push(this.htmlElements.input);
this.htmlElements.input.addEventListener("change", (event) => {
radios.forEach(function (radio) {
document.body.querySelector(`label[for="${radio.id}"]`).classList.remove("active");
});
this.htmlElements.label.classList.add("active");
});
document.body.appendChild(this.image);
makeDragable(this.image, this.updatePosition.bind(this));
this.updatePosition();
}
#loadInterface() {
this.#row1();
this.#row2();
}
#row1() {
const row = domMake.Row();
{
const paintCanvasButton = domMake.Button("Place");
paintCanvasButton.addEventListener("click", (event) => {
this.drawImage();
});
row.appendAll(paintCanvasButton);
}
{
const enableButton = domMake.Button("Delete");
enableButton.addEventListener("click", (event) => {
this.reduceToAtoms();
});
row.appendChild(enableButton);
this.htmlElements.toggleStatusButton = enableButton;
}
this.htmlElements.section.appendChild(row);
}
#row2() {
const row = domMake.Row();
{
const scaleInput = domMake.Tree("input", {
type: "number",
title: "rotation",
min: 0.1,
max: 10,
value: 1,
step: 0.02,
});
scaleInput.addEventListener("change", () => {
this.image.style.scale = scaleInput.value;
});
this.htmlElements.scaleInput = scaleInput;
row.appendAll(scaleInput);
}
{
const rotationInput = domMake.Tree("input", { type: "number", title: "rotation", value: 0, step: 1 });
rotationInput.addEventListener("change", () => {
this.image.style.rotate = `${rotationInput.value}deg`;
});
this.htmlElements.rotationInput = rotationInput;
row.appendChild(rotationInput);
}
this.htmlElements.section.appendChild(row);
}
drawImage() {
this.updatePosition();
const ctx = this.parent.DrawCanvasContext;
const offsetTop = Number(this.rect.top) - Number(this.parent.DrawCanvasRect.top);
const offsetLeft = Number(this.rect.left) - Number(this.parent.DrawCanvasRect.left);
// const multiX = Number(this.parent.DrawCanvasRect.alignModifierX);
// const multiY = Number(this.parent.DrawCanvasRect.alignModifierY);
const angle = (Math.PI / 180) * Number(this.htmlElements.rotationInput.value);
const scale = Number(this.htmlElements.scaleInput.value);
const imageWidth = this.image.width * scale;
const imageHeight = this.image.height * scale;
const imgHalfWidth = imageWidth * 0.5;
const imgHalfHeight = imageHeight * 0.5;
ctx.save();
ctx.translate(offsetLeft + imgHalfWidth, offsetTop + imgHalfHeight);
ctx.rotate(angle);
ctx.translate(-imgHalfWidth, -imgHalfHeight);
ctx.drawImage(this.image, 0, 0, imageWidth, imageHeight);
ctx.restore();
}
setImageSource(imageSource) {
this.image.src = imageSource;
this.setIcon(`<img src="${this.image.src}">`);
}
updatePosition() {
this.rect = getBoundingClientRect(this.image);
}
reduceToAtoms() {
this.image.remove();
const pos = radios.indexOf(this.htmlElements.input);
if (~pos) radios.splice(pos, 1);
let pos2 = this.parent.loadedImages.indexOf(this);
if (~pos2) {
this.parent.loadedImages.splice(pos2, 1);
}
this._EXP_destroy(!0);
}
}
class TaskManager {
isRunning;
pixelList;
parent;
BotClientManager;
singleColor;
brushColor;
brushSize;
constructor(parent) {
this.pixelList = [];
this.singleColor = !1;
this.brushColor = "blue";
this.brushSize = 2;
this.parent = parent;
}
startDrawing() {
this.BotClientManager = this.parent.findGlobal("BotClientManager")?.siblings[0];
this.isRunning = true;
this.doTasks();
this.parent.notify("info", "Started");
}
stopDrawing() {
this.isRunning = false;
}
doTasks() {
if (!this.BotClientManager || this.BotClientManager.children.length <= 0) this.stopDrawing();
if (!this.isRunning) return this.parent.notify("info", "Stopped");
this.BotClientManager.children.forEach((botClientInterface, index) => {
this.parseAndSendPixel(botClientInterface, index);
});
setTimeout(() => {
this.doTasks();
}, 1);
}
parseAndSendPixel(botClientInterface, index) {
if (this.pixelList.length <= 0) return this.stopDrawing();
if (!botClientInterface.bot || !botClientInterface.bot.getReadyState()) return;
const task = index % 2 == 0 ? this.pixelList.shift() : this.pixelList.pop();
botClientInterface.bot.send(this.convertTasks(task));
this.parent.htmlElements.pixelsLeftToDraw.value = this.pixelList.length;
}
convertTasks(pixel) {
const playerid = -1;
const lastx = pixel.x1 * this.parent.DrawCanvasRect.drawModifierX;
const lasty = pixel.y1 * this.parent.DrawCanvasRect.drawModifierY;
const x = pixel.x2 * this.parent.DrawCanvasRect.drawModifierX;
const y = pixel.y2 * this.parent.DrawCanvasRect.drawModifierY;
const isactive = !0;
const size = pixel.size ?? this.brushSize;
const pxColor = pixel.color;
const color = this.singleColor
? this.brushColor
: `rgba(${pxColor[0]},${pxColor[1]},${pxColor[2]},${parseFloat(pxColor[3] * 0.390625).toFixed(2)})`;
const ispixel = !1;
let data = [
"drawcmd",
0,
[lastx * 0.01, lasty * 0.01, x * 0.01, y * 0.01, isactive, -size, color, playerid, ispixel],
];
return `${42}${JSON.stringify(data)}`;
}
}
})("QBit");
(function GhostCanvasAlgorithms() {
const QBit = globalThis[arguments[0]];
function sortByColor(pixel1, pixel2) {
const color1 = rgbaArrayToHex(pixel1.color);
const color2 = rgbaArrayToHex(pixel2.color);
if (color1 < color2) {
return -1;
}
if (color1 > color2) {
return 1;
}
return 0;
}
function intToHex(number) {
return number.toString(16).padStart(2, "0");
}
function rgbaArrayToHex(rgbaArray) {
const r = intToHex(rgbaArray[0]);
const g = intToHex(rgbaArray[1]);
const b = intToHex(rgbaArray[2]);
const a = intToHex(rgbaArray[3]);
return "#" + r + g + b + a;
}
function areSameColor(colorArray1, colorArray2, allowedDifference = 8) {
var red = colorArray1[0] - colorArray2[0];
var green = colorArray1[1] - colorArray2[1];
var blue = colorArray1[2] - colorArray2[2];
if (red < 0) red = red * -1;
if (green < 0) green = green * -1;
if (blue < 0) blue = blue * -1;
if (blue > allowedDifference || green > allowedDifference || red > allowedDifference) return false;
return true;
}
class GhostCanvasMinify extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "GhostCanvas");
constructor() {
super("Minify", '<i class="fas fa-compress-arrows-alt"></i>');
this.minOpacity = 20;
this.maxFuzzyness = 32;
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
this.#row2();
this.#row3();
this.#row4();
}
#row1() {
const row = domMake.Row();
{
const fuzzynessInput = domMake.Tree("input", {
type: "number",
title: "Fuzzyness",
step: 1,
min: 0,
max: 255,
value: 1,
});
const opacityInput = domMake.Tree("input", {
type: "number",
title: "Opacity",
step: 1,
min: 0,
max: 255,
value: 0,
});
fuzzynessInput.addEventListener("change", () => {
this.maxFuzzyness = Number(fuzzynessInput.value);
});
opacityInput.addEventListener("change", () => {
this.minOpacity = Number(opacityInput.value);
});
row.appendAll(fuzzynessInput, opacityInput);
}
this.htmlElements.section.appendChild(row);
}
#row2() {
const row = domMake.Row();
{
const minifyPixelsArrayButton = domMake.Button("Minify");
minifyPixelsArrayButton.addEventListener("click", (event) => {
this.minifyPixelsArray();
});
row.appendAll(minifyPixelsArrayButton);
}
this.htmlElements.section.appendChild(row);
}
#row3() {}
#row4() {}
minifyPixelsArray() {
const pixelArray = this.parent.drawingManager.pixelList;
const newPixelArray = [];
let currentPixel = pixelArray[0];
let lastPixel = currentPixel;
let currentLine = currentPixel;
for (let index = 0; index < pixelArray.length; index++) {
currentPixel = pixelArray[index];
if (lastPixel.color[3] < 10 && currentPixel.color[3] >= 10) {
// From Transparent To Solid
currentLine = currentPixel;
} else if (lastPixel.color[3] >= 10 && currentPixel.color[3] < 10) {
// From Solid To Transparent
currentLine.x2 = lastPixel.x2;
newPixelArray.push(currentLine);
currentLine = currentPixel;
} else if (currentPixel.color[3] >= 10 && lastPixel.color[3] >= 10) {
// From Solid To Solid
if (
currentLine.y1 !== currentPixel.y1 ||
lastPixel.x2 !== currentPixel.x1 - 1 ||
!areSameColor(lastPixel.color, currentPixel.color, this.maxFuzzyness)
) {
currentLine.x2 = lastPixel.x2;
newPixelArray.push(currentLine);
currentLine = currentPixel;
}
} else {
// From Transparent To Transparent
}
lastPixel = currentPixel;
}
// if (currentLine.color[3] >= 10) newPixelArray.push(currentLine);
this.parent.setPixelList(newPixelArray);
}
minifyPixelsArray_alt() {
const pixelArray = this.parent.drawingManager.pixelList;
const newPixelArray = [];
var lastPixel = pixelArray[0];
var currentLine = lastPixel;
const stepsize = this.parent.stepsize ?? 1;
for (let i = 0; i < pixelArray.length; i += stepsize) {
const currentPixel = pixelArray[i];
if (currentPixel.y1 !== currentLine.y1 || currentPixel.color !== lastPixel.color) {
currentLine.x2 = lastPixel.x2;
if (!/^#[0-9a-fA-F]{6}[0-4]{2}$/.test(lastPixel.color)) newPixelArray.push(currentLine);
currentLine = currentPixel;
}
lastPixel = currentPixel;
}
newPixelArray.push(currentLine);
this.parent.setPixelList(newPixelArray);
}
}
class GhostCanvasSort extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "GhostCanvas");
constructor() {
super("Sort", '<i class="fas fa-sort-numeric-down"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
this.#row2();
this.#row3();
this.#row4();
}
#row1() {
const row = domMake.Row();
{
const sortPixelsArrayButton = domMake.Button("Sort");
sortPixelsArrayButton.addEventListener("click", (event) => {
this.sortPixelsArray();
});
row.appendAll(sortPixelsArrayButton);
}
this.htmlElements.section.appendChild(row);
}
#row2() {}
#row3() {}
#row4() {}
sortPixelsArray() {
const pixelArray = this.parent.drawingManager.pixelList;
const newPixelArray = [...pixelArray].sort(sortByColor);
this.parent.setPixelList(newPixelArray);
}
}
})("QBit");
(function BotClientInterface() {
const QBit = globalThis[arguments[0]];
const BotClient = QBit.findGlobal("BotClient");
let botcount = 0;
const radios = [];
function getMasterId() {
return document.querySelector(".playerlist-name-self")?.parentElement.dataset.playerid || 0;
}
function parseAvatarURL(arr = []) {
return `https://drawaria.online/avatar/cache/${arr.length > 0 ? arr.join(".") : "default"}.jpg`;
}
class BotClientManager extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
constructor() {
super(`BotClientManager`, '<i class="fas fa-robot"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
}
#row1() {
const row = domMake.IconList();
{
const id = generate.uuidv4();
const createBotClientInterfaceInput = domMake.Tree("input", { type: "button", id: id, hidden: true });
const createBotClientInterfaceLabel = domMake.Tree("label", { for: id, class: "icon", title: "Add Image" }, [
domMake.Tree("i", { class: "fas fa-plus" }),
]);
createBotClientInterfaceInput.addEventListener("click", () => {
this.createBotClientInterface();
});
row.appendAll(createBotClientInterfaceLabel);
row.appendAll(createBotClientInterfaceInput);
}
{
const id = generate.uuidv4();
const removeBotClientInterfaceInput = domMake.Tree("input", { type: "button", id: id, hidden: true });
const removeBotClientInterfaceLabel = domMake.Tree("label", { for: id, class: "icon", title: "Add Image" }, [
domMake.Tree("i", { class: "fas fa-minus" }),
]);
removeBotClientInterfaceInput.addEventListener("click", () => {
this.deleteBotClientInterface();
});
row.appendAll(removeBotClientInterfaceLabel);
row.appendAll(removeBotClientInterfaceInput);
}
this.htmlElements.header.before(row);
}
createBotClientInterface() {
const instance = this.loadExtension(BotClientInterface);
instance.htmlElements.input.type = "radio";
instance.htmlElements.input.name = "botClient";
radios.push(instance.htmlElements.input);
instance.htmlElements.input.addEventListener("change", (event) => {
radios.forEach(function (radio) {
document.body.querySelector(`label[for="${radio.id}"]`).classList.remove("active");
});
instance.htmlElements.label.classList.add("active");
});
return instance;
}
deleteBotClientInterface() {
const input = document.body.querySelector(`input[name="botClient"]:checked`);
const matches = this.children.filter((child) => {
return child.htmlElements.input === input;
});
if (matches.length <= 0) return;
const instance = matches[0];
instance.bot.disconnect();
const labelPos = radios.indexOf(instance.htmlElements.input);
if (~labelPos) radios.splice(labelPos, 1);
instance._EXP_destroy(!0);
}
}
class BotClientInterface extends QBit {
static dummy1 = QBit.register(this);
// static dummy2 = QBit.bind(this, 'CubeEngine');
// static dummy3 = QBit.bind(this, 'CubeEngine');
constructor() {
super(`Bot${botcount}`, '<i class="fas fa-robot"></i>');
this.bot = new BotClient();
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.setClientName(this.bot.name);
this.setClientIcon(this.bot.avatar);
}
#loadInterface() {
this.#row1();
}
#row1() {
const row = domMake.Row();
{
let join_button = domMake.Button("Enter");
let leave_button = domMake.Button("Leave");
join_button.addEventListener("click", (event) => {
this.bot.enterRoom(document.querySelector("#invurl").value);
});
leave_button.addEventListener("click", (event) => {
this.bot.disconnect();
});
row.appendAll(join_button, leave_button);
}
this.htmlElements.section.appendChild(row);
}
setClientName(name) {
this.setName(name);
this.bot.name = name;
}
setClientIcon(icon) {
this.setIcon(`<img src="${parseAvatarURL(this.bot.avatar)}">`);
this.bot.avatar = icon;
}
}
})("QBit");
(function BotClientInteractions() {
const QBit = globalThis[arguments[0]];
class BotPersonality extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "BotClientInterface");
constructor() {
super("Personality", '<i class="fas fa-user-cog"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
this.#row2();
}
#row1() {
const row = domMake.Row();
{
let botName = domMake.Tree("input", { type: "text", placeholder: "Your Bots name" });
let botNameAccept = domMake.Tree("button", { class: "icon" }, [domMake.Tree("i", { class: "fas fa-check" })]);
botNameAccept.addEventListener("click", (event) => {
this.parent.setClientName(botName.value);
});
row.appendAll(botName, botNameAccept);
}
this.htmlElements.section.appendChild(row);
}
#row2() {
let id = generate.uuidv4();
const row = domMake.Row();
{
let botAvatarUpload = domMake.Tree("input", { type: "file", id: id, hidden: true });
let botAvatarAccept = domMake.Tree("label", { for: id, class: "btn btn-outline-secondary" }, [
"Upload BotAvatar",
]);
const localThis = this;
function onChange() {
if (!this.files || !this.files[0]) return;
let myFileReader = new FileReader();
myFileReader.addEventListener("load", (e) => {
let a = e.target.result.replace("image/gif", "image/png");
fetch("https://drawaria.online/uploadavatarimage", {
method: "POST",
body: "imagedata=" + encodeURIComponent(a) + "&fromeditor=true",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
}).then((res) =>
res.text().then((body) => {
localThis.parent.setClientIcon(body.split("."));
})
);
});
myFileReader.readAsDataURL(this.files[0]);
}
botAvatarUpload.addEventListener("change", onChange);
row.appendAll(botAvatarUpload, botAvatarAccept);
}
this.htmlElements.section.appendChild(row);
}
}
class BotSozials extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "BotClientInterface");
constructor() {
super("Socialize", '<i class="fas fa-comment-dots"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
this.#row2();
}
#row1() {
const row = domMake.Row();
{
let messageClear_button = domMake.Button('<i class="fas fa-strikethrough"></i>');
let messageSend_button = domMake.Button('<i class="fas fa-paper-plane"></i>');
let message_input = domMake.Tree("input", { type: "text", placeholder: "message..." });
messageClear_button.classList.add("icon");
messageSend_button.classList.add("icon");
messageClear_button.addEventListener("click", (event) => {
message_input.value = "";
});
messageSend_button.addEventListener("click", (event) => {
this.parent.bot.emit("chatmsg", message_input.value);
});
message_input.addEventListener("keypress", (event) => {
if (event.keyCode != 13) return;
this.parent.bot.emit("chatmsg", message_input.value);
});
row.appendAll(messageClear_button, message_input, messageSend_button);
}
this.htmlElements.section.appendChild(row);
}
#row2() {
const row = domMake.IconList();
// row.classList.add('nowrap');
{
document
.querySelectorAll("#gesturespickerselector .gesturespicker-container .gesturespicker-item")
.forEach((node, index) => {
let clone = node.cloneNode(true);
clone.classList.add("icon");
clone.addEventListener("click", (event) => {
this.parent.bot.emit("sendgesture", index);
});
row.appendChild(clone);
});
}
this.htmlElements.section.appendChild(row);
}
}
class BotTokenGiver extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "BotClientInterface");
constructor() {
super("Tokken", '<i class="fas fa-thumbs-up"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
this.#row2();
}
#row1() {
const row = domMake.IconList();
// row.classList.add('nowrap');
{
let listOfTokens = [
'<i class="fas fa-thumbs-up"></i>',
'<i class="fas fa-heart"></i>',
'<i class="fas fa-paint-brush"></i>',
'<i class="fas fa-cocktail"></i>',
'<i class="fas fa-hand-peace"></i>',
'<i class="fas fa-feather-alt"></i>',
'<i class="fas fa-trophy"></i>',
'<i class="fas fa-mug-hot"></i>',
'<i class="fas fa-gift"></i>',
];
listOfTokens.forEach((token, index) => {
let tokenSend_button = domMake.Button(token);
tokenSend_button.classList.add("icon");
tokenSend_button.addEventListener("click", () => {
this.parent.bot.room.players.forEach((player) => {
this.parent.bot.emit("settoken", player.id, index);
});
});
row.appendChild(tokenSend_button);
});
}
this.htmlElements.section.appendChild(row);
}
#row2() {
const row = domMake.Row();
{
let toggleStatus_button = domMake.Button("Toggle Status");
toggleStatus_button.addEventListener("click", () => {
this.parent.bot.attributes.status = !this.parent.bot.attributes.status;
let status = this.parent.bot.attributes.status;
toggleStatus_button.classList[status ? "add" : "remove"]("active");
this.parent.bot.emit("setstatusflag", 0, status);
this.parent.bot.emit("setstatusflag", 1, status);
this.parent.bot.emit("setstatusflag", 2, status);
this.parent.bot.emit("setstatusflag", 3, status);
this.parent.bot.emit("setstatusflag", 4, status);
});
row.appendChild(toggleStatus_button);
}
this.htmlElements.section.appendChild(row);
}
}
class BotCanvasAvatar extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "BotClientInterface");
constructor() {
super("SpawnAvatar", '<i class="fas fa-user-circle"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1();
}
#row1() {
const row = domMake.Row();
{
// Spawn && Move Avatar
let avatarPosition = { x: 0, y: 0 };
let avatarSpawn_button = domMake.Button('<i class="fas fa-exchange-alt"></i>');
let avatarChange_button = domMake.Button('<i class="fas fa-retweet"></i>');
let avatarPositionX_button = domMake.Tree("input", {
type: "number",
value: 2,
min: 2,
max: 98,
title: "Left",
});
let avatarPositionY_button = domMake.Tree("input", {
type: "number",
value: 2,
min: 2,
max: 98,
title: "Top",
});
avatarSpawn_button.addEventListener("click", (event) => {
this.parent.bot.emit("spawnavatar");
this.parent.bot.attributes.spawned = !this.parent.bot.attributes.spawned;
});
avatarChange_button.addEventListener("click", (event) => {
this.parent.bot.emit("setavatarprop");
this.parent.bot.attributes.rounded = !this.parent.bot.attributes.rounded;
});
avatarPositionX_button.addEventListener("change", (event) => {
avatarPosition.x = avatarPositionX_button.value;
this.parent.bot.emit("moveavatar", avatarPosition.x, avatarPosition.y);
});
avatarPositionY_button.addEventListener("change", (event) => {
avatarPosition.y = avatarPositionY_button.value;
this.parent.bot.emit("moveavatar", avatarPosition.x, avatarPosition.y);
});
avatarSpawn_button.title = "Spawn Avatar";
avatarChange_button.title = "Toggle Round Avatar";
row.appendAll(avatarSpawn_button, avatarPositionX_button, avatarPositionY_button, avatarChange_button);
}
this.htmlElements.section.appendChild(row);
}
}
function getMyId() {
return document.querySelector(".playerlist-name-self")?.parentElement.dataset.playerid || 0;
}
function parseAvatarURL(arr = []) {
return `https://drawaria.online/avatar/cache/${arr.length > 0 ? arr.join(".") : "default"}.jpg`;
}
})("QBit");
// --- NEW FUNCTIONALITIES START HERE ---
// --- START NEW MODULE: PlayerKickTools (COMBINED MODULE) ---
(function PlayerKickToolsModule() {
const QBit = globalThis[arguments[0]];
QBit.Styles.addRules([
`#${QBit.identifier} .kick-tools-section {
border: 1px solid var(--CE-color);
border-radius: .25rem;
padding: 5px;
margin-bottom: 10px;
background-color: var(--CE-bg_color);
}`,
`#${QBit.identifier} .kick-tools-section-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--dark-blue-title);
text-align: center;
}`,
`#${QBit.identifier} .kick-tools-player-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
max-height: 150px; /* Limit height to show scrollbar if many players */
overflow-y: auto;
padding: 5px;
border: 1px dashed var(--CE-color);
border-radius: .25rem;
}`,
`#${QBit.identifier} .kick-tools-player-list .btn {
flex: 0 0 auto; /* Prevent stretching */
margin: 0; /* Remove default margin from btn class */
padding: 3px 8px; /* Compact padding */
font-size: 0.8em;
}`
]);
class PlayerKickTools extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#humanKickPlayerListContainer;
#botKickPlayerListContainer;
#cachedPlayers = []; // To store player IDs and names for both sections
constructor() {
super("Herramientas de Expulsión", '<i class="fas fa-gavel"></i>'); // Original icon
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#setupObservers(); // Setup MutationObserver for player list
}
#loadInterface() {
const container = domMake.Tree("div");
// --- Section: Voto para Expulsar (Tú) ---
const humanKickSection = domMake.Tree("div", { class: "kick-tools-section" });
humanKickSection.appendChild(domMake.Tree("div", { class: "kick-tools-section-title" }, ["Voto para Expulsar (Tú)"]));
this.#humanKickPlayerListContainer = domMake.IconList({ class: "kick-tools-player-list" }); // Use IconList for flex layout
humanKickSection.appendChild(this.#humanKickPlayerListContainer);
container.appendChild(humanKickSection);
// --- Section: Expulsar con Bot ---
const botKickSection = domMake.Tree("div", { class: "kick-tools-section" });
botKickSection.appendChild(domMake.Tree("div", { class: "kick-tools-section-title" }, ["Expulsar con Bot"]));
this.#botKickPlayerListContainer = domMake.IconList({ class: "kick-tools-player-list" }); // Use IconList for flex layout
botKickSection.appendChild(this.#botKickPlayerListContainer);
container.appendChild(botKickSection);
this.htmlElements.section.appendChild(container);
}
#setupObservers() {
const playerListElement = document.getElementById("playerlist");
if (playerListElement) {
const observer = new MutationObserver(() => this.#updatePlayerLists());
observer.observe(playerListElement, { childList: true, subtree: true });
this.#updatePlayerLists(); // Initial update
}
}
#updatePlayerLists() {
this.#humanKickPlayerListContainer.innerHTML = ''; // Clear existing buttons
this.#botKickPlayerListContainer.innerHTML = ''; // Clear existing buttons
this.#cachedPlayers = []; // Clear cached players
const playerRows = document.querySelectorAll("#playerlist .playerlist-row");
const myPlayerIdElement = document.querySelector(".playerlist-name-self")?.parentElement;
const myPlayerId = myPlayerIdElement ? myPlayerIdElement.dataset.playerid : null;
if (playerRows.length <= 1) { // Only self or no other players
const noPlayersMessage = domMake.Tree("span", {}, ["No hay otros jugadores para expulsar."]);
this.#humanKickPlayerListContainer.appendChild(noPlayersMessage.cloneNode(true));
this.#botKickPlayerListContainer.appendChild(noPlayersMessage);
return;
}
playerRows.forEach(playerRow => {
const playerId = playerRow.dataset.playerid;
const playerName = playerRow.querySelector(".playerlist-name a")?.textContent || `Jugador ${playerId}`;
if (playerId === myPlayerId) { // Don't allow kicking self
return;
}
this.#cachedPlayers.push({ id: parseInt(playerId), name: playerName });
});
this.#renderHumanKickButtons();
this.#renderBotKickButtons();
}
#renderHumanKickButtons() {
this.#humanKickPlayerListContainer.innerHTML = ''; // Clear before rendering
if (this.#cachedPlayers.length === 0) {
this.#humanKickPlayerListContainer.appendChild(domMake.Tree("span", {}, ["No hay jugadores."]));
return;
}
this.#cachedPlayers.forEach(player => {
const playerButton = domMake.Button(player.name);
playerButton.title = `Votar para expulsar a ${player.name} (ID: ${player.id}).`;
playerButton.addEventListener("click", () => this.#sendHumanVoteKick(player.id, player.name));
this.#humanKickPlayerListContainer.appendChild(playerButton);
});
}
#renderBotKickButtons() {
this.#botKickPlayerListContainer.innerHTML = ''; // Clear before rendering
if (this.#cachedPlayers.length === 0) {
this.#botKickPlayerListContainer.appendChild(domMake.Tree("span", {}, ["No hay jugadores."]));
return;
}
this.#cachedPlayers.forEach(player => {
const playerButton = domMake.Button(player.name);
playerButton.title = `Expulsar a ${player.name} (ID: ${player.id}) con el bot seleccionado.`;
playerButton.addEventListener("click", () => this.#sendBotKick(player.id, player.name));
this.#botKickPlayerListContainer.appendChild(playerButton);
});
}
// --- Human Vote Kick Method ---
#sendHumanVoteKick(targetPlayerId, targetPlayerName) {
if (globalThis.sockets && globalThis.sockets.length > 0) {
const data = _io.emits.sendvotekick(targetPlayerId);
globalThis.sockets[0].send(data);
this.notify("info", `Voto para expulsar enviado para ${targetPlayerName} (ID: ${targetPlayerId}).`);
} else {
this.notify("warning", "No hay conexión WebSocket activa para enviar el voto de expulsión.");
}
}
// --- Bot Kick Methods ---
#getBot() {
const botManagerClass = this.findGlobal("BotClientManager");
if (!botManagerClass || !botManagerClass.siblings || botManagerClass.siblings.length === 0) {
this.notify("warning", "No hay instancias activas de 'BotClientManager'. Por favor, crea uno desde 'CubeEngine'.");
return null;
}
const botManagerInstance = botManagerClass.siblings[0];
const botClientInterfaces = botManagerInstance.children;
let activeBotClientInterface = null;
const selectedBotInput = document.querySelector('input[name="botClient"]:checked');
if (selectedBotInput) {
activeBotClientInterface = botClientInterfaces.find(bci => bci.htmlElements.input === selectedBotInput);
}
if (!activeBotClientInterface && botClientInterfaces.length > 0) {
activeBotClientInterface = botClientInterfaces[0];
this.notify("info", `No se seleccionó un bot. Usando el primer bot disponible: ${activeBotClientInterface.getName()}.`);
}
if (!activeBotClientInterface || !activeBotClientInterface.bot || !activeBotClientInterface.bot.getReadyState()) {
this.notify("warning", `El bot "${activeBotClientInterface ? activeBotClientInterface.getName() : 'desconocido'}" no está conectado y listo para enviar comandos.`);
return null;
}
return activeBotClientInterface.bot;
}
#sendBotKick(targetPlayerId, targetPlayerName) {
const bot = this.#getBot();
if (!bot) return; // #getBot already handles notifications
const data = _io.emits.sendvotekick(targetPlayerId);
bot.send(data);
this.notify("success", `Bot "${bot.name}" envió solicitud de expulsión para ${targetPlayerName}.`);
}
}
})("QBit");
// --- END NEW MODULE: PlayerKickTools ---
// --- START NEW MODULE: PlayerSocials (COMBINED MODULE) ---
(function PlayerSocialsModule() {
const QBit = globalThis[arguments[0]];
// Define token names here to be self-contained
const TOKEN_NAMES = {
0: "Thumbs Up",
1: "Heart",
2: "Paint Brush",
3: "Cocktail",
4: "Peace Sign",
5: "Feather",
6: "Trophy",
7: "Mug",
8: "Gift"
};
QBit.Styles.addRules([
`#${QBit.identifier} .player-socials-section {
border: 1px solid var(--CE-color);
border-radius: .25rem;
padding: 5px;
margin-bottom: 10px;
background-color: var(--CE-bg_color);
}`,
`#${QBit.identifier} .player-socials-section-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--dark-blue-title); /* Use existing style var */
text-align: center;
}`,
`#${QBit.identifier} .player-socials-afk-toggle,
#${QBit.identifier} .player-socials-status-flags .status-flag-button {
background-color: var(--secondary);
color: var(--dark);
width: 100%;
padding: 5px 10px;
box-sizing: border-box;
}`,
`#${QBit.identifier} .player-socials-afk-toggle.active,
#${QBit.identifier} .player-socials-status-flags .status-flag-button.active {
background-color: var(--info); /* Consistent active color */
color: white;
}`,
`#${QBit.identifier} .player-socials-token-list .icon {
width: 38px; /* Slightly larger icons */
height: 38px;
min-width: 38px;
min-height: 38px;
font-size: 1.2em; /* Larger icon itself */
}`
]);
class PlayerSocials extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
// Custom Chat
#messageInput;
// Toggle AFK
#isAfkActive = false;
#afkToggleButton;
// Global Token Giver
#playerList = [];
// #tokenButtons will be local to render method
// Set Status Flag
#statusFlagsState = {}; // To keep track of active flags
constructor() {
super("Sociales del Jugador", '<i class="fas fa-comments-dollar"></i>'); // New icon
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#setupObservers(); // Setup MutationObserver for player list
}
#loadInterface() {
const container = domMake.Tree("div");
// --- Section: Custom Chat Message ---
const chatSection = domMake.Tree("div", { class: "player-socials-section" });
chatSection.appendChild(domMake.Tree("div", { class: "player-socials-section-title" }, ["Enviar Mensaje"]));
const chatRow = domMake.Row();
this.#messageInput = domMake.Tree("input", { type: "text", placeholder: "Tu mensaje..." });
const sendButton = domMake.Button('<i class="fas fa-paper-plane"></i>');
sendButton.classList.add("icon");
sendButton.addEventListener("click", () => {
this.#sendMessage(this.#messageInput.value);
this.#messageInput.value = '';
});
this.#messageInput.addEventListener("keypress", (event) => {
if (event.keyCode === 13) {
this.#sendMessage(this.#messageInput.value);
this.#messageInput.value = '';
}
});
chatRow.appendAll(this.#messageInput, sendButton);
chatSection.appendChild(chatRow);
container.appendChild(chatSection);
// --- Section: Toggle AFK ---
const afkSection = domMake.Tree("div", { class: "player-socials-section" });
afkSection.appendChild(domMake.Tree("div", { class: "player-socials-section-title" }, ["Estado AFK"]));
const afkRow = domMake.Row();
this.#afkToggleButton = domMake.Button("Toggle AFK");
this.#afkToggleButton.classList.add("player-socials-afk-toggle");
this.#afkToggleButton.addEventListener("click", () => this.#toggleAfkStatus());
afkRow.appendChild(this.#afkToggleButton);
afkSection.appendChild(afkRow);
container.appendChild(afkSection);
// --- Section: Global Token Giver ---
const tokensSection = domMake.Tree("div", { class: "player-socials-section" });
tokensSection.appendChild(domMake.Tree("div", { class: "player-socials-section-title" }, ["Dar Emblemas"]));
const tokensRow = domMake.IconList({ class: "player-socials-token-list" });
const tokens = [
{ id: 0, icon: '<i class="fas fa-thumbs-up"></i>' },
{ id: 1, icon: '<i class="fas fa-heart"></i>' },
{ id: 2, icon: '<i class="fas fa-paint-brush"></i>' },
{ id: 3, icon: '<i class="fas fa-cocktail"></i>' },
{ id: 4, icon: '<i class="fas fa-hand-peace"></i>' },
{ id: 5, icon: '<i class="fas fa-feather-alt"></i>' },
{ id: 6, icon: '<i class="fas fa-trophy"></i>' },
{ id: 7, icon: '<i class="fas fa-mug-hot"></i>' },
{ id: 8, icon: '<i class="fas fa-gift"></i>' }
];
tokens.forEach(token => {
const tokenButton = domMake.Button(token.icon);
tokenButton.classList.add("icon");
tokenButton.title = `Dar Emblema: ${TOKEN_NAMES[token.id]}`;
tokenButton.addEventListener("click", () => this.#giveToken(token.id));
tokensRow.appendChild(tokenButton);
});
tokensSection.appendChild(tokensRow);
container.appendChild(tokensSection);
// --- Section: Set Status Flag ---
const statusSection = domMake.Tree("div", { class: "player-socials-section" });
statusSection.appendChild(domMake.Tree("div", { class: "player-socials-section-title" }, ["Establecer Estado"]));
const flags = [
{ id: 0, name: "Música", icon: '<i class="fas fa-music"></i>' },
{ id: 1, name: "AFK 1", icon: '<i class="fas fa-bed"></i>' },
{ id: 2, name: "AFK 2", icon: '<i class="fas fa-couch"></i>' },
{ id: 3, name: "Inventario Abierto", icon: '<i class="fas fa-box-open"></i>' },
{ id: 4, name: "Lista Amigos Abierta", icon: '<i class="fas fa-user-friends"></i>' }
];
flags.forEach(flag => {
const flagRow = domMake.Row();
const toggleButton = domMake.Button(flag.icon + " " + flag.name);
toggleButton.classList.add("status-flag-button");
toggleButton.dataset.flagId = flag.id;
toggleButton.dataset.isActive = "false"; // Initial state
this.#statusFlagsState[flag.id] = false; // Initialize internal state
toggleButton.addEventListener("click", () => this.#toggleStatusFlag(flag.id, toggleButton));
flagRow.appendChild(toggleButton);
statusSection.appendChild(flagRow);
});
container.appendChild(statusSection);
this.htmlElements.section.appendChild(container);
}
#setupObservers() {
// Observer for player list changes (for Token Giver)
const playerListElement = document.getElementById("playerlist");
if (playerListElement) {
const observer = new MutationObserver(() => this.#updatePlayerList());
observer.observe(playerListElement, { childList: true, subtree: true });
this.#updatePlayerList(); // Initial update
}
}
// --- Custom Chat Message Methods ---
#sendMessage(message) {
if (!message.trim()) return;
if (globalThis.sockets && globalThis.sockets.length > 0) {
const data = _io.emits.chatmsg(message);
globalThis.sockets[0].send(data);
this.notify("info", `Mensaje enviado: "${message}"`);
} else {
this.notify("warning", "No hay conexión WebSocket activa.");
}
}
// --- Toggle AFK Methods ---
#toggleAfkStatus() {
this.#isAfkActive = !this.#isAfkActive;
if (globalThis.sockets && globalThis.sockets.length > 0) {
const data = _io.emits.playerafk();
globalThis.sockets[0].send(data);
this.#afkToggleButton.classList[this.#isAfkActive ? "add" : "remove"]("active");
this.#afkToggleButton.textContent = this.#isAfkActive ? "AFK Activo" : "AFK Inactivo";
this.notify("info", `Estado AFK cambiado a: ${this.#isAfkActive}`);
} else {
this.notify("warning", "No hay conexión WebSocket activa.");
}
}
// --- Global Token Giver Methods ---
#updatePlayerList() {
this.#playerList = [];
const playerRows = document.querySelectorAll("#playerlist .playerlist-row");
playerRows.forEach(playerRow => {
const playerId = playerRow.dataset.playerid;
const playerName = playerRow.querySelector(".playerlist-name a")?.textContent || `Jugador ${playerId}`;
this.#playerList.push({ id: parseInt(playerId), name: playerName });
});
}
#giveToken(tokenId) {
if (globalThis.sockets && globalThis.sockets.length > 0) {
if (this.#playerList.length > 0) {
this.#playerList.forEach(player => {
const data = _io.emits.settoken(player.id, tokenId);
globalThis.sockets[0].send(data);
this.notify("info", `Emblema '${TOKEN_NAMES[tokenId]}' enviado a ${player.name} (ID: ${player.id}).`);
});
} else {
this.notify("warning", "No se encontraron jugadores en la sala para dar emblemas.");
}
} else {
this.notify("warning", "No hay conexión WebSocket activa.");
}
}
// --- Set Status Flag Methods ---
#toggleStatusFlag(flagId, buttonElement) {
const newActiveState = !this.#statusFlagsState[flagId];
this.#statusFlagsState[flagId] = newActiveState;
buttonElement.classList[newActiveState ? "add" : "remove"]("active");
buttonElement.dataset.isActive = newActiveState; // Update dataset
if (globalThis.sockets && globalThis.sockets.length > 0) {
const data = _io.emits.setstatusflag(flagId, newActiveState);
globalThis.sockets[0].send(data);
this.notify("info", `Estado '${buttonElement.textContent.trim()}' cambiado a: ${newActiveState}`);
} else {
this.notify("warning", "No hay conexión WebSocket activa.");
}
}
}
})("QBit");
// --- END NEW MODULE: PlayerSocials ---
(function BotClientModifications() {
const QBit = globalThis[arguments[0]];
// Re-declare parseServerUrl and parseRoomId if they are not globally accessible outside BotClient scope
function parseServerUrl(any) {
var prefix = String(any).length == 1 ? `sv${any}.` : ""; // Expects a single digit server ID
return `wss://${prefix}drawaria.online/socket.io/?sid1=undefined&hostname=drawaria.online&EIO=3&transport=websocket`;
}
function parseRoomId(any) {
// Extract the main room ID part, before any optional .server_id
const match = String(any).match(/^([a-f0-9-]+)(?:\.\d+)?$/i);
if (match && match[1]) {
return match[1];
}
// If it's a direct room ID like "12345", then that's the ID.
// If it's a full URL, attempt to get last segment as ID.
const urlMatch = String(any).match(/([a-f0-9.-]+?)$/gi);
if (urlMatch && urlMatch[0]) {
return urlMatch[0];
}
return String(any); // Fallback to original if no specific ID extracted
}
// AÑADIDO: RE-DECLARACIÓN DE parseSocketIOEvent para su accesibilidad
function parseSocketIOEvent(prefix_length, event_data) {
try {
return JSON.parse(event_data.slice(prefix_length));
} catch (error) {
// Puedes añadir un console.error aquí para depuración si es necesario
// console.error("Error parsing socket event data:", error, "Data:", event_data);
return null; // Retorna null o un array vacío si falla el parseo
}
}
// FIN AÑADIDO
// Reference to original emits object (if it's not global)
const emits = _io.emits; // Assuming _io is global as per original script
// Redefine BotClient class to inject into QBit's context
// This is necessary because we are modifying its methods for room joining.
// Ensure the original BotClient is available for modification.
const OriginalBotClient = QBit.findGlobal("BotClient"); // Get the original class reference
if (OriginalBotClient) {
// Override/extend methods of the original BotClient prototype
// This is safer than re-declaring the whole class if other modules rely on its specific constructor
// or static properties.
// Store original methods to call them if needed or as a fallback
const originalBotClientConnect = OriginalBotClient.prototype.connect;
const originalBotClientEnterRoom = OriginalBotClient.prototype.enterRoom;
const originalBotClientDisconnect = OriginalBotClient.prototype.disconnect;
const originalBotClientReconnect = OriginalBotClient.prototype.reconnect;
Object.assign(OriginalBotClient.prototype, {
// New or modified internal method to handle socket opening and initial commands
// Renamed to avoid conflicts and clearly separate concerns.
_onSocketOpenHandler(event) {
const localThis = this;
clearInterval(localThis.interval_id); // Clear any old interval
localThis.interval_id = setInterval(function () {
if (!localThis.getReadyState()) {
clearInterval(localThis.interval_id);
localThis.notify("info", `Bot ${localThis.name} desconectado (ping fallido).`);
return;
}
localThis.send(2); // Keep-alive ping
}, localThis.interval_ms);
// ONLY send startplay AFTER the socket is confirmed open
if (localThis.room.id) { // Ensure there's a room ID set before attempting to start play
localThis.send(emits.startplay(localThis.room, localThis.name, localThis.avatar));
localThis.notify("success", `Bot ${localThis.name} conectado y en sala ${localThis.room.id}.`);
} else {
localThis.notify("warning", `Bot ${localThis.name} conectado, pero sin ID de sala para iniciar juego.`);
}
},
_onSocketMessageHandler(message_event) {
var prefix = String(message_event.data).match(/(^\d+)/gi)?.[0] || "";
var data = parseSocketIOEvent(prefix.length, message_event.data) || [];
if (data && data.length === 1) {
if (data[0].players) this.room.players = data[0].players;
}
if (data && data.length > 1) {
var event = data.shift();
// Special handling for raw 'drawcmd' to dispatch a custom event
if (event === "drawcmd") {
this.customObservers.forEach((listener) => {
if (listener.event === "bc_drawcommand") { // Dispatch as bc_drawcommand for DrawingReplay etc.
if (listener.callback) listener.callback(data);
}
});
}
// Dispatch any other event normally
this.customObservers.forEach((listener) => {
if (listener.event === event) if (listener.callback) listener.callback(data);
});
}
},
_onSocketCloseHandler(event) {
clearInterval(this.interval_id);
this.socket = null; // Ensure socket reference is cleared
this.notify("info", `Bot ${this.name} socket cerrado. Código: ${event.code}, Razón: ${event.reason}`);
// The global `sockets` array is managed by WebSocket.prototype.send override, no need to manually splice here.
},
_onSocketErrorHandler(event) {
this.notify("error", `Error de socket para bot ${this.name}.`);
console.error(`WebSocket Error for ${this.name}:`, event);
clearInterval(this.interval_id);
this.socket = null;
},
// Overriding connect method
connect(serverUrlSegment = "") {
if (this.getReadyState()) {
// Already connected. Disconnect first to ensure a clean new connection.
this.notify("info", `Bot ${this.name} ya está conectado. Desconectando para reconectar.`);
this.disconnect();
}
const fullServerUrl = parseServerUrl(serverUrlSegment);
this.socket = new WebSocket(fullServerUrl);
// Bind handlers to 'this' context of the BotClient instance
this.socket.addEventListener("open", this._onSocketOpenHandler.bind(this));
this.socket.addEventListener("message", this._onSocketMessageHandler.bind(this));
this.socket.addEventListener("close", this._onSocketCloseHandler.bind(this));
this.socket.addEventListener("error", this._onSocketErrorHandler.bind(this));
this.notify("info", `Bot ${this.name} intentando conectar a: ${fullServerUrl}`);
},
// Overriding enterRoom method
enterRoom(fullRoomIdOrUrl) {
this.room.id = parseRoomId(fullRoomIdOrUrl); // Gets "uuid" part
let serverIdSegment = "";
const parts = String(fullRoomIdOrUrl).split('.');
// Check if the last part is a number and indicates a server ID
if (parts.length > 1 && !isNaN(parseInt(parts[parts.length - 1]))) {
serverIdSegment = parts[parts.length - 1];
}
this.connect(serverIdSegment); // Connect using only the server ID part
// The `startplay` command will be sent automatically once the socket opens (in _onSocketOpenHandler)
},
// Overriding disconnect method
disconnect() {
if (!this.getReadyState()) {
this.notify("info", `Bot ${this.name} ya está desconectado.`);
return;
}
clearInterval(this.interval_id); // Clear ping interval
this.send(41); // Explicitly send disconnect message
this.socket.close(); // Close the WebSocket connection
this.socket = null; // Clear the socket reference
this.notify("info", `Bot ${this.name} se ha desconectado de la sala.`);
},
// Overriding reconnect method - now primarily re-enters the current room
reconnect() {
if (this.room.id) {
this.notify("info", `Bot ${this.name} intentando reconectar a la sala ${this.room.id}.`);
this.enterRoom(this.room.id); // Re-use enterRoom logic
} else {
this.notify("warning", `Bot ${this.name} no tiene una sala establecida para reconectar.`);
}
}
});
} else {
console.error("BotClient class not found for modification. Join room functionality may not work.");
}
})("QBit");
// --- START UPDATED MODULE: AutodrawV2 (ACTUALIZADO con Modo Formas) ---
(function AutodrawV2() {
const QBit = globalThis[arguments[0]];
QBit.Styles.addRules([
`#${QBit.identifier} .autodraw-controls .cheat-row {
display: flex;
width: 100%;
margin-bottom: 5px;
}`,
`#${QBit.identifier} .autodraw-controls .cheat-row > * {
flex: 1;
margin: 0 2px;
text-align: center;
}`,
`#${QBit.identifier} .autodraw-controls input[type="number"],
#${QBit.identifier} .autodraw-controls input[type="file"] {
width: 100%;
padding: 5px;
box-sizing: border-box;
border: 1px solid var(--CE-color);
border-radius: .25rem;
background-color: var(--CE-bg_color);
color: var(--CE-color);
}`,
`#${QBit.identifier} .autodraw-controls .btn {
width: 100%;
padding: 5px;
box-sizing: border-box;
background-color: var(--secondary); /* Grey button for controls */
}`,
`#${QBit.identifier} .autodraw-controls .btn.active {
background-color: var(--success); /* Active state green */
}`,
`#${QBit.identifier} .autodraw-controls .btn i {
margin-right: 5px;
}`,
`#${QBit.identifier} .autodraw-controls .effect-toggle.active {
background-color: var(--info); /* Blue for active effect */
color: white;
}`,
`#${QBit.identifier} .autodraw-controls .effect-toggle {
background-color: var(--secondary);
color: var(--dark);
}`,
`#${QBit.identifier} .autodraw-controls label {
font-size: 0.85em;
margin-bottom: 3px;
display: block;
}`
]);
class AutodrawV2 extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#previewCanvas; // This internal canvas is now crucial for image processing
#originalCanvas; // The game's canvas (not used for image loading, but for dimensions)
#imageData = null; // Raw pixel data from the loaded image
#executionLine = []; // Array of drawing commands (lines/dots)
#drawingActive = false; // Control drawing loop
// Drawing Modes
#currentDrawingMode = 'rainMode'; // Default mode
#rainColumns = []; // For rain mode
#spiralAngle = 0; // For spiral mode
// UI Element References for settings
#imageSizeInput;
#brushSizeInput;
#pixelSizeInput;
#offsetXInput;
#offsetYInput;
#drawingSpeedInput;
#modeButtons = {}; // To manage active state of mode buttons
#imageFileInput; // Reference to the file input
// Default Settings
#defaultSettings = {
imageSize: 4,
brushSize: 32,
pixelSize: 1,
offsetX: 10,
offsetY: 0,
drawingSpeed: 10
};
constructor() {
super("Autodraw", '<i class="fas fa-magic"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#setupCanvas();
this.#setInitialSettings();
}
#loadInterface() {
const container = domMake.Tree("div", { class: "autodraw-controls" });
// Image Loader
const imageLoadRow = domMake.Row();
this.#imageFileInput = domMake.Tree("input", { type: "file", id: "autodraw-image-input", title: "Cargar Imagen (PNG/JPG)" });
this.#imageFileInput.addEventListener("change", (e) => this.#readImage(e.target));
imageLoadRow.appendChild(this.#imageFileInput);
container.appendChild(imageLoadRow);
// Drawing Settings - Row 1 (Image Size, Brush Size, Pixel Size)
const settingsRow1 = domMake.Row();
this.#imageSizeInput = domMake.Tree("input", { type: "number", min: "1", max: "20", value: this.#defaultSettings.imageSize, title: "Tamaño de Imagen (1=grande, 20=pequeña)" });
this.#brushSizeInput = domMake.Tree("input", { type: "number", min: "1", max: "100", value: this.#defaultSettings.brushSize, title: "Tamaño del Pincel" });
this.#pixelSizeInput = domMake.Tree("input", { type: "number", min: "1", max: "50", value: this.#defaultSettings.pixelSize, title: "Espacio entre Píxeles" });
settingsRow1.appendAll(
domMake.Tree("div", {}, [domMake.Tree("label", {}, ["Tamaño Img:"]), this.#imageSizeInput]),
domMake.Tree("div", {}, [domMake.Tree("label", {}, ["Tamaño Pincel:"]), this.#brushSizeInput]),
domMake.Tree("div", {}, [domMake.Tree("label", {}, ["Espacio Px:"]), this.#pixelSizeInput])
);
container.appendChild(settingsRow1);
// Drawing Settings - Row 2 (Offset X, Offset Y, Drawing Speed)
const settingsRow2 = domMake.Row();
this.#offsetXInput = domMake.Tree("input", { type: "number", min: "-50", max: "150", value: this.#defaultSettings.offsetX, title: "Desplazamiento X (0-100)" });
this.#offsetYInput = domMake.Tree("input", { type: "number", min: "-50", max: "150", value: this.#defaultSettings.offsetY, title: "Desplazamiento Y (0-100)" });
this.#drawingSpeedInput = domMake.Tree("input", { type: "number", min: "1", max: "100", value: this.#defaultSettings.drawingSpeed, title: "Velocidad de Dibujo (ms/línea)" });
settingsRow2.appendAll(
domMake.Tree("div", {}, [domMake.Tree("label", {}, ["Offset X:"]), this.#offsetXInput]),
domMake.Tree("div", {}, [domMake.Tree("label", {}, ["Offset Y:"]), this.#offsetYInput]),
domMake.Tree("div", {}, [domMake.Tree("label", {}, ["Vel. Dibujo (ms):"]), this.#drawingSpeedInput])
);
container.appendChild(settingsRow2);
// Control Buttons (Start/Stop)
const controlButtonsRow = domMake.Row();
const startButton = domMake.Button('<i class="fas fa-play-circle"></i> Iniciar Dibujo');
startButton.addEventListener("click", () => this.#startDrawing());
const stopButton = domMake.Button('<i class="fas fa-stop-circle"></i> Detener Dibujo');
stopButton.addEventListener("click", () => this.#stopDrawing());
controlButtonsRow.appendAll(startButton, stopButton);
container.appendChild(controlButtonsRow);
// Drawing Modes
const modesRow = domMake.Row();
modesRow.style.marginTop = "10px";
modesRow.style.flexWrap = "wrap";
modesRow.style.gap = "5px";
const addModeButton = (label, modeKey, iconClass) => {
const button = domMake.Button(`<i class="${iconClass}"></i> ${label}`);
button.classList.add("effect-toggle");
button.addEventListener("click", () => this.#setDrawingMode(modeKey));
this.#modeButtons[modeKey] = button;
modesRow.appendChild(button);
};
addModeButton("Modo Lluvia", "rainMode", "fas fa-cloud-rain");
addModeButton("Onda", "waveDraw", "fas fa-wave-square");
addModeButton("Espiral", "spiralDraw", "fas fa-circle-notch");
addModeButton("Píxel Aleatorio", "randomPixelDraw", "fas fa-dice");
addModeButton("Modo Formas", "shapeDrawMode", "fas fa-bezier-curve"); // Nuevo botón para el modo formas
container.appendChild(modesRow);
this.htmlElements.section.appendChild(container); // AÑADIDO: Asegura que el contenedor principal esté añadido
}
#setInitialSettings() {
this.#imageSizeInput.value = this.#defaultSettings.imageSize;
this.#brushSizeInput.value = this.#defaultSettings.brushSize;
this.#pixelSizeInput.value = this.#defaultSettings.pixelSize;
this.#offsetXInput.value = this.#defaultSettings.offsetX;
this.#offsetYInput.value = this.#defaultSettings.offsetY;
this.#drawingSpeedInput.value = this.#defaultSettings.drawingSpeed;
this.#setDrawingMode('rainMode'); // Activate rainMode by default
}
#setupCanvas() {
this.#previewCanvas = document.createElement('canvas');
this.#originalCanvas = document.getElementById('canvas'); // The game's drawing canvas
if (this.#originalCanvas) {
this.#previewCanvas.width = this.#originalCanvas.width;
this.#previewCanvas.height = this.#originalCanvas.height;
} else {
this.#previewCanvas.width = 1000;
this.#previewCanvas.height = 1000;
this.notify("warning", "Canvas del juego no encontrado. Usando dimensiones por defecto para el lienzo interno.");
}
}
#readImage(fileInput) {
if (!fileInput.files || !fileInput.files[0]) {
this.notify("warning", "No se seleccionó ninguna imagen.");
this.#imageData = null; // Clear any previous image data
return;
}
const FR = new FileReader();
FR.addEventListener('load', (evt) => {
const img = new Image();
img.addEventListener('load', () => {
this.notify("info", "Imagen cargada. Procesando pixeles...");
const ctx = this.#previewCanvas.getContext('2d');
ctx.clearRect(0, 0, this.#previewCanvas.width, this.#previewCanvas.height);
ctx.drawImage(img, 0, 0, this.#previewCanvas.width, this.#previewCanvas.height);
this.#imageData = ctx.getImageData(0, 0, this.#previewCanvas.width, this.#previewCanvas.height);
this.notify("success", "Imagen lista para dibujar.");
if (this.#currentDrawingMode) {
this.#prepareDrawingCommands();
}
});
img.crossOrigin = 'anonymous';
img.src = evt.target.result;
});
FR.readAsDataURL(fileInput.files[0]);
}
async #prepareDrawingCommands() {
if (!this.#imageData) {
this.notify("warning", "Carga una imagen primero.");
return false;
}
this.#executionLine = [];
const { width, height } = this.#imageData;
const size = parseFloat(this.#imageSizeInput.value);
const modifier = parseFloat(this.#pixelSizeInput.value);
const thickness = parseFloat(this.#brushSizeInput.value);
const offsetX = parseFloat(this.#offsetXInput.value);
const offsetY = parseFloat(this.#offsetYInput.value);
const toGameCoords = (x_pixel, y_pixel, originalImgWidth, originalImgHeight) => {
const finalDrawWidth = 100 / size;
const finalDrawHeight = 100 / size;
const gameX = (x_pixel / originalImgWidth) * finalDrawWidth + offsetX;
const gameY = (y_pixel / originalImgHeight) * finalDrawHeight + offsetY;
return [gameX, gameY];
};
const getPixelColor = (x, y) => {
const originalPxX = Math.floor(x * (this.#imageData.width / this.#previewCanvas.width));
const originalPxY = Math.floor(y * (this.#imageData.height / this.#previewCanvas.height));
if (originalPxX < 0 || originalPxX >= this.#imageData.width || originalPxY < 0 || originalPxY >= this.#imageData.height) return null;
const index = (originalPxY * this.#imageData.width + originalPxX) * 4;
const a = this.#imageData.data[index + 3];
if (a < 20) return null;
const r = this.#imageData.data[index + 0];
const g = this.#imageData.data[index + 1];
const b = this.#imageData.data[index + 2];
return `rgb(${r},${g},${b})`;
};
if (this.#currentDrawingMode === 'rainMode') {
for (let x = 0; x < width; x += modifier) {
let columnPixels = [];
for (let y = 0; y < height; y += modifier) {
const color = getPixelColor(x, y);
if (color) {
columnPixels.push({ x, y, color });
}
}
if (columnPixels.length > 0) {
this.#rainColumns.push({ x, pixels: columnPixels });
}
}
this.#shuffleArray(this.#rainColumns);
for (let col of this.#rainColumns) {
let pixels = col.pixels;
if (pixels.length === 0) continue;
pixels.sort((a, b) => a.y - b.y);
let startPixel = pixels[0];
let prevPixel = pixels[0];
for (let i = 1; i < pixels.length; i++) {
let currentPixel = pixels[i];
if (currentPixel.y !== prevPixel.y + modifier || currentPixel.color !== prevPixel.color) {
this.#executionLine.push({
pos1: toGameCoords(startPixel.x, startPixel.y, width, height),
pos2: toGameCoords(prevPixel.x, prevPixel.y, width, height),
color: startPixel.color,
thickness
});
startPixel = currentPixel;
}
prevPixel = currentPixel;
}
this.#executionLine.push({
pos1: toGameCoords(startPixel.x, startPixel.y, width, height),
pos2: toGameCoords(prevPixel.x, prevPixel.y, width, height),
color: startPixel.color,
thickness
});
}
} else if (this.#currentDrawingMode === 'waveDraw') {
const waveAmplitude = 15;
const waveFrequency = 0.05;
for (let y = 0; y < height; y += modifier) {
let startPixel = null;
let lastColor = null;
for (let x = 0; x < width; x += modifier) {
let currentX = x;
let currentY = y + waveAmplitude * Math.sin(x * waveFrequency);
const actualColor = getPixelColor(currentX, currentY);
if (actualColor) {
if (!startPixel) {
startPixel = { x: currentX, y: currentY, color: actualColor };
lastColor = actualColor;
} else if (actualColor !== lastColor) {
this.#executionLine.push({
pos1: toGameCoords(startPixel.x, startPixel.y, width, height),
pos2: toGameCoords(currentX, currentY, width, height),
color: lastColor,
thickness
});
startPixel = { x: currentX, y: currentY, color: actualColor };
lastColor = actualColor;
}
} else if (startPixel) {
this.#executionLine.push({
pos1: toGameCoords(startPixel.x, startPixel.y, width, height),
pos2: toGameCoords(currentX, currentY, width, height),
color: lastColor,
thickness
});
startPixel = null;
lastColor = null;
}
}
if (startPixel) {
this.#executionLine.push({
pos1: toGameCoords(startPixel.x, startPixel.y, width, height),
pos2: toGameCoords(width, y + waveAmplitude * Math.sin(width * waveFrequency), width, height),
color: lastColor,
thickness
});
}
}
} else if (this.#currentDrawingMode === 'spiralDraw') {
const centerX = width / 2;
const centerY = height / 2;
const maxRadius = Math.min(width, height) / 2;
const density = 0.5;
for (let r = 0; r < maxRadius; r += modifier * density) {
const numPoints = Math.floor(2 * Math.PI * r / modifier);
if (numPoints === 0) continue;
let prevX = -1, prevY = -1;
let startPoint = null;
let lastColor = null;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * 2 * Math.PI;
const spiralX = centerX + r * Math.cos(angle);
const spiralY = centerY + r * Math.sin(angle);
const color = getPixelColor(spiralX, spiralY);
if (color) {
if (startPoint === null) {
startPoint = { x: spiralX, y: spiralY, color: color };
lastColor = color;
} else if (color !== lastColor) {
this.#executionLine.push({
pos1: toGameCoords(startPoint.x, startPoint.y, width, height),
pos2: toGameCoords(prevX, prevY, width, height),
color: lastColor,
thickness
});
startPoint = { x: spiralX, y: spiralY, color: color };
lastColor = color;
}
prevX = spiralX;
prevY = spiralY;
} else if (startPoint !== null) {
this.#executionLine.push({
pos1: toGameCoords(startPoint.x, startPoint.y, width, height),
pos2: toGameCoords(prevX, prevY, width, height),
color: lastColor,
thickness
});
startPoint = null;
}
}
if (startPoint) {
this.#executionLine.push({
pos1: toGameCoords(startPoint.x, startPoint.y, width, height),
pos2: toGameCoords(prevX, prevY, width, height),
color: lastColor,
thickness
});
}
}
} else if (this.#currentDrawingMode === 'randomPixelDraw') {
let allPixels = [];
for (let y = 0; y < height; y += modifier) {
for (let x = 0; x < width; x += modifier) {
const color = getPixelColor(x, y);
if (color) {
allPixels.push({ x, y, color });
}
}
}
this.#shuffleArray(allPixels);
allPixels.forEach(p => {
this.#executionLine.push({
pos1: toGameCoords(p.x, p.y, width, height),
pos2: toGameCoords(p.x + modifier / 2, p.y + modifier / 2, width, height),
color: p.color,
thickness
});
});
} else if (this.#currentDrawingMode === 'shapeDrawMode') {
const shapeDetectorClass = this.findGlobal("ShapeDetector");
if (!shapeDetectorClass || !shapeDetectorClass.siblings || shapeDetectorClass.siblings.length === 0) {
this.notify("error", "El módulo 'Detector de Formas' no está activo. Asegúrate de que está cargado.");
return false;
}
const shapeDetectorInstance = shapeDetectorClass.siblings[0];
if (!shapeDetectorInstance || typeof shapeDetectorInstance.analyzeImageDataForShapes !== 'function') {
this.notify("error", "El módulo 'Detector de Formas' no está listo o le falta el método 'analyzeImageDataForShapes'.");
return false;
}
this.notify("info", "Analizando imagen para formas con Detector de Formas...");
const { drawingCommands: shapeCommands } = await shapeDetectorInstance.analyzeImageDataForShapes(
this.#imageData,
this.#imageData.width,
this.#imageData.height,
false
);
if (shapeCommands.length === 0) {
this.notify("warning", "No se detectaron formas significativas en la imagen para dibujar.");
return false;
}
shapeCommands.forEach(cmd => {
this.#executionLine.push({
pos1: [cmd.x1, cmd.y1], // Ya vienen en coords 0-100 del juego
pos2: [cmd.x2, cmd.y2],
color: this.htmlElements.colorInput?.value || "#000000",
thickness: this.#brushSizeInput.value
});
});
this.notify("success", `Modo Formas: Se prepararon ${this.#executionLine.length} líneas desde formas detectadas.`);
}
this.notify("info", `Comandos de dibujo preparados para el modo '${this.#currentDrawingMode}': ${this.#executionLine.length} líneas.`);
return true;
}
#shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
async #startDrawing() {
const commandsPrepared = await this.#prepareDrawingCommands();
if (!commandsPrepared) {
return;
}
const botManager = this.findGlobal("BotClientManager")?.siblings[0];
if (!botManager || botManager.children.length === 0) {
this.notify("warning", "Necesitas al menos 1 bot activo en 'BotClientManager' para dibujar.");
return;
}
const activeBots = botManager.children.filter(b => b.bot && b.bot.getReadyState());
if (activeBots.length === 0) {
this.notify("warning", "Ningún bot está conectado y listo para dibujar. Conecta un bot primero.");
return;
}
this.#drawingActive = true;
this.notify("info", `Iniciando dibujo con ${activeBots.length} bots...`);
const delayMs = parseInt(this.#drawingSpeedInput.value);
let currentLineIndex = 0;
while (this.#drawingActive && currentLineIndex < this.#executionLine.length) {
const line = this.#executionLine[currentLineIndex];
const botIndex = currentLineIndex % activeBots.length;
const botInterface = activeBots[botIndex];
if (botInterface && botInterface.bot && botInterface.bot.getReadyState()) {
botInterface.bot.emit(
"line",
-1,
line.pos1[0], line.pos1[1],
line.pos2[0], line.pos2[1],
true,
line.thickness,
line.color,
false
);
currentLineIndex++;
} else {
this.notify("warning", `Bot ${botInterface ? botInterface.getName() : 'desconocido'} no está listo. Saltando...`);
currentLineIndex++;
}
await new Promise(resolve => setTimeout(resolve, delayMs));
}
if (!this.#drawingActive) {
this.notify("info", `Dibujo detenido manualmente. Completado ${currentLineIndex} de ${this.#executionLine.length} líneas.`);
} else {
this.notify("success", "Dibujo completado!");
}
this.#drawingActive = false;
}
#stopDrawing() {
this.#drawingActive = false;
}
#setDrawingMode(mode) {
this.#currentDrawingMode = mode;
for (const key in this.#modeButtons) {
if (this.#modeButtons.hasOwnProperty(key)) {
this.#modeButtons[key].classList.toggle("active", key === mode);
}
}
this.notify("info", `Modo de dibujo cambiado a: '${mode}'`);
if (this.#imageData && this.#imageData.data && this.#imageData.data.length > 0) {
this.#prepareDrawingCommands();
}
}
}
})("QBit");
// --- END UPDATED MODULE: AutodrawV2 ---
(function IntelligentArtist() {
const QBit = globalThis[arguments[0]];
// --- BASE DE DATOS DE BOCETOS DETALLADOS (50 PALABRAS) ---
// (X, Y son porcentajes del 0 al 100)
// Cada boceto está diseñado para ser reconocible y compuesto por múltiples trazos.
// Helper para bocetos simples por defecto (genera círculos o cuadrados)
function generateDefaultSimpleSketch(word) {
const hash = word.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const isCircle = hash % 2 === 0; // Alternar entre círculo y cuadrado
if (isCircle) {
const centerX = 50, centerY = 50, radius = 15;
const segments = 16; // Más segmentos para un círculo más suave
const sketch = [];
for (let i = 0; i < segments; i++) {
const angle1 = (i / segments) * Math.PI * 2;
const angle2 = ((i + 1) / segments) * Math.PI * 2;
sketch.push({
x1: centerX + radius * Math.cos(angle1),
y1: centerY + radius * Math.sin(angle1),
x2: centerX + radius * Math.cos(angle2),
y2: centerY + radius * Math.sin(angle2)
});
}
return sketch;
} else {
// Generar un cuadrado o un rectángulo simple
const xOffset = 25 + (hash % 10); // Ligeramente variable
const yOffset = 25 + (hash % 10);
const size = 50 - (hash % 10);
return [
{ x1: xOffset, y1: yOffset, x2: xOffset + size, y2: yOffset },
{ x1: xOffset + size, y1: yOffset, x2: xOffset + size, y2: yOffset + size },
{ x1: xOffset + size, y1: yOffset + size, x2: xOffset, y2: yOffset + size },
{ x1: xOffset, y1: yOffset + size, x2: xOffset, y2: yOffset }
];
}
}
const SKETCH_DATABASE = {
// --- BOCETOS DETALLADOS MEJORADOS (Simulando curvas y texturas) ---
"ARBOL": [
// Tronco (más orgánico y con textura)
{ x1: 45, y1: 80, x2: 43, y2: 60 }, { x1: 43, y1: 60, x2: 45, y2: 40 },
{ x1: 55, y1: 80, x2: 57, y2: 60 }, { x1: 57, y1: 60, x2: 55, y2: 40 },
// Textura del tronco (Bark)
{ x1: 48, y1: 75, x2: 48, y2: 70 }, { x1: 51, y1: 65, x2: 51, y2: 60 },
{ x1: 46, y1: 55, x2: 46, y2: 50 },
// Raíces más definidas
{ x1: 45, y1: 80, x2: 35, y2: 85 }, { x1: 55, y1: 80, x2: 65, y2: 85 },
// Follaje (Contorno más irregular y segmentado)
{ x1: 45, y1: 40, x2: 30, y2: 45 }, { x1: 30, y1: 45, x2: 25, y2: 35 },
{ x1: 25, y1: 35, x2: 35, y2: 25 }, { x1: 35, y1: 25, x2: 40, y2: 20 },
{ x1: 40, y1: 20, x2: 50, y2: 15 }, { x1: 50, y1: 15, x2: 60, y2: 20 },
{ x1: 60, y1: 20, x2: 70, y2: 25 }, { x1: 70, y1: 25, x2: 75, y2: 35 },
{ x1: 75, y1: 35, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 55, y2: 40 },
// Detalles internos del follaje (Clumps of leaves)
{ x1: 45, y1: 35, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 55, y2: 35 },
{ x1: 35, y1: 40, x2: 30, y2: 35 }, { x1: 65, y1: 40, x2: 70, y2: 35 },
{ x1: 50, y1: 25, x2: 45, y2: 22 }, { x1: 50, y1: 25, x2: 55, y2: 22 }
],
"CASA": [
// Estructura principal
{ x1: 30, y1: 80, x2: 70, y2: 80 }, { x1: 30, y1: 80, x2: 30, y2: 50 },
{ x1: 70, y1: 80, x2: 70, y2: 50 },
// Tejado (Con alero)
{ x1: 25, y1: 50, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 75, y2: 50 },
{ x1: 25, y1: 50, x2: 75, y2: 50 },
// Textura del tejado (Shingles)
{ x1: 28, y1: 45, x2: 72, y2: 45 }, { x1: 32, y1: 40, x2: 68, y2: 40 },
{ x1: 38, y1: 35, x2: 62, y2: 35 },
// Chimenea con ladrillos
{ x1: 60, y1: 40, x2: 60, y2: 25 }, { x1: 65, y1: 40, x2: 65, y2: 25 },
{ x1: 60, y1: 25, x2: 65, y2: 25 },
{ x1: 60, y1: 30, x2: 65, y2: 30 }, { x1: 60, y1: 35, x2: 65, y2: 35 }, // Ladrillos
// Puerta con pomo y marco
{ x1: 45, y1: 80, x2: 45, y2: 60 }, { x1: 55, y1: 80, x2: 55, y2: 60 },
{ x1: 45, y1: 60, x2: 55, y2: 60 },
{ type: "circle", x1: 53, y1: 70, radius: 1 }, // Pomo
// Ventana con cruz y marco
{ x1: 32, y1: 55, x2: 42, y2: 55 }, { x1: 42, y1: 55, x2: 42, y2: 65 },
{ x1: 42, y1: 65, x2: 32, y2: 65 }, { x1: 32, y1: 65, x2: 32, y2: 55 },
{ x1: 37, y1: 55, x2: 37, y2: 65 }, { x1: 32, y1: 60, x2: 42, y2: 60 } // Cruz ventana
],
"SOL": [
{ type: "circle", x1: 50, y1: 50, radius: 15 }, // Círculo central
// Rayos (más variados y con un ligero patrón)
{ x1: 50, y1: 35, x2: 50, y2: 25 }, { x1: 50, y1: 35, x2: 48, y2: 28 }, { x1: 50, y1: 35, x2: 52, y2: 28 }, // Arriba
{ x1: 60, y1: 40, x2: 68, y2: 32 }, { x1: 60, y1: 40, x2: 65, y2: 35 }, // Arriba-derecha
{ x1: 65, y1: 50, x2: 75, y2: 50 }, { x1: 65, y1: 50, x2: 70, y2: 48 }, // Derecha
{ x1: 60, y1: 60, x2: 68, y2: 68 }, { x1: 60, y1: 60, x2: 65, y2: 65 }, // Abajo-derecha
{ x1: 50, y1: 65, x2: 50, y2: 75 }, { x1: 50, y1: 65, x2: 48, y2: 72 }, { x1: 50, y1: 65, x2: 52, y2: 72 }, // Abajo
{ x1: 40, y1: 60, x2: 32, y2: 68 }, { x1: 40, y1: 60, x2: 35, y2: 65 }, // Abajo-izquierda
{ x1: 35, y1: 50, x2: 25, y2: 50 }, { x1: 35, y1: 50, x2: 30, y2: 48 }, // Izquierda
{ x1: 40, y1: 40, x2: 32, y2: 32 }, { x1: 40, y1: 40, x2: 35, y2: 35 }, // Arriba-izquierda
// Ojos y Boca (con más expresión)
{ type: "circle", x1: 45, y1: 45, radius: 1.5 }, { type: "circle", x1: 55, y1: 45, radius: 1.5 }, // Ojos más grandes
{ x1: 45, y1: 55, x2: 47, y2: 58 }, { x1: 47, y1: 58, x2: 53, y2: 58 }, { x1: 53, y1: 58, x2: 55, y2: 55 } // Boca más curvada
],
"FLOR": [
{ x1: 50, y1: 80, x2: 50, y2: 55 }, // Tallo
// Hojas (Más orgánicas y con venas)
{ x1: 50, y1: 70, x2: 40, y2: 65 }, { x1: 40, y1: 65, x2: 45, y2: 58 },
{ x1: 42, y1: 66, x2: 48, y2: 62 }, // Vena hoja 1
{ x1: 50, y1: 70, x2: 60, y2: 65 }, { x1: 60, y1: 65, x2: 55, y2: 58 },
{ x1: 58, y1: 66, x2: 52, y2: 62 }, // Vena hoja 2
// Centro de la flor
{ type: "circle", x1: 50, y1: 50, radius: 5 }, // Círculo central para estambres
{ type: "circle", x1: 50, y1: 50, radius: 2 }, // Punto central del estambre
// Pétalos (Más suaves y superpuestos, usando más segmentos)
{ x1: 50, y1: 45, x2: 40, y2: 38 }, { x1: 40, y1: 38, x2: 35, y2: 45 }, { x1: 35, y1: 45, x2: 38, y2: 55 }, { x1: 38, y1: 55, x2: 45, y2: 55 }, { x1: 45, y1: 55, x2: 50, y2: 45 }, // Pétalo superior-izquierda
{ x1: 50, y1: 45, x2: 60, y2: 38 }, { x1: 60, y1: 38, x2: 65, y2: 45 }, { x1: 65, y1: 45, x2: 62, y2: 55 }, { x1: 62, y1: 55, x2: 55, y2: 55 }, { x1: 55, y1: 55, x2: 50, y2: 45 }, // Pétalo superior-derecha
{ x1: 45, y1: 50, x2: 38, y2: 58 }, { x1: 38, y1: 58, x2: 40, y2: 65 }, { x1: 40, y1: 65, x2: 48, y2: 65 }, { x1: 48, y1: 65, x2: 50, y2: 55 }, { x1: 50, y1: 55, x2: 45, y2: 50 }, // Pétalo inferior-izquierda
{ x1: 55, y1: 50, x2: 62, y2: 58 }, { x1: 62, y1: 58, x2: 60, y2: 65 }, { x1: 60, y1: 65, x2: 52, y2: 65 }, { x1: 52, y1: 65, x2: 50, y2: 55 }, { x1: 50, y1: 55, x2: 55, y2: 50 } // Pétalo inferior-derecha
],
"PERRO": [
// Cuerpo (Más curvo y orgánico)
{ x1: 40, y1: 60, x2: 50, y2: 55 }, { x1: 50, y1: 55, x2: 70, y2: 58 }, // Lomo
{ x1: 40, y1: 70, x2: 50, y2: 68 }, { x1: 50, y1: 68, x2: 70, y2: 65 }, // Vientre
// Cabeza y Hocico
{ x1: 30, y1: 65, x2: 40, y2: 60 }, { x1: 40, y1: 60, x2: 35, y2: 50 }, // Cuello y cabeza
{ x1: 35, y1: 50, x2: 25, y2: 55 }, { x1: 25, y1: 55, x2: 30, y2: 65 }, // Hocico
// Oreja
{ x1: 35, y1: 50, x2: 30, y2: 45 }, { x1: 30, y1: 45, x2: 40, y2: 50 },
// Ojo y Nariz
{ type: "circle", x1: 32, y1: 55, radius: 1 }, // Ojo
{ x1: 25, y1: 55, x2: 26, y2: 56 }, // Nariz (punto)
// Patas (Con grosor y articulaciones)
// Delantera
{ x1: 40, y1: 70, x2: 40, y2: 80 }, { x1: 43, y1: 70, x2: 43, y2: 80 },
// Trasera
{ x1: 70, y1: 65, x2: 65, y2: 75 }, { x1: 65, y1: 75, x2: 65, y2: 80 },
{ x1: 73, y1: 65, x2: 68, y2: 80 },
// Cola (Curva)
{ x1: 70, y1: 58, x2: 75, y2: 50 }, { x1: 75, y1: 50, x2: 80, y2: 45 },
// Textura de pelaje (líneas cortas)
{ x1: 50, y1: 55, x2: 50, y2: 53 }, { x1: 60, y1: 56, x2: 60, y2: 54 },
{ x1: 45, y1: 68, x2: 45, y2: 70 }, { x1: 55, y1: 68, x2: 55, y2: 70 }
],
"GATO": [
// Cuerpo (sentado, más curvo)
{ x1: 40, y1: 80, x2: 60, y2: 80 }, // Base
{ x1: 40, y1: 80, x2: 35, y2: 60 }, { x1: 35, y1: 60, x2: 40, y2: 50 }, // Lado Izq
{ x1: 60, y1: 80, x2: 65, y2: 60 }, { x1: 65, y1: 60, x2: 60, y2: 50 }, // Lado Der
// Cabeza (más redonda)
{ type: "circle", x1: 50, y1: 45, radius: 10 },
// Orejas puntiagudas detalladas
{ x1: 42, y1: 40, x2: 40, y2: 30 }, { x1: 40, y1: 30, x2: 48, y2: 38 }, // Oreja Izq
{ x1: 58, y1: 40, x2: 60, y2: 30 }, { x1: 60, y1: 30, x2: 52, y2: 38 }, // Oreja Der
// Ojos y Nariz
{ type: "circle", x1: 45, y1: 45, radius: 1 }, { type: "circle", x1: 55, y1: 45, radius: 1 },
{ x1: 50, y1: 48, x2: 50, y2: 50 }, // Nariz y boca (línea Y)
{ x1: 50, y1: 50, x2: 47, y2: 52 }, { x1: 50, y1: 50, x2: 53, y2: 52 },
// Bigotes (Whiskers)
{ x1: 45, y1: 50, x2: 30, y2: 48 }, { x1: 45, y1: 51, x2: 30, y2: 51 },
{ x1: 55, y1: 50, x2: 70, y2: 48 }, { x1: 55, y1: 51, x2: 70, y2: 51 },
// Cola (enroscada alrededor del cuerpo)
{ x1: 65, y1: 75, x2: 75, y2: 75 }, { x1: 75, y1: 75, x2: 70, y2: 80 },
// Textura de pelaje (Hatching ligero)
{ x1: 50, y1: 60, x2: 50, y2: 58 }, { x1: 45, y1: 70, x2: 45, y2: 68 },
{ x1: 55, y1: 70, x2: 55, y2: 68 }
],
"MESA": [
// Superficie (con grosor y perspectiva mejorada)
{ x1: 25, y1: 40, x2: 75, y2: 40 }, // Borde frontal
{ x1: 25, y1: 40, x2: 20, y2: 35 }, // Lado izquierdo superior
{ x1: 75, y1: 40, x2: 80, y2: 35 }, // Lado derecho superior
{ x1: 20, y1: 35, x2: 80, y2: 35 }, // Borde trasero
{ x1: 25, y1: 45, x2: 75, y2: 45 }, // Borde frontal inferior (grosor)
{ x1: 20, y1: 40, x2: 20, y2: 35 }, // Lateral inferior izq
{ x1: 80, y1: 40, x2: 80, y2: 35 }, // Lateral inferior der
// Patas delanteras (con grosor)
{ x1: 30, y1: 45, x2: 30, y2: 70 }, { x1: 32, y1: 45, x2: 32, y2: 70 },
{ x1: 68, y1: 45, x2: 68, y2: 70 }, { x1: 70, y1: 45, x2: 70, y2: 70 },
// Patas traseras en perspectiva (con grosor)
{ x1: 20, y1: 35, x2: 20, y2: 60 }, { x1: 22, y1: 35, x2: 22, y2: 60 }, // Pata trasera izq
{ x1: 78, y1: 35, x2: 78, y2: 60 }, { x1: 80, y1: 35, x2: 80, y2: 60 }, // Pata trasera der
// Líneas de conexión para perspectiva
{ x1: 30, y1: 70, x2: 20, y2: 60 }, // Diagonal inferior izq
{ x1: 70, y1: 70, x2: 80, y2: 60 } // Diagonal inferior der
],
"SILLA": [
// Respaldo (con detalles de listones)
{ x1: 35, y1: 40, x2: 35, y2: 60 }, { x1: 65, y1: 40, x2: 65, y2: 60 }, // Postes verticales
{ x1: 35, y1: 40, x2: 65, y2: 40 }, // Borde superior
{ x1: 38, y1: 45, x2: 62, y2: 45 }, { x1: 38, y1: 50, x2: 62, y2: 50 }, // Listones horizontales
{ x1: 38, y1: 55, x2: 62, y2: 55 },
// Asiento (con perspectiva)
{ x1: 35, y1: 60, x2: 65, y2: 60 }, // Borde frontal
{ x1: 35, y1: 60, x2: 32, y2: 63 }, // Lado izquierdo
{ x1: 65, y1: 60, x2: 68, y2: 63 }, // Lado derecho
{ x1: 32, y1: 63, x2: 68, y2: 63 }, // Borde trasero
// Patas delanteras (con grosor)
{ x1: 38, y1: 60, x2: 38, y2: 75 }, { x1: 40, y1: 60, x2: 40, y2: 75 },
{ x1: 60, y1: 60, x2: 60, y2: 75 }, { x1: 62, y1: 60, x2: 62, y2: 75 },
// Patas traseras en perspectiva (con grosor y curvatura)
{ x1: 32, y1: 63, x2: 32, y2: 75 }, { x1: 30, y1: 63, x2: 30, y2: 75 }, // Pata trasera izq
{ x1: 68, y1: 63, x2: 68, y2: 75 }, { x1: 70, y1: 63, x2: 70, y2: 75 }, // Pata trasera der
// Conexiones de las patas al asiento
{ x1: 38, y1: 60, x2: 32, y2: 63 },
{ x1: 62, y1: 60, x2: 68, y2: 63 }
],
"LIBRO": [
// Lado izquierdo (con grosor de página)
{ x1: 30, y1: 40, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 50, y2: 70 },
{ x1: 50, y1: 70, x2: 30, y2: 60 }, { x1: 30, y1: 60, x2: 30, y2: 40 },
{ x1: 32, y1: 42, x2: 48, y2: 32 }, { x1: 48, y1: 32, x2: 48, y2: 68 }, // Página izquierda interior
// Lado derecho (con grosor de página)
{ x1: 50, y1: 30, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 70, y2: 60 },
{ x1: 70, y1: 60, x2: 50, y2: 70 },
{ x1: 52, y1: 32, x2: 68, y2: 42 }, { x1: 68, y1: 42, x2: 68, y2: 68 }, // Página derecha interior
// Lomo del libro
{ x1: 50, y1: 30, x2: 50, y2: 70 },
// Páginas (líneas que simulan texto)
{ x1: 35, y1: 45, x2: 45, y2: 40 }, { x1: 35, y1: 50, x2: 45, y2: 45 },
{ x1: 55, y1: 45, x2: 65, y2: 50 }, { x1: 55, y1: 50, x2: 65, y2: 55 }
],
"TAZA": [
// Cuerpo de la taza (más curvo y con base más ancha)
{ x1: 40, y1: 40, x2: 60, y2: 40 }, // Borde superior
{ x1: 38, y1: 40, x2: 35, y2: 65 }, // Lado izquierdo
{ x1: 62, y1: 40, x2: 65, y2: 65 }, // Lado derecho
{ x1: 35, y1: 65, x2: 65, y2: 65 }, // Base (curvada)
// Asa (con grosor y curvatura)
{ x1: 65, y1: 45, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 70, y2: 55 }, { x1: 70, y1: 55, x2: 65, y2: 55 },
{ x1: 65, y1: 47, x2: 68, y2: 47 }, { x1: 68, y1: 47, x2: 68, y2: 53 }, { x1: 68, y1: 53, x2: 65, y2: 53 }, // Grosor del asa
// Interior de la taza (para dar profundidad)
{ x1: 42, y1: 42, x2: 58, y2: 42 }
],
"VENTANA": [
// Marco exterior (con grosor)
{ x1: 28, y1: 28, x2: 72, y2: 28 }, { x1: 72, y1: 28, x2: 72, y2: 72 },
{ x1: 72, y1: 72, x2: 28, y2: 72 }, { x1: 28, y1: 72, x2: 28, y2: 28 },
{ x1: 30, y1: 30, x2: 70, y2: 30 }, { x1: 70, y1: 30, x2: 70, y2: 70 },
{ x1: 70, y1: 70, x2: 30, y2: 70 }, { x1: 30, y1: 70, x2: 30, y2: 30 },
// Marco interior (con grosor y divisiones)
{ x1: 35, y1: 35, x2: 65, y2: 35 }, { x1: 65, y1: 35, x2: 65, y2: 65 },
{ x1: 65, y1: 65, x2: 35, y2: 65 }, { x1: 35, y1: 65, x2: 35, y2: 35 },
{ x1: 40, y1: 35, x2: 40, y2: 65 }, { x1: 50, y1: 35, x2: 50, y2: 65 },
{ x1: 60, y1: 35, x2: 60, y2: 65 },
{ x1: 35, y1: 40, x2: 65, y2: 40 }, { x1: 35, y1: 50, x2: 65, y2: 50 },
{ x1: 35, y1: 60, x2: 65, y2: 60 }
],
"PUERTA": [
// Puerta principal (con paneles y marco)
{ x1: 40, y1: 30, x2: 60, y2: 30 }, { x1: 60, y1: 30, x2: 60, y2: 70 },
{ x1: 60, y1: 70, x2: 40, y2: 70 }, { x1: 40, y1: 70, x2: 40, y2: 30 },
// Marco exterior (con grosor)
{ x1: 38, y1: 28, x2: 38, y2: 72 }, { x1: 62, y1: 28, x2: 62, y2: 72 },
{ x1: 38, y1: 28, x2: 62, y2: 28 }, { x1: 38, y1: 72, x2: 62, y2: 72 },
// Paneles de la puerta
{ x1: 43, y1: 35, x2: 57, y2: 35 }, { x1: 57, y1: 35, x2: 57, y2: 45 },
{ x1: 57, y1: 45, x2: 43, y2: 45 }, { x1: 43, y1: 45, x2: 43, y2: 35 },
{ x1: 43, y1: 50, x2: 57, y2: 50 }, { x1: 57, y1: 50, x2: 57, y2: 65 },
{ x1: 57, y1: 65, x2: 43, y2: 65 }, { x1: 43, y1: 65, x2: 43, y2: 50 },
// Pomo (círculo)
{ type: "circle", x1: 55, y1: 50, radius: 2 }
],
"CAJA": [
// Frente de la caja (con grosor de las líneas)
{ x1: 30, y1: 40, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 70, y2: 60 },
{ x1: 70, y1: 60, x2: 30, y2: 60 }, { x1: 30, y1: 60, x2: 30, y2: 40 },
// Tapa (con grosor y ligera elevación)
{ x1: 30, y1: 40, x2: 35, y2: 35 }, { x1: 70, y1: 40, x2: 75, y2: 35 },
{ x1: 35, y1: 35, x2: 75, y2: 35 }, // Borde frontal de la tapa
{ x1: 32, y1: 38, x2: 72, y2: 38 }, // Borde trasero de la tapa (grosor)
{ x1: 35, y1: 35, x2: 32, y2: 38 }, { x1: 75, y1: 35, x2: 72, y2: 38 },
// Lado derecho (con grosor)
{ x1: 70, y1: 40, x2: 75, y2: 35 }, { x1: 70, y1: 60, x2: 75, y2: 55 }, // Perspectiva
{ x1: 75, y1: 35, x2: 75, y2: 55 },
// Sombreado interior (opcional, para dar profundidad)
{ x1: 32, y1: 60, x2: 32, y2: 58 }, { x1: 32, y1: 58, x2: 68, y2: 58 }, { x1: 68, y1: 58, x2: 68, y2: 60 }
],
"BOTELLA": [
// Cuello (más largo y definido)
{ x1: 48, y1: 20, x2: 52, y2: 20 }, // Boca
{ x1: 48, y1: 20, x2: 45, y2: 30 }, { x1: 52, y1: 20, x2: 55, y2: 30 }, // Parte superior del cuello
{ x1: 45, y1: 30, x2: 45, y2: 35 }, { x1: 55, y1: 30, x2: 55, y2: 35 }, // Parte inferior del cuello
// Hombros de la botella (curva)
{ x1: 45, y1: 35, x2: 40, y2: 45 }, { x1: 55, y1: 35, x2: 60, y2: 45 },
// Cuerpo (más curvado)
{ x1: 40, y1: 45, x2: 38, y2: 70 }, { x1: 60, y1: 45, x2: 62, y2: 70 },
// Base (con grosor y curvatura)
{ x1: 38, y1: 70, x2: 62, y2: 70 },
{ x1: 39, y1: 72, x2: 61, y2: 72 } // Sombra de la base
],
"CAMISA": [
// Cuello
{ x1: 45, y1: 35, x2: 55, y2: 35 }, { x1: 45, y1: 35, x2: 40, y2: 40 }, // Lado izquierdo del cuello
{ x1: 55, y1: 35, x2: 60, y2: 40 }, // Lado derecho del cuello
// Hombros y mangas (con pliegues)
{ x1: 40, y1: 40, x2: 25, y2: 50 }, { x1: 25, y1: 50, x2: 20, y2: 60 }, // Manga izquierda
{ x1: 60, y1: 40, x2: 75, y2: 50 }, { x1: 75, y1: 50, x2: 80, y2: 60 }, // Manga derecha
// Cuerpo (con pliegues y dobladillo)
{ x1: 20, y1: 60, x2: 20, y2: 75 }, { x1: 80, y1: 60, x2: 80, y2: 75 },
{ x1: 20, y1: 75, x2: 80, y2: 75 },
// Línea central y botones
{ x1: 50, y1: 40, x2: 50, y2: 75 },
{ type: "circle", x1: 50, y1: 48, radius: 1 }, { type: "circle", x1: 50, y1: 55, radius: 1 },
{ type: "circle", x1: 50, y1: 62, radius: 1 }, { type: "circle", x1: 50, y1: 69, radius: 1 },
// Pliegues sutiles en la tela
{ x1: 30, y1: 65, x2: 35, y2: 68 }, { x1: 70, y1: 65, x2: 65, y2: 68 }
],
"ZAPATO": [
// Suela (más gruesa y con relieve)
{ x1: 20, y1: 70, x2: 80, y2: 70 }, { x1: 20, y1: 70, x2: 25, y2: 75 },
{ x1: 80, y1: 70, x2: 75, y2: 75 }, { x1: 25, y1: 75, x2: 75, y2: 75 },
{ x1: 30, y1: 72, x2: 35, y2: 72 }, { x1: 40, y1: 72, x2: 45, y2: 72 }, // Relieve suela
// Cuerpo del zapato (más orgánico y con punta)
{ x1: 20, y1: 70, x2: 25, y2: 60 }, { x1: 25, y1: 60, x2: 35, y2: 55 },
{ x1: 35, y1: 55, x2: 65, y2: 55 }, { x1: 65, y1: 55, x2: 75, y2: 60 },
{ x1: 75, y1: 60, x2: 80, y2: 70 },
// Lengüeta del zapato
{ x1: 40, y1: 55, x2: 45, y2: 50 }, { x1: 45, y1: 50, x2: 55, y2: 50 }, { x1: 55, y1: 50, x2: 60, y2: 55 },
// Cordones (con más detalle)
{ x1: 42, y1: 56, x2: 58, y2: 56 }, // Línea base
{ x1: 40, y1: 57, x2: 45, y2: 53 }, { x1: 45, y1: 53, x2: 50, y2: 57 }, // Cruce 1
{ x1: 50, y1: 57, x2: 55, y2: 53 }, { x1: 55, y1: 53, x2: 60, y2: 57 } // Cruce 2
],
"AGUA": [
// Superficie del agua (ondas y reflejos)
{ x1: 20, y1: 70, x2: 80, y2: 70 }, { x1: 25, y1: 65, x2: 75, y2: 65 },
{ x1: 30, y1: 60, x2: 70, y2: 60 }, // Ondas concéntricas
{ x1: 35, y1: 55, x2: 65, y2: 55 },
// Gotas de agua (con brillo)
{ type: "circle", x1: 50, y1: 40, radius: 5 }, { type: "circle", x1: 48, y1: 38, radius: 1 }, // Gota 1 con brillo
{ type: "circle", x1: 40, y1: 50, radius: 4 }, { type: "circle", x1: 39, y1: 49, radius: 0.8 }, // Gota 2 con brillo
{ type: "circle", x1: 60, y1: 55, radius: 3 }, { type: "circle", x1: 59, y1: 54, radius: 0.6 } // Gota 3 con brillo
],
"FUEGO": [
// Llama (más dinámica y con múltiples picos)
{ x1: 50, y1: 75, x2: 40, y2: 65 }, { x1: 40, y1: 65, x2: 45, y2: 50 },
{ x1: 45, y1: 50, x2: 50, y2: 40 }, { x1: 50, y1: 40, x2: 55, y2: 50 },
{ x1: 55, y1: 50, x2: 60, y2: 65 }, { x1: 60, y1: 65, x2: 50, y2: 75 },
{ x1: 48, y1: 55, x2: 45, y2: 48 }, { x1: 45, y1: 48, x2: 50, y2: 42 }, // Llama interior 1
{ x1: 52, y1: 55, x2: 55, y2: 48 }, { x1: 55, y1: 48, x2: 50, y2: 42 }, // Llama interior 2
// Base del fuego (simulando troncos o brasas)
{ x1: 40, y1: 75, x2: 60, y2: 75 },
{ x1: 42, y1: 77, x2: 50, y2: 77 }, { x1: 50, y1: 77, x2: 58, y2: 77 } // Líneas internas de la base
],
"VIENTO": [
// Líneas de viento con mayor dinamismo y remolinos definidos
{ x1: 15, y1: 50, x2: 85, y2: 50 }, { x1: 20, y1: 55, x2: 90, y2: 55 },
{ x1: 25, y1: 60, x2: 95, y2: 60 },
// Remolino 1 (espiral simulada con segmentos)
{ x1: 30, y1: 45, x2: 35, y2: 40 }, { x1: 35, y1: 40, x2: 40, y2: 45 },
{ x1: 40, y1: 45, x2: 35, y2: 50 }, { x1: 35, y1: 50, x2: 30, y2: 45 },
// Remolino 2
{ x1: 70, y1: 45, x2: 75, y2: 40 }, { x1: 75, y1: 40, x2: 80, y2: 45 },
{ x1: 80, y1: 45, x2: 75, y2: 50 }, { x1: 75, y1: 50, x2: 70, y2: 45 },
// Pequeñas ráfagas adicionales
{ x1: 45, y1: 40, x2: 40, y2: 38 }, { x1: 55, y1: 40, x2: 60, y2: 38 }
],
"TIERRA": [
// Horizonte montañoso (más picos y valles)
{ x1: 20, y1: 70, x2: 30, y2: 65 }, { x1: 30, y1: 65, x2: 35, y2: 70 },
{ x1: 35, y1: 70, x2: 45, y2: 60 }, { x1: 45, y1: 60, x2: 50, y2: 65 },
{ x1: 50, y1: 65, x2: 60, y2: 55 }, { x1: 60, y1: 55, x2: 65, y2: 60 },
{ x1: 65, y1: 60, x2: 75, y2: 65 }, { x1: 75, y1: 65, x2: 80, y2: 70 },
// Terreno frontal (rocas y textura)
{ x1: 25, y1: 75, x2: 35, y2: 78 }, { x1: 40, y1: 75, x2: 45, y2: 78 },
{ x1: 55, y1: 75, x2: 60, y2: 78 },
// Árboles distantes (más pequeños y esquemáticos)
{ x1: 30, y1: 62, x2: 30, y2: 58 }, { x1: 28, y1: 58, x2: 32, y2: 58 }, // Árbol 1
{ x1: 70, y1: 62, x2: 70, y2: 58 }, { x1: 68, y1: 58, x2: 72, y2: 58 } // Árbol 2
],
"NUBE": [
// Forma de nube (más orgánica e irregular)
{ x1: 30, y1: 40, x2: 40, y2: 35 }, { x1: 40, y1: 35, x2: 48, y2: 32 }, { x1: 48, y1: 32, x2: 55, y2: 35 },
{ x1: 55, y1: 35, x2: 62, y2: 32 }, { x1: 62, y1: 32, x2: 70, y2: 35 }, { x1: 70, y1: 35, x2: 75, y2: 40 },
{ x1: 75, y1: 40, x2: 70, y2: 50 }, { x1: 70, y1: 50, x2: 65, y2: 55 }, { x1: 65, y1: 55, x2: 55, y2: 58 },
{ x1: 55, y1: 58, x2: 45, y2: 55 }, { x1: 45, y1: 55, x2: 35, y2: 50 }, { x1: 35, y1: 50, x2: 30, y2: 40 },
// Sombreado interno (líneas curvas paralelas para volumen)
{ x1: 35, y1: 45, x2: 45, y2: 43 }, { x1: 45, y1: 43, x2: 55, y2: 45 },
{ x1: 40, y1: 50, x2: 50, y2: 52 }, { x1: 50, y1: 52, x2: 60, y2: 50 }
],
"LUNA": [
// Luna creciente (con un corte más suave)
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Círculo exterior
{ type: "circle", x1: 42, y1: 50, radius: 18 }, // Círculo interior para la forma creciente
// Cráteres (varios tamaños y posiciones)
{ type: "circle", x1: 55, y1: 40, radius: 3 },
{ type: "circle", x1: 60, y1: 50, radius: 2.5 },
{ type: "circle", x1: 52, y1: 58, radius: 4 },
{ type: "circle", x1: 48, y1: 45, radius: 1.5 }
],
"ESTRELLA": [
// Estrella de 5 puntas (proporciones más precisas)
{ x1: 50, y1: 20, x2: 55, y2: 40 }, { x1: 55, y1: 40, x2: 75, y2: 45 },
{ x1: 75, y1: 45, x2: 60, y2: 60 }, { x1: 60, y1: 60, x2: 65, y2: 80 },
{ x1: 65, y1: 80, x2: 50, y2: 70 }, { x1: 50, y1: 70, x2: 35, y2: 80 },
{ x1: 35, y1: 80, x2: 40, y2: 60 }, { x1: 40, y1: 60, x2: 25, y2: 45 },
{ x1: 25, y1: 45, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 50, y2: 20 },
// Brillo (múltiples líneas finas saliendo del centro)
{ x1: 50, y1: 50, x2: 50, y2: 30 }, { x1: 50, y1: 50, x2: 65, y2: 40 },
{ x1: 50, y1: 50, x2: 60, y2: 65 }, { x1: 50, y1: 50, x2: 35, y2: 65 },
{ x1: 50, y1: 50, x2: 35, y2: 40 }
],
"AUTO": [
// Chasis inferior y parachoques
{ x1: 15, y1: 75, x2: 85, y2: 75 },
{ x1: 15, y1: 75, x2: 10, y2: 70 }, { x1: 85, y1: 75, x2: 90, y2: 70 },
// Carrocería (más aerodinámica)
{ x1: 10, y1: 70, x2: 10, y2: 60 }, // Frente
{ x1: 10, y1: 60, x2: 25, y2: 50 }, // Capó
{ x1: 25, y1: 50, x2: 35, y2: 40 }, // Parabrisas
{ x1: 35, y1: 40, x2: 65, y2: 40 }, // Techo
{ x1: 65, y1: 40, x2: 80, y2: 50 }, // Ventana trasera
{ x1: 80, y1: 50, x2: 90, y2: 60 }, // Maletero
{ x1: 90, y1: 60, x2: 90, y2: 70 }, // Trasera
// Ruedas (Con detalle interior/Hubcap)
{ type: "circle", x1: 25, y1: 75, radius: 8 },
{ type: "circle", x1: 25, y1: 75, radius: 3 }, // Hubcap
{ type: "circle", x1: 75, y1: 75, radius: 8 },
{ type: "circle", x1: 75, y1: 75, radius: 3 }, // Hubcap
// Ventanas y Puertas
{ x1: 35, y1: 40, x2: 35, y2: 55 }, { x1: 65, y1: 40, x2: 65, y2: 55 },
{ x1: 50, y1: 40, x2: 50, y2: 75 }, // División de puertas
// Detalles (Manija y Faro)
{ x1: 40, y1: 60, x2: 45, y2: 60 }, // Manija delantera
{ x1: 10, y1: 65, x2: 15, y2: 65 }, { x1: 15, y1: 65, x2: 15, y2: 70 } // Faro
],
"BICICLETA": [
// Ruedas (con rayos y válvula)
{ type: "circle", x1: 35, y1: 65, radius: 10 }, { type: "circle", x1: 35, y1: 65, radius: 1 }, // Válvula
{ x1: 35, y1: 55, x2: 35, y2: 75 }, { x1: 25, y1: 65, x2: 45, y2: 65 }, // Rayos
{ type: "circle", x1: 65, y1: 65, radius: 10 }, { type: "circle", x1: 65, y1: 65, radius: 1 }, // Válvula
{ x1: 65, y1: 55, x2: 65, y2: 75 }, { x1: 55, y1: 65, x2: 75, y2: 65 }, // Rayos
// Cuadro principal (más triángulos y estructura)
{ x1: 35, y1: 65, x2: 50, y2: 55 }, { x1: 50, y1: 55, x2: 65, y2: 65 },
{ x1: 50, y1: 55, x2: 40, y2: 45 }, { x1: 40, y1: 45, x2: 35, y2: 65 }, // Tubo diagonal
{ x1: 50, y1: 55, x2: 50, y2: 50 }, // Tubo de asiento
// Asiento (con forma de sillín)
{ x1: 48, y1: 48, x2: 52, y2: 48 }, { x1: 48, y1: 48, x2: 45, y2: 45 }, { x1: 52, y1: 48, x2: 55, y2: 45 }, // Sillín
// Manillar (con puños)
{ x1: 40, y1: 45, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 55, y2: 40 }, // Manillar
{ x1: 40, y1: 45, x2: 38, y2: 45 }, { x1: 55, y1: 40, x2: 57, y2: 40 }, // Puños
// Pedal y cadena (detalles)
{ x1: 50, y1: 65, x2: 52, y2: 68 }, { x1: 52, y1: 68, x2: 55, y2: 65 }, // Pedal
{ x1: 50, y1: 65, x2: 60, y2: 65 } // Cadena (línea)
],
"BARCO": [
// Casco (más curvado y con profundidad)
{ x1: 20, y1: 70, x2: 80, y2: 70 }, // Base
{ x1: 20, y1: 70, x2: 25, y2: 60 }, { x1: 80, y1: 70, x2: 75, y2: 60 }, // Lados del casco
{ x1: 25, y1: 60, x2: 75, y2: 60 },
{ x1: 28, y1: 62, x2: 72, y2: 62 }, // Borde interior del casco
// Mástil (más grueso y con uniones)
{ x1: 50, y1: 60, x2: 50, y2: 30 }, { x1: 48, y1: 60, x2: 48, y2: 30 }, // Grosor del mástil
{ x1: 48, y1: 30, x2: 52, y2: 30 }, // Parte superior del mástil
// Vela (más voluminosa y con líneas de viento)
{ x1: 50, y1: 30, x2: 65, y2: 40 }, { x1: 65, y1: 40, x2: 50, y2: 55 }, // Contorno de la vela
{ x1: 52, y1: 35, x2: 60, y2: 40 }, { x1: 52, y1: 40, x2: 60, y2: 45 }, // Líneas de viento en la vela
// Bandera (ondeando)
{ x1: 50, y1: 30, x2: 55, y2: 25 }, { x1: 55, y1: 25, x2: 50, y2: 20 }, { x1: 50, y1: 20, x2: 50, y2: 30 }
],
"PESCADO": [
// Cuerpo (más hidrodinámico y con escamas)
{ x1: 30, y1: 50, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 75, y2: 50 }, // Lomo
{ x1: 30, y1: 50, x2: 70, y2: 55 }, { x1: 70, y1: 55, x2: 75, y2: 50 }, // Vientre
{ x1: 75, y1: 50, x2: 80, y2: 48 }, // Transición a la cola
// Cola (más bifurcada y detallada)
{ x1: 80, y1: 48, x2: 85, y2: 40 }, { x1: 85, y1: 40, x2: 85, y2: 60 }, { x1: 85, y1: 60, x2: 80, y2: 52 },
// Aletas (con rayos)
{ x1: 40, y1: 45, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 40, y2: 50 }, // Aleta dorsal
{ x1: 40, y1: 55, x2: 45, y2: 60 }, { x1: 45, y1: 60, x2: 40, y2: 50 }, // Aleta ventral
// Ojo y Boca (con detalle)
{ type: "circle", x1: 35, y1: 48, radius: 1.5 }, { type: "circle", x1: 34.5, y1: 47.5, radius: 0.5 }, // Ojo con brillo
{ x1: 30, y1: 50, x2: 32, y2: 53 }, { x1: 32, y1: 53, x2: 30, y2: 52 } // Boca
],
"MANZANA": [
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Contorno
// Tallito (más realista)
{ x1: 48, y1: 30, x2: 52, y2: 30 }, { x1: 50, y1: 30, x2: 50, y2: 25 }, // Grosor del tallo
// Hoja (con vena)
{ x1: 50, y1: 25, x2: 55, y2: 28 }, { x1: 55, y1: 28, x2: 52, y2: 32 }, { x1: 52, y1: 32, x2: 50, y2: 25 },
{ x1: 52, y1: 27, x2: 54, y2: 30 } // Vena de la hoja
],
"PLATO": [
{ type: "circle", x1: 50, y1: 50, radius: 25 }, // Borde exterior
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Borde interior
// Detalles para dar profundidad y relieve
{ x1: 30, y1: 50, x2: 32, y2: 52 }, { x1: 70, y1: 50, x2: 68, y2: 52 }, // Curvas sutiles
{ x1: 50, y1: 25, x2: 52, y2: 27 }, { x1: 50, y1: 75, x2: 48, y2: 73 }
],
"CEREBRO": [
// Hemisferios con surcos más detallados y orgánicos
{ x1: 30, y1: 40, x2: 40, y2: 30 }, { x1: 40, y1: 30, x2: 60, y2: 30 },
{ x1: 60, y1: 30, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 65, y2: 50 },
{ x1: 65, y1: 50, x2: 55, y2: 55 }, { x1: 55, y1: 55, x2: 45, y2: 55 },
{ x1: 45, y1: 55, x2: 35, y2: 50 }, { x1: 35, y1: 50, x2: 30, y2: 40 }, // Contorno exterior
{ x1: 50, y1: 30, x2: 50, y2: 55 }, // Surco central (más profundo)
// Surcos secundarios
{ x1: 35, y1: 35, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 40, y2: 48 }, // Surco 1
{ x1: 55, y1: 35, x2: 65, y2: 40 }, { x1: 65, y1: 40, x2: 60, y2: 48 }, // Surco 2
{ x1: 40, y1: 32, x2: 35, y2: 38 }, { x1: 60, y1: 32, x2: 65, y2: 38 } // Surcos superiores
],
"CORAZON": [
// Corazón con curvas más suaves y volumen
{ x1: 50, y1: 75, x2: 30, y2: 50 }, { x1: 30, y1: 50, x2: 30, y2: 40 },
{ x1: 30, y1: 40, x2: 40, y2: 30 }, { x1: 40, y1: 30, x2: 50, y2: 35 }, // Curva superior izquierda
{ x1: 50, y1: 35, x2: 60, y2: 30 }, { x1: 60, y1: 30, x2: 70, y2: 40 },
{ x1: 70, y1: 40, x2: 70, y2: 50 }, { x1: 70, y1: 50, x2: 50, y2: 75 }, // Curva superior derecha y base
// Línea central (para dar forma al lóbulo)
{ x1: 50, y1: 35, x2: 50, y2: 65 }
],
"MANO": [
// Palma y muñeca (base más orgánica)
{ x1: 50, y1: 80, x2: 40, y2: 75 }, { x1: 40, y1: 75, x2: 40, y2: 60 },
{ x1: 50, y1: 80, x2: 70, y2: 75 }, { x1: 70, y1: 75, x2: 70, y2: 60 },
{ x1: 40, y1: 60, x2: 70, y2: 60 },
// Pulgar
{ x1: 40, y1: 65, x2: 35, y2: 55 }, { x1: 35, y1: 55, x2: 38, y2: 50 },
{ x1: 38, y1: 50, x2: 45, y2: 58 },
// Índice
{ x1: 45, y1: 60, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 48, y2: 35 },
{ x1: 48, y1: 35, x2: 52, y2: 35 }, { x1: 52, y1: 35, x2: 52, y2: 60 },
// Medio
{ x1: 52, y1: 60, x2: 52, y2: 32 }, { x1: 52, y1: 32, x2: 55, y2: 30 },
{ x1: 55, y1: 30, x2: 58, y2: 32 }, { x1: 58, y1: 32, x2: 58, y2: 60 },
// Anular
{ x1: 58, y1: 60, x2: 58, y2: 35 }, { x1: 58, y1: 35, x2: 61, y2: 33 },
{ x1: 61, y1: 33, x2: 64, y2: 35 }, { x1: 64, y1: 35, x2: 64, y2: 60 },
// Meñique
{ x1: 64, y1: 60, x2: 64, y2: 45 }, { x1: 64, y1: 45, x2: 67, y2: 43 },
{ x1: 67, y1: 43, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 70, y2: 60 },
// Detalles (Nudillos/Líneas de la palma)
{ x1: 48, y1: 55, x2: 55, y2: 55 }, { x1: 60, y1: 55, x2: 65, y2: 55 }
],
"OJO": [
// Forma almendrada del ojo (simulada con segmentos)
{ x1: 30, y1: 50, x2: 40, y2: 42 }, { x1: 40, y1: 42, x2: 60, y2: 42 },
{ x1: 60, y1: 42, x2: 70, y2: 50 }, // Párpado superior
{ x1: 30, y1: 50, x2: 40, y2: 58 }, { x1: 40, y1: 58, x2: 60, y2: 58 },
{ x1: 60, y1: 58, x2: 70, y2: 50 }, // Párpado inferior
// Iris y Pupila
{ type: "circle", x1: 50, y1: 50, radius: 10 }, // Iris
{ type: "circle", x1: 50, y1: 50, radius: 4 }, // Pupila
// Brillo (Highlight) - da vida al ojo
{ type: "circle", x1: 47, y1: 47, radius: 1 },
// Pestañas (varias líneas finas)
{ x1: 40, y1: 42, x2: 38, y2: 38 }, { x1: 45, y1: 42, x2: 45, y2: 37 },
{ x1: 50, y1: 42, x2: 52, y2: 37 }, { x1: 55, y1: 42, x2: 57, y2: 38 },
{ x1: 60, y1: 42, x2: 62, y2: 39 },
// Pliegue del párpado superior
{ x1: 35, y1: 40, x2: 50, y2: 38 }, { x1: 50, y1: 38, x2: 65, y2: 40 }
],
"BOCA": [
// Contorno de labios más detallado y curvo
{ x1: 35, y1: 50, x2: 45, y2: 45 }, { x1: 45, y1: 45, x2: 55, y2: 45 }, { x1: 55, y1: 45, x2: 65, y2: 50 }, // Labio superior
{ x1: 35, y1: 50, x2: 45, y2: 55 }, { x1: 45, y1: 55, x2: 55, y2: 55 }, { x1: 55, y1: 55, x2: 65, y2: 50 }, // Labio inferior
// Comisuras y pliegues
{ x1: 35, y1: 50, x2: 33, y2: 52 }, { x1: 65, y1: 50, x2: 67, y2: 52 },
{ x1: 40, y1: 50, x2: 40, y2: 52 }, { x1: 60, y1: 50, x2: 60, y2: 52 } // Líneas internas
],
"NARIZ": [
// Puente de la nariz (con detalle)
{ x1: 50, y1: 40, x2: 45, y2: 50 }, { x1: 50, y1: 40, x2: 55, y2: 50 },
// Base de la nariz y fosas nasales (más detalladas)
{ x1: 45, y1: 50, x2: 45, y2: 55 }, { x1: 55, y1: 50, x2: 55, y2: 55 },
{ x1: 45, y1: 55, x2: 55, y2: 55 },
{ type: "circle", x1: 47, y1: 57, radius: 1 }, { type: "circle", x1: 53, y1: 57, radius: 1 }, // Fosas nasales
// Sombreado bajo la nariz
{ x1: 46, y1: 58, x2: 54, y2: 58 }
],
"OREJA": [
// Contorno exterior (más orgánico y con lóbulo)
{ x1: 50, y1: 40, x2: 40, y2: 45 }, { x1: 40, y1: 45, x2: 38, y2: 55 },
{ x1: 38, y1: 55, x2: 45, y2: 65 }, { x1: 45, y1: 65, x2: 50, y2: 68 }, // Lóbulo
{ x1: 50, y1: 68, x2: 55, y2: 65 }, { x1: 55, y1: 65, x2: 62, y2: 55 },
{ x1: 62, y1: 55, x2: 60, y2: 45 }, { x1: 60, y1: 45, x2: 50, y1: 40 },
// Detalles internos (conchas de la oreja)
{ x1: 45, y1: 48, x2: 42, y2: 52 }, { x1: 42, y1: 52, x2: 45, y2: 58 },
{ x1: 55, y1: 48, x2: 58, y2: 52 }, { x1: 58, y1: 52, x2: 55, y2: 58 },
{ x1: 50, y1: 50, x2: 50, y2: 55 } // Conector
],
"DIENTE": [
// Cuerpo del diente (más natural y redondeado)
{ x1: 45, y1: 30, x2: 55, y2: 30 }, // Parte superior
{ x1: 45, y1: 30, x2: 43, y2: 50 }, { x1: 55, y1: 30, x2: 57, y2: 50 }, // Curvas laterales
{ x1: 43, y1: 50, x2: 45, y2: 60 }, { x1: 57, y1: 50, x2: 55, y2: 60 }, // Transición a la encía
{ x1: 45, y1: 60, x2: 55, y2: 60 }, // Borde inferior (encía)
// Raíces (más definidas y con separación)
{ x1: 46, y1: 60, x2: 44, y2: 68 }, { x1: 44, y1: 68, x2: 46, y2: 70 },
{ x1: 54, y1: 60, x2: 56, y2: 68 }, { x1: 56, y1: 68, x2: 54, y2: 70 },
// Encía (curvada)
{ x1: 40, y1: 65, x2: 50, y2: 68 }, { x1: 50, y1: 68, x2: 60, y2: 65 }
],
"PAN": [
// Barra de pan (más redondeada y con volumen)
{ x1: 20, y1: 60, x2: 80, y2: 60 }, // Base
{ x1: 20, y1: 60, x2: 22, y2: 50 }, { x1: 80, y1: 60, x2: 78, y2: 50 }, // Laterales
{ x1: 22, y1: 50, x2: 78, y2: 50 }, // Parte superior
{ x1: 20, y1: 60, x2: 25, y2: 70 }, { x1: 80, y1: 60, x2: 75, y2: 70 }, // Sombras de base
{ x1: 25, y1: 70, x2: 75, y2: 70 },
// Cortes (más profundos y definidos)
{ x1: 30, y1: 50, x2: 35, y2: 60 }, { x1: 35, y1: 60, x2: 40, y2: 50 },
{ x1: 45, y1: 50, x2: 50, y2: 60 }, { x1: 50, y1: 60, x2: 55, y2: 50 },
{ x1: 60, y1: 50, x2: 65, y2: 60 }, { x1: 65, y1: 60, x2: 70, y2: 50 }
],
"QUESO": [
// Trozo de queso triangular (con grosor y textura)
{ x1: 30, y1: 70, x2: 70, y2: 70 }, // Base
{ x1: 30, y1: 70, x2: 50, y2: 30 }, // Lado izquierdo
{ x1: 70, y1: 70, x2: 50, y2: 30 }, // Lado derecho
{ x1: 35, y1: 68, x2: 50, y2: 35 }, { x1: 65, y1: 68, x2: 50, y2: 35 }, // Grosor y cara superior
// Agujeros (más irregulares)
{ type: "circle", x1: 45, y1: 55, radius: 5 },
{ type: "circle", x1: 58, y1: 60, radius: 4 },
{ type: "circle", x1: 40, y1: 65, radius: 3 },
{ type: "circle", x1: 50, y1: 45, radius: 2 } // Agujero pequeño
],
"HUEVO": [
// Contorno de huevo (más ovalado en la parte superior)
{ type: "circle", x1: 50, y1: 55, radius: 20 }, // Base redonda
{ x1: 50, y1: 35, x2: 45, y2: 30 }, { x1: 50, y1: 35, x2: 55, y2: 30 }, // Parte superior ovalada
{ x1: 45, y1: 30, x2: 42, y2: 35 }, { x1: 55, y1: 30, x2: 58, y2: 35 },
{ x1: 42, y1: 35, x2: 30, y2: 55 }, { x1: 58, y1: 35, x2: 70, y2: 55 },
// Yema (con brillo)
{ type: "circle", x1: 50, y1: 50, radius: 8 },
{ type: "circle", x1: 48, y1: 48, radius: 1 } // Brillo en la yema
],
"CARNE": [
// Trozo de carne (forma más irregular y orgánica)
{ x1: 30, y1: 50, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 75, y2: 60 },
{ x1: 75, y1: 60, x2: 60, y2: 70 }, { x1: 60, y1: 70, x2: 35, y2: 65 },
{ x1: 35, y1: 65, x2: 30, y2: 50 },
// Hueso (más detallado)
{ x1: 50, y1: 50, x2: 50, y2: 35 }, { x1: 45, y1: 35, x2: 55, y2: 35 }, // Hueso principal
{ type: "circle", x1: 45, y1: 35, radius: 3 }, { type: "circle", x1: 55, y1: 35, radius: 3 }, // Nudillos del hueso
// Líneas de grasa/textura
{ x1: 40, y1: 55, x2: 50, y2: 58 }, { x1: 60, y1: 50, x2: 65, y2: 55 }
],
"POLLO": [
// Pollo asado (más volumen y piernas definidas)
{ x1: 30, y1: 60, x2: 70, y2: 60 }, { x1: 70, y1: 60, x2: 70, y2: 50 },
{ x1: 70, y1: 50, x2: 60, y2: 45 }, { x1: 60, y1: 45, x2: 40, y2: 45 },
{ x1: 40, y1: 45, x2: 30, y2: 50 }, { x1: 30, y1: 50, x2: 30, y2: 60 }, // Cuerpo
// Piernas (muslos y contramuslos)
{ x1: 35, y1: 58, x2: 25, y2: 55 }, { x1: 25, y1: 55, x2: 20, y2: 60 }, // Pierna izq
{ x1: 65, y1: 58, x2: 75, y2: 55 }, { x1: 75, y1: 55, x2: 80, y2: 60 }, // Pierna der
// Textura de piel/dorado
{ x1: 40, y1: 50, x2: 50, y2: 48 }, { x1: 50, y1: 48, x2: 60, y2: 50 } // Líneas para sombreado
],
"ARROZ": [
// Bol (con grosor y apertura)
{ type: "circle", x1: 50, y1: 70, radius: 20 }, // Base del bol
{ x1: 30, y1: 70, x2: 35, y2: 60 }, { x1: 70, y1: 70, x2: 65, y2: 60 }, // Lados inclinados
{ x1: 35, y1: 60, x2: 65, y2: 60 }, // Borde superior del bol
// Arroz (montón de puntos o pequeñas líneas)
{ type: "circle", x1: 50, y1: 55, radius: 15 }, // Simular el montón
{ x1: 45, y1: 50, x2: 46, y2: 50 }, { x1: 55, y1: 52, x2: 56, y2: 52 }, // Granos de arroz (puntos)
{ x1: 48, y1: 58, x2: 49, y2: 58 }, { x1: 52, y1: 47, x2: 53, y2: 47 },
// Palillos (con detalle y en ángulo)
{ x1: 40, y1: 55, x2: 40, y2: 35 }, { x1: 40, y1: 35, x2: 42, y2: 30 }, // Palillo 1
{ x1: 42, y1: 30, x2: 42, y2: 50 },
{ x1: 45, y1: 55, x2: 45, y2: 35 }, { x1: 45, y1: 35, x2: 47, y2: 30 }, // Palillo 2
{ x1: 47, y1: 30, x2: 47, y2: 50 }
],
"PASTA": [
// Plato (con borde y sombreado para profundidad)
{ type: "circle", x1: 50, y1: 65, radius: 20 }, // Borde exterior
{ type: "circle", x1: 50, y1: 65, radius: 18 }, // Borde interior
{ x1: 32, y1: 65, x2: 35, y2: 68 }, { x1: 68, y1: 65, x2: 65, y2: 68 }, // Sombreado del plato
// Pasta (espagueti en un montón)
{ type: "circle", x1: 50, y1: 50, radius: 10 }, // Base del montón
{ x1: 40, y1: 50, x2: 45, y2: 45 }, { x1: 45, y1: 45, x2: 50, y2: 50 }, // Hilos de pasta
{ x1: 50, y1: 50, x2: 55, y2: 45 }, { x1: 55, y1: 45, x2: 60, y2: 50 },
{ x1: 45, y1: 55, x2: 50, y2: 50 }, { x1: 55, y1: 55, x2: 50, y2: 50 },
// Tenedor (con dientes)
{ x1: 48, y1: 50, x2: 48, y2: 40 }, { x1: 52, y1: 50, x2: 52, y2: 40 }, // Mangos
{ x1: 47, y1: 40, x2: 47, y2: 35 }, { x1: 49, y1: 40, x2: 49, y2: 35 }, // Dientes
{ x1: 51, y1: 40, x2: 51, y2: 35 }, { x1: 53, y1: 40, x2: 53, y2: 35 }
],
"FRUTA": [
// Canasta (con textura de mimbre y asa)
{ x1: 30, y1: 70, x2: 70, y2: 70 }, // Base
{ x1: 30, y1: 70, x2: 35, y2: 60 }, { x1: 70, y1: 70, x2: 65, y2: 60 }, // Lados inclinados
{ x1: 35, y1: 60, x2: 65, y2: 60 }, // Borde superior de la canasta
{ x1: 37, y1: 62, x2: 63, y2: 62 }, { x1: 39, y1: 64, x2: 61, y2: 64 }, // Textura de mimbre
{ x1: 45, y1: 60, x2: 45, y2: 50 }, { x1: 55, y1: 60, x2: 55, y2: 50 }, // Asa
{ x1: 45, y1: 50, x2: 55, y2: 50 },
// Frutas (varias formas y tamaños)
{ type: "circle", x1: 40, y1: 55, radius: 5 }, // Manzana
{ type: "circle", x1: 60, y1: 55, radius: 4 }, // Naranja
{ x1: 50, y1: 48, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 55, y2: 40 }, { x1: 55, y1: 40, x2: 50, y2: 48 } // Plátano
],
"UVA": [
// Racimo de uvas (más densas y con superposición)
{ type: "circle", x1: 50, y1: 40, radius: 5 }, // Uva superior
{ type: "circle", x1: 45, y1: 48, radius: 5 }, { type: "circle", x1: 55, y1: 48, radius: 5 },
{ type: "circle", x1: 40, y1: 56, radius: 5 }, { type: "circle", x1: 50, y1: 56, radius: 5 },
{ type: "circle", x1: 60, y1: 56, radius: 5 },
{ type: "circle", x1: 45, y1: 64, radius: 5 }, { type: "circle", x1: 55, y1: 64, radius: 5 },
{ type: "circle", x1: 50, y1: 72, radius: 5 }, // Uva inferior
// Tallo (más orgánico y con rama pequeña)
{ x1: 50, y1: 35, x2: 50, y2: 25 },
{ x1: 50, y1: 30, x2: 55, y2: 28 }, { x1: 55, y1: 28, x2: 52, y2: 32 } // Rama
],
"PIÑA": [
// Cuerpo ovalado (con textura de rombos)
{ x1: 40, y1: 40, x2: 60, y2: 40 }, { x1: 40, y1: 40, x2: 35, y2: 60 },
{ x1: 60, y1: 40, x2: 65, y2: 60 }, { x1: 35, y1: 60, x2: 65, y2: 60 },
// Textura de rombos
{ x1: 40, y1: 45, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 50, y2: 45 }, // Fila 1
{ x1: 50, y1: 45, x2: 55, y2: 40 }, { x1: 55, y1: 40, x2: 60, y2: 45 },
{ x1: 37, y1: 50, x2: 42, y2: 45 }, { x1: 42, y1: 45, x2: 47, y2: 50 }, // Fila 2
{ x1: 47, y1: 50, x2: 52, y2: 45 }, { x1: 52, y1: 45, x2: 57, y2: 50 },
{ x1: 57, y1: 50, x2: 62, y2: 45 },
// Hojas superiores (más puntiagudas y variadas)
{ x1: 45, y1: 38, x2: 40, y2: 25 }, { x1: 50, y1: 38, x2: 45, y2: 22 },
{ x1: 55, y1: 38, x2: 50, y2: 25 }, { x1: 60, y1: 38, x2: 55, y2: 22 }
],
"KIWI": [
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Contorno exterior
{ type: "circle", x1: 50, y1: 50, radius: 5 }, // Centro
// Líneas internas (más para simular las semillas y la pulpa)
{ x1: 50, y1: 30, x2: 50, y2: 70 }, { x1: 30, y1: 50, x2: 70, y2: 50 },
{ x1: 35, y1: 35, x2: 65, y2: 65 }, { x1: 35, y1: 65, x2: 65, y2: 35 },
{ x1: 40, y1: 30, x2: 40, y2: 70 }, { x1: 60, y1: 30, x2: 60, y2: 70 }, // Líneas de la pulpa
{ x1: 30, y1: 40, x2: 70, y2: 40 }, { x1: 30, y1: 60, x2: 70, y2: 60 },
// Semillas (puntos pequeños alrededor del centro)
{ type: "circle", x1: 42, y1: 48, radius: 0.5 }, { type: "circle", x1: 42, y1: 52, radius: 0.5 },
{ type: "circle", x1: 48, y1: 42, radius: 0.5 }, { type: "circle", x1: 52, y1: 42, radius: 0.5 },
{ type: "circle", x1: 58, y1: 48, radius: 0.5 }, { type: "circle", x1: 58, y1: 52, radius: 0.5 },
{ type: "circle", x1: 48, y1: 58, radius: 0.5 }, { type: "circle", x1: 52, y1: 58, radius: 0.5 }
],
"CHOCOLATE": [
// Barra de chocolate (con grosor y divisiones en 3D)
{ x1: 30, y1: 45, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 70, y2: 65 },
{ x1: 70, y1: 65, x2: 30, y2: 65 }, { x1: 30, y1: 65, x2: 30, y2: 45 }, // Cara frontal
{ x1: 30, y1: 45, x2: 25, y2: 40 }, { x1: 70, y1: 45, x2: 65, y2: 40 }, // Borde superior en perspectiva
{ x1: 25, y1: 40, x2: 65, y2: 40 },
{ x1: 25, y1: 40, x2: 25, y2: 60 }, { x1: 65, y1: 40, x2: 65, y2: 60 }, // Lados en perspectiva
{ x1: 25, y1: 60, x2: 30, y2: 65 }, { x1: 65, y1: 60, x2: 70, y2: 65 },
// Divisiones (con líneas que simulan los cuadrados)
{ x1: 35, y1: 45, x2: 35, y2: 65 }, { x1: 45, y1: 45, x2: 45, y2: 65 },
{ x1: 55, y1: 45, x2: 55, y2: 65 }, { x1: 65, y1: 45, x2: 65, y2: 65 },
{ x1: 30, y1: 55, x2: 70, y2: 55 }
],
"HELADO": [
// Cono (con textura de waffle)
{ x1: 40, y1: 70, x2: 50, y2: 30 }, { x1: 60, y1: 70, x2: 50, y2: 30 },
{ x1: 40, y1: 70, x2: 60, y2: 70 },
{ x1: 45, y1: 65, x2: 55, y2: 65 }, { x1: 42, y1: 60, x2: 58, y2: 60 }, // Líneas horizontales del waffle
{ x1: 47, y1: 70, x2: 50, y2: 30 }, { x1: 53, y1: 70, x2: 50, y2: 30 }, // Líneas verticales del waffle
// Bola de helado (con derretimiento)
{ type: "circle", x1: 50, y1: 30, radius: 15 },
{ x1: 40, y1: 35, x2: 42, y2: 40 }, { x1: 42, y1: 40, x2: 40, y2: 42 }, // Gotas simuladas
{ x1: 60, y1: 35, x2: 58, y2: 40 }, { x1: 58, y1: 40, x2: 60, y2: 42 }
],
"GALLETA": [
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Contorno (más irregular para un aspecto casero)
{ x1: 35, y1: 45, x2: 38, y2: 42 }, { x1: 42, y1: 38, x2: 45, y2: 35 },
{ x1: 65, y1: 45, x2: 62, y2: 42 }, { x1: 58, y1: 38, x2: 55, y2: 35 },
// Chispas de chocolate (con un punto central para dar volumen)
{ type: "circle", x1: 40, y1: 45, radius: 2 }, { type: "circle", x1: 40, y1: 45, radius: 0.5 },
{ type: "circle", x1: 60, y1: 45, radius: 2 }, { type: "circle", x1: 60, y1: 45, radius: 0.5 },
{ type: "circle", x1: 50, y1: 58, radius: 2 }, { type: "circle", x1: 50, y1: 58, radius: 0.5 },
{ type: "circle", x1: 45, y1: 52, radius: 1.5 }, { type: "circle", x1: 55, y1: 52, radius: 1.5 }
],
"CARAMELO": [
// Cuerpo central (más grueso y con reflejo)
{ x1: 30, y1: 50, x2: 70, y2: 50 },
{ x1: 30, y1: 52, x2: 70, y2: 52 }, // Grosor
{ x1: 35, y1: 50, x2: 65, y2: 50 }, // Línea de reflejo
// Envoltorio (más plisado y con puntas definidas)
{ x1: 25, y1: 45, x2: 30, y2: 50 }, { x1: 30, y1: 50, x2: 25, y2: 55 },
{ x1: 20, y1: 48, x2: 25, y2: 45 }, { x1: 20, y1: 52, x2: 25, y2: 55 }, // Pliegues izq
{ x1: 70, y1: 50, x2: 75, y2: 45 }, { x1: 75, y1: 45, x2: 70, y2: 50 },
{ x1: 75, y1: 50, x2: 75, y2: 55 }, { x1: 70, y1: 50, x2: 80, y2: 48 }, // Pliegues der
{ x1: 80, y1: 48, x2: 75, y2: 45 }, { x1: 80, y1: 52, x2: 75, y2: 55 }
],
"TARTA": [
// Rebanada de tarta (con capas y textura de crema)
{ x1: 50, y1: 30, x2: 30, y2: 70 }, { x1: 30, y1: 70, x2: 70, y2: 70 },
{ x1: 70, y1: 70, x2: 50, y2: 30 }, // Triángulo principal
// Capas internas
{ x1: 40, y1: 45, x2: 60, y2: 45 }, // Capa 1
{ x1: 35, y1: 55, x2: 65, y2: 55 }, // Capa 2
// Textura de crema/glaseado
{ x1: 50, y1: 30, x2: 50, y2: 28 }, { x1: 48, y1: 28, x2: 52, y2: 28 }, // Topping
{ x1: 30, y1: 70, x2: 32, y2: 68 }, { x1: 70, y1: 70, x2: 68, y2: 68 } // Borde inferior
],
"PIZZA": [
// Rebanada de pizza (con borde y ingredientes)
{ x1: 50, y1: 30, x2: 30, y2: 70 }, { x1: 30, y1: 70, x2: 70, y2: 70 },
{ x1: 70, y1: 70, x2: 50, y2: 30 }, // Triángulo de la rebanada
// Borde de la corteza
{ x1: 50, y1: 30, x2: 40, y2: 35 }, { x1: 40, y1: 35, x2: 32, y2: 65 },
{ x1: 32, y1: 65, x2: 30, y2: 70 }, // Borde izquierdo
{ x1: 50, y1: 30, x2: 60, y2: 35 }, { x1: 60, y1: 35, x2: 68, y2: 65 },
{ x1: 68, y1: 65, x2: 70, y2: 70 }, // Borde derecho
// Pepperoni (con detalle interior)
{ type: "circle", x1: 40, y1: 50, radius: 3 }, { type: "circle", x1: 40, y1: 50, radius: 1 },
{ type: "circle", x1: 60, y1: 55, radius: 3 }, { type: "circle", x1: 60, y1: 55, radius: 1 },
{ type: "circle", x1: 50, y1: 40, radius: 2.5 } // Otro pepperoni
],
"HAMBURGUESA": [
// Pan superior (curvo y con semillas)
{ x1: 30, y1: 40, x2: 70, y2: 40 }, { x1: 30, y1: 40, x2: 25, y2: 45 },
{ x1: 70, y1: 40, x2: 75, y2: 45 }, { x1: 25, y1: 45, x2: 75, y2: 45 }, // Curva superior
{ type: "circle", x1: 40, y1: 42, radius: 0.5 }, { type: "circle", x1: 50, y1: 42, radius: 0.5 }, { type: "circle", x1: 60, y1: 42, radius: 0.5 }, // Semillas
// Lechuga/Tomate (línea ondulada)
{ x1: 25, y1: 45, x2: 30, y2: 48 }, { x1: 30, y1: 48, x2: 40, y2: 46 }, { x1: 40, y1: 46, x2: 50, y2: 48 },
{ x1: 50, y1: 48, x2: 60, y2: 46 }, { x1: 60, y1: 46, x2: 70, y2: 48 }, { x1: 70, y1: 48, x2: 75, y2: 45 },
// Carne (con textura)
{ x1: 28, y1: 50, x2: 72, y2: 50 }, { x1: 28, y1: 50, x2: 28, y2: 55 },
{ x1: 72, y1: 50, x2: 72, y2: 55 }, { x1: 28, y1: 55, x2: 72, y2: 55 },
{ x1: 35, y1: 52, x2: 38, y2: 52 }, { x1: 45, y1: 53, x2: 48, y2: 53 }, // Textura
// Pan inferior (curvo)
{ x1: 25, y1: 60, x2: 75, y2: 60 }, { x1: 25, y1: 60, x2: 30, y2: 55 },
{ x1: 75, y1: 60, x2: 70, y2: 55 }
],
"PERRITO": [
// Salchicha (con textura y ligeramente curva)
{ x1: 30, y1: 50, x2: 70, y2: 50 }, { x1: 30, y1: 50, x2: 30, y2: 53 },
{ x1: 70, y1: 50, x2: 70, y2: 53 }, { x1: 30, y1: 53, x2: 70, y2: 53 },
{ x1: 40, y1: 51, x2: 40, y2: 52 }, { x1: 50, y1: 51, x2: 50, y2: 52 }, { x1: 60, y1: 51, x2: 60, y2: 52 }, // Rayas de la salchicha
// Bollo (más abierto y con textura)
{ x1: 25, y1: 55, x2: 75, y2: 55 }, // Borde superior del bollo
{ x1: 25, y1: 55, x2: 20, y2: 65 }, { x1: 75, y1: 55, x2: 80, y2: 65 }, // Lados del bollo
{ x1: 20, y1: 65, x2: 80, y2: 65 }, // Base del bollo
{ x1: 30, y1: 60, x2: 35, y2: 62 }, { x1: 65, y1: 60, x2: 60, y2: 62 } // Sombreado interno del bollo
],
"MAPACHE": [
// Cabeza (más definida)
{ type: "circle", x1: 50, y1: 50, radius: 20 },
// Orejas (con forma de gota y pelaje)
{ x1: 40, y1: 35, x2: 35, y2: 40 }, { x1: 35, y1: 40, x2: 40, y2: 45 }, { x1: 40, y1: 45, x2: 45, y2: 40 },
{ x1: 60, y1: 35, x2: 65, y2: 40 }, { x1: 65, y1: 40, x2: 60, y2: 45 }, { x1: 60, y1: 45, x2: 55, y2: 40 },
// Manchas de ojos (más oscuras y con forma)
{ x1: 38, y1: 45, x2: 42, y2: 45 }, { x1: 38, y1: 50, x2: 42, y2: 50 }, // Mancha izquierda
{ x1: 58, y1: 45, x2: 62, y2: 45 }, { x1: 58, y1: 50, x2: 62, y2: 50 }, // Mancha derecha
// Nariz (triángulo invertido)
{ x1: 50, y1: 55, x2: 48, y2: 58 }, { x1: 48, y1: 58, x2: 52, y2: 58 }, { x1: 52, y1: 58, x2: 50, y2: 55 },
// Hocico (forma de "U" invertida)
{ x1: 45, y1: 55, x2: 55, y2: 55 },
// Ojos y bigotes (más detallados)
{ type: "circle", x1: 40, y1: 47, radius: 1 }, { type: "circle", x1: 60, y1: 47, radius: 1 },
{ x1: 45, y1: 55, x2: 35, y2: 53 }, { x1: 45, y1: 56, x2: 35, y2: 56 },
{ x1: 55, y1: 55, x2: 65, y2: 53 }, { x1: 55, y1: 56, x2: 65, y2: 56 }
],
"ZORRO": [
// Cabeza (forma más triangular)
{ x1: 50, y1: 65, x2: 35, y2: 50 }, { x1: 35, y1: 50, x2: 50, y2: 35 },
{ x1: 50, y1: 35, x2: 65, y2: 50 }, { x1: 65, y1: 50, x2: 50, y2: 65 },
// Hocico (más puntiagudo)
{ x1: 50, y1: 60, x2: 45, y2: 55 }, { x1: 45, y1: 55, x2: 50, y2: 50 },
{ x1: 50, y1: 50, x2: 55, y2: 55 }, { x1: 55, y1: 55, x2: 50, y2: 60 },
// Nariz
{ x1: 50, y1: 52, x2: 50, y2: 54 },
// Orejas puntiagudas (con detalle interior)
{ x1: 45, y1: 35, x2: 40, y2: 25 }, { x1: 40, y1: 25, x2: 48, y2: 30 },
{ x1: 55, y1: 35, x2: 60, y2: 25 }, { x1: 60, y1: 25, x2: 52, y2: 30 },
{ x1: 44, y1: 30, x2: 46, y2: 28 }, { x1: 56, y1: 30, x2: 54, y2: 28 }, // Detalle interior
// Ojos y bigotes
{ type: "circle", x1: 42, y1: 45, radius: 1 }, { type: "circle", x1: 58, y1: 45, radius: 1 },
{ x1: 45, y1: 55, x2: 35, y2: 53 }, { x1: 45, y1: 56, x2: 35, y2: 56 },
{ x1: 55, y1: 55, x2: 65, y2: 53 }, { x1: 55, y1: 56, x2: 65, y2: 56 }
],
"LOBO": [
// Cara (más angulosa y con hocico alargado)
{ x1: 50, y1: 65, x2: 35, y2: 50 }, { x1: 35, y1: 50, x2: 50, y2: 35 },
{ x1: 50, y1: 35, x2: 65, y2: 50 }, { x1: 65, y1: 50, x2: 50, y2: 65 },
// Hocico alargado y aullando
{ x1: 50, y1: 60, x2: 45, y2: 50 }, { x1: 45, y1: 50, x2: 50, y2: 45 },
{ x1: 50, y1: 45, x2: 55, y2: 50 }, { x1: 55, y1: 50, x2: 50, y2: 60 },
{ x1: 50, y1: 60, x2: 50, y2: 70 }, // Boca abierta
{ x1: 48, y1: 68, x2: 52, y2: 68 }, // Dientes (simulados)
// Orejas (más grandes y puntiagudas)
{ x1: 45, y1: 35, x2: 40, y2: 25 }, { x1: 40, y1: 25, x2: 48, y2: 30 },
{ x1: 55, y1: 35, x2: 60, y2: 25 }, { x1: 60, y1: 25, x2: 52, y2: 30 },
// Ojos
{ type: "circle", x1: 42, y1: 45, radius: 1.5 }, { type: "circle", x1: 58, y1: 45, radius: 1.5 }
],
"OSO": [
// Cabeza (más definida y peluda)
{ type: "circle", x1: 50, y1: 50, radius: 20 },
{ x1: 30, y1: 45, x2: 32, y2: 42 }, { x1: 32, y1: 42, x2: 35, y2: 45 }, // Pelaje en el contorno
// Orejas redondas (con detalle interior)
{ type: "circle", x1: 40, y1: 35, radius: 8 }, { type: "circle", x1: 40, y1: 35, radius: 4 }, // Oreja izq
{ type: "circle", x1: 60, y1: 35, radius: 8 }, { type: "circle", x1: 60, y1: 35, radius: 4 }, // Oreja der
// Hocico (más prominente)
{ type: "circle", x1: 50, y1: 58, radius: 7 },
{ type: "circle", x1: 50, y1: 55, radius: 2 }, // Nariz
// Ojos
{ type: "circle", x1: 45, y1: 48, radius: 1.5 }, { type: "circle", x1: 55, y1: 48, radius: 1.5 }
],
"TIGRE": [
// Cabeza (más cuadrada y fuerte)
{ type: "circle", x1: 50, y1: 50, radius: 25 },
// Hocico (más amplio)
{ x1: 40, y1: 55, x2: 60, y2: 55 }, { x1: 40, y1: 55, x2: 35, y2: 60 },
{ x1: 60, y1: 55, x2: 65, y2: 60 }, { x1: 35, y1: 60, x2: 65, y2: 60 },
// Ojos y Nariz
{ type: "circle", x1: 42, y1: 45, radius: 2 }, { type: "circle", x1: 58, y1: 45, radius: 2 },
{ x1: 50, y1: 58, x2: 50, y2: 60 },
// Rayas (más variadas y angulares)
{ x1: 35, y1: 35, x2: 40, y2: 30 }, { x1: 40, y1: 30, x2: 45, y2: 35 }, // Raya superior izq
{ x1: 65, y1: 35, x2: 60, y2: 30 }, { x1: 60, y1: 30, x2: 55, y2: 35 }, // Raya superior der
{ x1: 30, y1: 50, x2: 35, y2: 50 }, { x1: 30, y1: 52, x2: 35, y2: 52 }, // Rayas laterales
{ x1: 70, y1: 50, x2: 65, y2: 50 }, { x1: 70, y1: 52, x2: 65, y2: 52 }
],
"LEON": [
// Cabeza (más grande para la melena)
{ type: "circle", x1: 50, y1: 50, radius: 25 },
// Melena (líneas radiales para simular pelaje)
{ x1: 50, y1: 25, x2: 40, y2: 20 }, { x1: 50, y1: 25, x2: 60, y2: 20 },
{ x1: 50, y1: 75, x2: 40, y2: 80 }, { x1: 50, y1: 75, x2: 60, y2: 80 },
{ x1: 25, y1: 50, x2: 20, y2: 40 }, { x1: 25, y1: 50, x2: 20, y2: 60 },
{ x1: 75, y1: 50, x2: 80, y2: 40 }, { x1: 75, y1: 50, x2: 80, y2: 60 },
// Cara interna (hocico y boca)
{ type: "circle", x1: 50, y1: 50, radius: 15 },
{ x1: 50, y1: 55, x2: 45, y2: 58 }, { x1: 45, y1: 58, x2: 55, y2: 58 }, { x1: 55, y1: 58, x2: 50, y2: 55 }, // Boca
{ type: "circle", x1: 50, y1: 52, radius: 1.5 }, // Nariz
// Ojos
{ type: "circle", x1: 45, y1: 45, radius: 1.5 }, { type: "circle", x1: 55, y1: 45, radius: 1.5 }
],
"ELEFANTE": [
// Cabeza (con más forma)
{ type: "circle", x1: 50, y1: 50, radius: 20 },
// Trompa (más curva y con arrugas)
{ x1: 50, y1: 60, x2: 55, y2: 70 }, { x1: 55, y1: 70, x2: 50, y2: 75 },
{ x1: 50, y1: 75, x2: 45, y2: 70 }, { x1: 45, y1: 70, x2: 50, y2: 60 },
{ x1: 50, y1: 65, x2: 50, y2: 68 }, { x1: 48, y1: 72, x2: 52, y2: 72 }, // Arrugas de la trompa
// Orejas (grandes y colgantes)
{ x1: 35, y1: 40, x2: 25, y2: 55 }, { x1: 25, y2: 55, x2: 35, y2: 65 }, { x1: 35, y1: 65, x2: 40, y2: 55 }, { x1: 40, y2: 55, x2: 35, y2: 40 }, // Oreja 1
{ x1: 65, y1: 40, x2: 75, y2: 55 }, { x1: 75, y2: 55, x2: 65, y2: 65 }, { x1: 65, y1: 65, x2: 60, y2: 55 }, { x1: 60, y2: 55, x2: 65, y2: 40 }, // Oreja 2
// Ojo
{ type: "circle", x1: 45, y1: 45, radius: 1.5 }
],
"JIRAFA": [
// Cuello largo (con grosor y manchas)
{ x1: 48, y1: 80, x2: 48, y2: 30 }, { x1: 52, y1: 80, x2: 52, y2: 30 }, // Grosor del cuello
{ x1: 48, y1: 70, x2: 52, y2: 70 }, { x1: 48, y1: 55, x2: 52, y2: 55 }, // Líneas de las manchas
// Cabeza (más definida y con osiconos)
{ type: "circle", x1: 50, y1: 25, radius: 8 },
{ x1: 48, y1: 20, x2: 48, y2: 15 }, { x1: 52, y1: 20, x2: 52, y2: 15 }, // Osiconos
{ type: "circle", x1: 50, y1: 15, radius: 1 },
// Hocico y ojos
{ x1: 45, y1: 30, x2: 55, y2: 30 }, // Hocico
{ type: "circle", x1: 47, y1: 27, radius: 1 }, { type: "circle", x1: 53, y1: 27, radius: 1 } // Ojos
],
"MONO": [
// Cuerpo (más curvado)
{ type: "circle", x1: 50, y1: 55, radius: 20 },
// Cabeza (más pequeña y con cara)
{ type: "circle", x1: 50, y1: 30, radius: 10 },
{ type: "circle", x1: 50, y1: 32, radius: 5 }, // Cara interna
// Orejas (redondas y prominentes)
{ type: "circle", x1: 40, y1: 25, radius: 5 }, { type: "circle", x1: 60, y1: 25, radius: 5 },
// Cola (más larga y enroscada)
{ x1: 60, y1: 65, x2: 70, y2: 60 }, { x1: 70, y2: 60, x2: 75, y2: 70 }, { x1: 75, y2: 70, x2: 70, y2: 75 },
// Brazos y piernas (simulados con líneas)
{ x1: 40, y1: 50, x2: 35, y2: 45 }, { x1: 60, y1: 50, x2: 65, y2: 45 },
{ x1: 45, y1: 70, x2: 40, y2: 75 }, { x1: 55, y1: 70, x2: 60, y2: 75 }
],
"KOALA": [
// Cuerpo (más peludo)
{ type: "circle", x1: 50, y1: 50, radius: 25 },
{ x1: 30, y1: 45, x2: 32, y2: 42 }, { x1: 32, y1: 42, x2: 35, y2: 45 }, // Pelaje en el contorno
// Orejas grandes y peludas
{ type: "circle", x1: 40, y1: 35, radius: 10 }, { type: "circle", x1: 40, y1: 35, radius: 5 }, // Oreja izq
{ type: "circle", x1: 60, y1: 35, radius: 10 }, { type: "circle", x1: 60, y1: 35, radius: 5 }, // Oreja der
// Nariz grande y aplanada
{ x1: 50, y1: 55, x2: 45, y2: 58 }, { x1: 45, y1: 58, x2: 55, y2: 58 }, { x1: 55, y1: 58, x2: 50, y2: 55 },
// Ojos
{ type: "circle", x1: 45, y1: 48, radius: 1.5 }, { type: "circle", x1: 55, y1: 48, radius: 1.5 }
],
"PINGUINO": [
// Cuerpo ovalado (con vientre blanco y alas)
{ x1: 50, y1: 80, x2: 50, y2: 30 }, { x1: 40, y1: 80, x2: 40, y2: 35 }, { x1: 60, y1: 80, x2: 60, y2: 35 },
{ x1: 40, y1: 35, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 60, y2: 35 }, // Cabeza y cuerpo
{ x1: 45, y1: 70, x2: 55, y2: 70 }, // Base del vientre
{ x1: 45, y1: 70, x2: 45, y2: 45 }, { x1: 55, y1: 70, x2: 55, y2: 45 }, // Contorno del vientre
{ x1: 40, y1: 50, x2: 35, y2: 55 }, { x1: 35, y1: 55, x2: 40, y2: 60 }, // Ala izq
{ x1: 60, y1: 50, x2: 65, y2: 55 }, { x1: 65, y1: 55, x2: 60, y2: 60 }, // Ala der
// Ojos y pico (más detallados)
{ type: "circle", x1: 47, y1: 40, radius: 1 }, { type: "circle", x1: 53, y1: 40, radius: 1 }, // Ojos
{ x1: 50, y1: 42, x2: 48, y2: 45 }, { x1: 48, y1: 45, x2: 52, y2: 45 }, { x1: 52, y1: 45, x2: 50, y2: 42 } // Pico
],
"BUHO": [
// Cuerpo (más plumoso y forma de corazón invertido en la cara)
{ type: "circle", x1: 50, y1: 50, radius: 25 },
{ x1: 40, y1: 40, x2: 60, y2: 40 }, { x1: 40, y1: 40, x2: 35, y2: 50 }, // Cara en forma de corazón
{ x1: 60, y1: 40, x2: 65, y2: 50 }, { x1: 35, y1: 50, x2: 65, y2: 50 },
// Ojos grandes (con brillo y párpados)
{ type: "circle", x1: 42, y1: 45, radius: 8 }, { type: "circle", x1: 42, y1: 45, radius: 2 }, // Ojo izq con brillo
{ type: "circle", x1: 58, y1: 45, radius: 8 }, { type: "circle", x1: 58, y1: 45, radius: 2 }, // Ojo der con brillo
{ x1: 38, y1: 42, x2: 46, y2: 42 }, { x1: 54, y1: 42, x2: 62, y2: 42 }, // Párpados
// Pico (pequeño y puntiagudo)
{ x1: 50, y1: 52, x2: 48, y2: 55 }, { x1: 48, y1: 55, x2: 52, y2: 55 }, { x1: 52, y1: 55, x2: 50, y2: 52 }
],
"AGUILA": [
// Cuerpo y cabeza (más robustos)
{ x1: 50, y1: 40, x2: 50, y2: 30 }, { x1: 45, y1: 30, x2: 55, y2: 30 }, // Cabeza
{ x1: 50, y1: 40, x2: 45, y2: 50 }, { x1: 45, y1: 50, x2: 50, y2: 60 }, // Cuerpo
{ x1: 50, y1: 60, x2: 55, y2: 50 }, { x1: 55, y1: 50, x2: 50, y2: 40 },
// Alas (más grandes y plumosas)
{ x1: 45, y1: 45, x2: 20, y2: 40 }, { x1: 20, y1: 40, x2: 25, y2: 50 }, { x1: 25, y1: 50, x2: 30, y2: 55 }, // Ala izq
{ x1: 55, y1: 45, x2: 80, y2: 40 }, { x1: 80, y1: 40, x2: 75, y2: 50 }, { x1: 75, y1: 50, x2: 70, y2: 55 }, // Ala der
// Pico y ojo
{ x1: 50, y1: 35, x2: 48, y2: 38 }, { x1: 48, y1: 38, x2: 50, y2: 40 }, { x1: 50, y1: 40, x2: 52, y2: 38 }, { x1: 52, y1: 38, x2: 50, y2: 35 }, // Pico
{ type: "circle", x1: 47, y1: 35, radius: 1 } // Ojo
],
"DELFIN": [
// Cuerpo (más elegante y curvado)
{ x1: 30, y1: 60, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 75, y2: 45 }, // Lomo y morro
{ x1: 30, y1: 60, x2: 70, y2: 70 }, { x1: 70, y1: 70, x2: 75, y2: 65 }, // Vientre
{ x1: 75, y1: 45, x2: 80, y2: 55 }, { x1: 80, y1: 55, x2: 75, y2: 65 }, // Transición a la cola
// Cola (más ancha y bifurcada)
{ x1: 80, y1: 55, x2: 85, y2: 50 }, { x1: 85, y1: 50, x2: 85, y2: 60 }, { x1: 85, y1: 60, x2: 80, y2: 55 },
// Aleta dorsal (curvada)
{ x1: 60, y1: 40, x2: 60, y2: 30 }, { x1: 60, y1: 30, x2: 55, y2: 35 }, { x1: 55, y1: 35, x2: 60, y2: 40 },
// Aleta pectoral (sutil)
{ x1: 40, y1: 60, x2: 42, y2: 65 }
],
"BALLENA": [
// Cuerpo (más masivo y redondeado)
{ x1: 20, y1: 60, x2: 80, y2: 60 }, { x1: 20, y1: 60, x2: 25, y2: 50 },
{ x1: 80, y1: 60, x2: 75, y2: 50 }, { x1: 25, y1: 50, x2: 75, y2: 50 }, // Contorno del cuerpo
{ x1: 30, y1: 55, x2: 70, y2: 55 }, // Línea de la barriga
// Cola (grande y bifurcada)
{ x1: 70, y1: 55, x2: 80, y2: 45 }, { x1: 80, y2: 45, x2: 85, y2: 45 }, { x1: 85, y2: 45, x2: 80, y2: 55 }, // Lado superior de la cola
{ x1: 70, y1: 55, x2: 80, y2: 65 }, { x1: 80, y2: 65, x2: 85, y2: 65 }, { x1: 85, y2: 65, x2: 80, y2: 55 }, // Lado inferior de la cola
// Soplete (líneas hacia arriba)
{ x1: 40, y1: 45, x2: 40, y2: 35 }, { x1: 42, y1: 45, x2: 42, y2: 35 }, { x1: 38, y1: 45, x2: 38, y2: 35 }
],
"TIBURON": [
// Cuerpo (más agresivo y aerodinámico)
{ x1: 20, y1: 60, x2: 80, y2: 55 }, { x1: 80, y1: 55, x2: 75, y2: 65 },
{ x1: 75, y2: 65, x2: 20, y2: 60 }, // Contorno del cuerpo
{ x1: 30, y1: 60, x2: 35, y2: 62 }, { x1: 60, y1: 58, x2: 65, y2: 60 }, // Branquias simuladas
// Aleta dorsal (grande y puntiaguda)
{ x1: 50, y1: 50, x2: 50, y2: 40 }, { x1: 45, y1: 40, x2: 55, y2: 40 }, { x1: 55, y1: 40, x2: 50, y2: 50 },
// Aleta caudal (cola)
{ x1: 80, y1: 55, x2: 85, y2: 50 }, { x1: 85, y1: 50, x2: 80, y2: 60 }, { x1: 80, y1: 60, x2: 85, y2: 65 }, // Bifurcación
// Ojo y boca con dientes
{ type: "circle", x1: 30, y1: 55, radius: 1 }, // Ojo
{ x1: 25, y1: 58, x2: 35, y2: 60 }, { x1: 35, y1: 60, x2: 40, y2: 58 } // Boca con dientes simulados
],
"PULPO": [
// Cabeza (más abultada)
{ type: "circle", x1: 50, y1: 40, radius: 15 },
// Ojos
{ type: "circle", x1: 45, y1: 38, radius: 1 }, { type: "circle", x1: 55, y1: 38, radius: 1 },
// Tentáculos (más curvos y con ventosas)
{ x1: 40, y1: 55, x2: 30, y2: 65 }, { x1: 30, y2: 65, x2: 35, y2: 70 }, { x1: 35, y2: 70, x2: 40, y2: 60 }, // Tentáculo 1
{ type: "circle", x1: 33, y1: 67, radius: 0.5 }, { type: "circle", x1: 37, y1: 62, radius: 0.5 }, // Ventosas
{ x1: 50, y1: 55, x2: 45, y2: 65 }, { x1: 45, y2: 65, x2: 50, y2: 70 }, { x1: 50, y2: 70, x2: 55, y2: 65 }, { x1: 55, y2: 65, x2: 50, y2: 55 }, // Tentáculo 2 (frontal)
{ type: "circle", x1: 47, y1: 67, radius: 0.5 }, { type: "circle", x1: 53, y1: 67, radius: 0.5 },
{ x1: 60, y1: 55, x2: 70, y2: 65 }, { x1: 70, y2: 65, x2: 65, y2: 70 }, { x1: 65, y2: 70, x2: 60, y2: 60 }, // Tentáculo 3
{ type: "circle", x1: 67, y1: 67, radius: 0.5 }, { type: "circle", x1: 63, y1: 62, radius: 0.5 }
],
"CANGREJO": [
// Cuerpo (más ancho y con textura)
{ x1: 30, y1: 60, x2: 70, y2: 60 }, { x1: 30, y1: 60, x2: 25, y2: 55 },
{ x1: 70, y1: 60, x2: 75, y2: 55 }, { x1: 25, y1: 55, x2: 75, y2: 55 }, // Parte superior
{ x1: 30, y1: 60, x2: 30, y2: 65 }, { x1: 70, y1: 60, x2: 70, y2: 65 }, { x1: 30, y1: 65, x2: 70, y2: 65 }, // Base del cuerpo
// Ojos tallo (stalk eyes)
{ x1: 40, y1: 55, x2: 40, y2: 45 }, { type: "circle", x1: 40, y1: 45, radius: 1 },
{ x1: 60, y1: 55, x2: 60, y2: 45 }, { type: "circle", x1: 60, y1: 45, radius: 1 },
// Patas (varias y articuladas)
{ x1: 25, y1: 60, x2: 20, y2: 65 }, { x1: 20, y2: 65, x2: 25, y2: 70 }, // Pata 1
{ x1: 35, y1: 65, x2: 30, y2: 70 }, { x1: 30, y2: 70, x2: 35, y2: 75 }, // Pata 2
{ x1: 75, y1: 60, x2: 80, y2: 65 }, { x1: 80, y2: 65, x2: 75, y2: 70 }, // Pata 3
{ x1: 65, y1: 65, x2: 70, y2: 70 }, { x1: 70, y2: 70, x2: 65, y2: 75 }, // Pata 4
// Pinzas (más grandes y con detalles)
{ x1: 25, y1: 55, x2: 15, y2: 50 }, { x1: 15, y1: 50, x2: 20, y2: 45 }, { x1: 20, y1: 45, x2: 25, y2: 50 }, // Pinza izq (cierre)
{ x1: 18, y1: 50, x2: 23, y2: 50 }, // Detalle de cierre
{ x1: 75, y1: 55, x2: 85, y2: 50 }, { x1: 85, y1: 50, x2: 80, y2: 45 }, { x1: 80, y1: 45, x2: 75, y2: 50 } // Pinza der (cierre)
],
"MEDUSA": [
// Cuerpo superior (forma de campana y con patrón)
{ x1: 50, y1: 40, x2: 35, y2: 45 }, { x1: 35, y1: 45, x2: 30, y2: 55 }, { x1: 30, y1: 55, x2: 40, y2: 60 },
{ x1: 40, y1: 60, x2: 60, y2: 60 }, { x1: 60, y1: 60, x2: 70, y2: 55 }, { x1: 70, y1: 55, x2: 65, y2: 45 },
{ x1: 65, y1: 45, x2: 50, y2: 40 }, // Contorno de la campana
{ x1: 40, y1: 48, x2: 60, y2: 48 }, // Línea interna
// Tentáculos (más numerosos y ondulados)
{ x1: 40, y1: 60, x2: 35, y2: 70 }, { x1: 35, y2: 70, x2: 40, y2: 75 }, // Tentáculo 1
{ x1: 50, y1: 60, x2: 45, y2: 70 }, { x1: 45, y2: 70, x2: 50, y2: 75 }, // Tentáculo 2
{ x1: 60, y1: 60, x2: 65, y2: 70 }, { x1: 65, y2: 70, x2: 60, y2: 75 }, // Tentáculo 3
{ x1: 42, y1: 60, x2: 38, y2: 68 }, { x1: 38, y2: 68, x2: 42, y2: 72 }, // Tentáculo 4 (intermedio)
{ x1: 58, y1: 60, x2: 62, y2: 68 }, { x1: 62, y2: 68, x2: 58, y2: 72 } // Tentáculo 5 (intermedio)
],
"DINOSAURIO": [
// Contorno de dinosaurio (cuello largo, cuerpo y patas robustas)
{ x1: 20, y1: 70, x2: 30, y2: 65 }, { x1: 30, y1: 65, x2: 40, y2: 68 }, // Pierna delantera
{ x1: 40, y1: 68, x2: 60, y2: 68 }, // Vientre
{ x1: 60, y1: 68, x2: 70, y2: 65 }, { x1: 70, y1: 65, x2: 75, y2: 70 }, // Pierna trasera y cola
{ x1: 75, y1: 70, x2: 80, y2: 65 }, { x1: 80, y1: 65, x2: 75, y2: 60 }, // Cola
{ x1: 60, y1: 68, x2: 65, y2: 55 }, { x1: 65, y1: 55, x2: 60, y2: 50 }, // Lomo
{ x1: 60, y1: 50, x2: 50, y2: 40 }, { x1: 50, y1: 40, x2: 45, y2: 35 }, { x1: 45, y1: 35, x2: 40, y2: 40 }, // Cuello
{ x1: 40, y1: 40, x2: 35, y2: 45 }, { x1: 35, y1: 45, x2: 30, y2: 50 }, { x1: 30, y1: 50, x2: 30, y2: 65 } // Cabeza y cuello
],
"DRAGON": [
// Cuerpo (más musculoso y con escamas)
{ x1: 30, y1: 60, x2: 70, y2: 60 }, { x1: 70, y1: 60, x2: 65, y2: 40 },
{ x1: 65, y1: 40, x2: 35, y2: 40 }, { x1: 35, y1: 40, x2: 30, y2: 60 },
{ x1: 35, y1: 45, x2: 40, y2: 48 }, { x1: 45, y1: 45, x2: 50, y2: 48 }, // Escamas (simuladas)
// Alas (más grandes y membranosas)
{ x1: 40, y1: 40, x2: 30, y2: 30 }, { x1: 30, y1: 30, x2: 40, y2: 25 }, { x1: 40, y1: 25, x2: 45, y2: 30 }, { x1: 45, y1: 30, x2: 40, y2: 40 }, // Ala izq
{ x1: 60, y1: 40, x2: 70, y2: 30 }, { x1: 70, y1: 30, x2: 60, y2: 25 }, { x1: 60, y1: 25, x2: 55, y2: 30 }, { x1: 55, y1: 30, x2: 60, y2: 40 }, // Ala der
// Cola (larga y puntiaguda)
{ x1: 70, y1: 60, x2: 80, y2: 55 }, { x1: 80, y1: 55, x2: 85, y2: 60 }, { x1: 85, y1: 60, x2: 80, y2: 65 }, { x1: 80, y1: 65, x2: 75, y2: 60 },
// Cabeza y cuernos
{ x1: 35, y1: 40, x2: 30, y2: 35 }, { x1: 30, y2: 35, x2: 35, y2: 30 }, // Cuerno izq
{ x1: 65, y1: 40, x2: 70, y2: 35 }, { x1: 70, y2: 35, x2: 65, y2: 30 } // Cuerno der
],
"UNICORNIO": [
// Cuerpo de caballo (elegante y estilizado)
{ x1: 50, y1: 70, x2: 35, y2: 60 }, { x1: 35, y1: 60, x2: 30, y2: 45 }, // Cuello y pecho
{ x1: 30, y1: 45, x2: 35, y2: 35 }, { x1: 35, y1: 35, x2: 45, y2: 30 }, // Cabeza
{ x1: 45, y1: 30, x2: 55, y2: 30 }, { x1: 55, y1: 30, x2: 65, y2: 35 },
{ x1: 65, y1: 35, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 65, y2: 60 }, // Lomo
{ x1: 65, y1: 60, x2: 50, y2: 70 },
// Cuerno (espiral y puntiagudo)
{ x1: 50, y1: 30, x2: 50, y2: 15 }, { x1: 48, y1: 15, x2: 52, y2: 15 }, // Punta
{ x1: 49, y1: 20, x2: 51, y2: 20 }, { x1: 49, y1: 25, x2: 51, y2: 25 }, // Espiral
// Melena (líneas fluidas)
{ x1: 45, y1: 30, x2: 40, y2: 25 }, { x1: 40, y2: 25, x2: 35, y2: 30 }
],
"SIRENA": [
// Cuerpo (más curvado y femenino)
{ x1: 50, y1: 40, x2: 45, y2: 50 }, { x1: 45, y1: 50, x2: 50, y2: 60 }, // Torso
{ x1: 50, y1: 40, x2: 55, y2: 50 }, { x1: 55, y1: 50, x2: 50, y2: 60 },
// Cola de pez (escamas y aleta bifurcada)
{ x1: 50, y1: 60, x2: 45, y2: 70 }, { x1: 45, y1: 70, x2: 55, y2: 70 }, { x1: 55, y1: 70, x2: 50, y2: 60 },
{ x1: 45, y1: 70, x2: 40, y2: 75 }, { x1: 40, y1: 75, x2: 45, y2: 80 }, { x1: 45, y1: 80, x2: 50, y2: 70 }, // Aleta inferior
{ x1: 55, y1: 70, x2: 60, y2: 75 }, { x1: 60, y1: 75, x2: 55, y2: 80 }, { x1: 55, y1: 80, x2: 50, y2: 70 },
// Pelo largo
{ x1: 45, y1: 40, x2: 40, y2: 30 }, { x1: 40, y2: 30, x2: 35, y2: 45 }
],
"HADA": [
// Cuerpo (más delicado)
{ x1: 50, y1: 40, x2: 50, y2: 60 },
{ x1: 45, y1: 40, x2: 55, y2: 40 }, // Hombros
// Alas (transparentes, con venas y formas más orgánicas)
{ x1: 40, y1: 50, x2: 30, y2: 45 }, { x1: 30, y1: 45, x2: 40, y2: 40 }, { x1: 40, y1: 40, x2: 45, y2: 45 }, { x1: 45, y1: 45, x2: 40, y2: 50 }, // Ala superior izq
{ x1: 35, y1: 50, x2: 25, y2: 55 }, { x1: 25, y1: 55, x2: 35, y2: 60 }, { x1: 35, y1: 60, x2: 40, y2: 55 }, { x1: 40, y2: 55, x2: 35, y2: 50 }, // Ala inferior izq
{ x1: 60, y1: 50, x2: 70, y2: 45 }, { x1: 70, y1: 45, x2: 60, y2: 40 }, { x1: 60, y1: 40, x2: 55, y2: 45 }, { x1: 55, y1: 45, x2: 60, y2: 50 }, // Ala superior der
{ x1: 65, y1: 50, x2: 75, y2: 55 }, { x1: 75, y1: 55, x2: 65, y2: 60 }, { x1: 65, y1: 60, x2: 60, y2: 55 }, { x1: 60, y2: 55, x2: 65, y2: 50 }, // Ala inferior der
// Varita mágica (con estrella pequeña)
{ x1: 60, y1: 40, x2: 70, y2: 35 }, { x1: 68, y1: 35, x2: 70, y2: 30 }, { x1: 70, y2: 30, x2: 72, y2: 35 }, { x1: 72, y2: 35, x2: 70, y2: 35 }
],
"MAGO": [
// Cuerpo (con túnica)
{ x1: 50, y1: 40, x2: 50, y2: 60 }, { x1: 40, y1: 60, x2: 60, y2: 60 },
{ x1: 40, y1: 60, x2: 35, y2: 75 }, { x1: 60, y1: 60, x2: 65, y2: 75 }, { x1: 35, y1: 75, x2: 65, y2: 75 },
// Sombrero de punta (más detallado)
{ x1: 40, y1: 60, x2: 60, y2: 60 }, // Base del sombrero
{ x1: 40, y1: 60, x2: 45, y2: 40 }, { x1: 60, y1: 60, x2: 55, y2: 40 }, // Lados
{ x1: 45, y1: 40, x2: 50, y2: 25 }, { x1: 50, y1: 25, x2: 55, y2: 40 }, // Parte superior puntiaguda
// Varita mágica (con un brillo en la punta)
{ x1: 65, y1: 50, x2: 75, y2: 45 },
{ type: "circle", x1: 75, y1: 45, radius: 1.5 } // Brillo
],
"GUERRERO": [
// Armadura básica (torso, hombreras, piernas)
{ x1: 50, y1: 40, x2: 50, y2: 60 }, { x1: 40, y1: 60, x2: 60, y2: 60 },
{ x1: 40, y1: 60, x2: 40, y2: 75 }, { x1: 60, y1: 60, x2: 60, y2: 75 }, { x1: 40, y1: 75, x2: 60, y2: 75 }, // Piernas
// Yelmo (con visera)
{ x1: 45, y1: 40, x2: 55, y2: 40 }, { x1: 45, y1: 40, x2: 40, y2: 35 }, { x1: 55, y1: 40, x2: 60, y2: 35 }, // Base del yelmo
{ x1: 40, y1: 35, x2: 60, y2: 35 },
{ x1: 45, y1: 38, x2: 55, y2: 38 }, // Visera
// Hombros (con forma de armadura)
{ x1: 40, y1: 45, x2: 35, y2: 50 }, { x1: 35, y2: 50, x2: 40, y2: 55 },
{ x1: 60, y1: 45, x2: 65, y2: 50 }, { x1: 65, y2: 50, x2: 60, y2: 55 }
],
"CABALLERO": [
// Armadura (con más detalles y grebas)
{ x1: 50, y1: 40, x2: 50, y2: 60 }, { x1: 40, y1: 60, x2: 60, y2: 60 },
{ x1: 40, y1: 60, x2: 40, y2: 75 }, { x1: 60, y1: 60, x2: 60, y2: 75 }, { x1: 40, y1: 75, x2: 60, y2: 75 }, // Grebas
{ x1: 42, y1: 65, x2: 58, y2: 65 }, // Rodillera (línea)
// Yelmo (con pluma)
{ x1: 45, y1: 40, x2: 55, y2: 40 }, { x1: 45, y1: 40, x2: 40, y2: 35 }, { x1: 55, y1: 40, x2: 60, y2: 35 },
{ x1: 40, y1: 35, x2: 60, y2: 35 },
{ x1: 50, y1: 35, x2: 50, y2: 25 }, { x1: 48, y1: 25, x2: 52, y2: 25 }, // Pluma
// Espada (con guarda y pomo)
{ x1: 65, y1: 50, x2: 75, y2: 40 }, { x1: 75, y1: 40, x2: 78, y2: 43 }, { x1: 78, y2: 43, x2: 68, y2: 53 }, // Hoja
{ x1: 65, y1: 50, x2: 68, y2: 50 }, { x1: 68, y1: 50, x2: 68, y2: 53 }, { x1: 68, y1: 53, x2: 65, y2: 53 } // Guarda
],
"PRINCESA": [
// Cuerpo (más curvado)
{ x1: 50, y1: 40, x2: 50, y2: 60 },
{ x1: 45, y1: 40, x2: 55, y2: 40 }, // Hombros
// Vestido (más voluminoso y con pliegues)
{ x1: 40, y1: 60, x2: 60, y2: 60 },
{ x1: 35, y1: 60, x2: 40, y2: 70 }, { x1: 65, y1: 60, x2: 60, y2: 70 },
{ x1: 35, y1: 70, x2: 65, y2: 70 },
{ x1: 45, y1: 65, x2: 50, y2: 68 }, { x1: 50, y1: 68, x2: 55, y2: 65 }, // Pliegues
// Corona (con gemas)
{ x1: 45, y1: 35, x2: 55, y2: 35 },
{ x1: 48, y1: 35, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 52, y2: 35 },
{ type: "circle", x1: 48, y1: 32, radius: 0.5 }, { type: "circle", x1: 52, y1: 32, radius: 0.5 } // Gemas
],
"REINA": [
// Cuerpo (similar a princesa, pero más formal)
{ x1: 50, y1: 40, x2: 50, y2: 60 },
{ x1: 45, y1: 40, x2: 55, y2: 40 },
// Vestido (más regio y amplio)
{ x1: 40, y1: 60, x2: 60, y2: 60 }, { x1: 35, y1: 60, x2: 40, y2: 70 },
{ x1: 65, y1: 60, x2: 60, y2: 70 }, { x1: 35, y1: 70, x2: 65, y2: 70 },
{ x1: 40, y1: 65, x2: 45, y2: 68 }, { x1: 55, y1: 68, x2: 60, y2: 65 }, // Pliegues
// Corona grande (con más picos y gemas)
{ x1: 40, y1: 35, x2: 60, y2: 35 },
{ x1: 42, y1: 35, x2: 45, y2: 30 }, { x1: 45, y1: 30, x2: 50, y2: 32 },
{ x1: 50, y1: 32, x2: 55, y2: 30 }, { x1: 55, y1: 30, x2: 58, y2: 35 },
{ type: "circle", x1: 45, y1: 32, radius: 0.8 }, { type: "circle", x1: 50, y1: 30, radius: 0.8 }, { type: "circle", x1: 55, y1: 32, radius: 0.8 } // Gemas
],
"REY": [
// Cuerpo (robusto y con capa)
{ x1: 50, y1: 40, x2: 50, y2: 60 }, { x1: 40, y1: 60, x2: 60, y2: 60 },
{ x1: 35, y1: 45, x2: 30, y2: 60 }, { x1: 30, y2: 60, x2: 35, y2: 70 }, // Capa izquierda
{ x1: 65, y1: 45, x2: 70, y2: 60 }, { x1: 70, y2: 60, x2: 65, y2: 70 }, // Capa derecha
{ x1: 40, y1: 70, x2: 60, y2: 70 }, // Base
// Corona (con cruces pequeñas y gemas)
{ x1: 45, y1: 35, x2: 55, y2: 35 },
{ x1: 48, y1: 35, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 52, y2: 35 },
{ type: "circle", x1: 48, y1: 32, radius: 0.5 }, { type: "circle", x1: 52, y1: 32, radius: 0.5 },
{ x1: 47, y1: 37, x2: 47, y2: 39 }, { x1: 46, y1: 38, x2: 48, y2: 38 }, // Cruz izq
{ x1: 53, y1: 37, x2: 53, y2: 39 }, { x1: 52, y1: 38, x2: 54, y2: 38 }, // Cruz der
// Cetro (más detallado)
{ x1: 65, y1: 50, x2: 75, y2: 40 }, { x1: 75, y1: 40, x2: 78, y2: 43 },
{ x1: 78, y2: 43, x2: 75, y2: 46 }, { x1: 75, y2: 46, x2: 65, y2: 56 }
],
"CASTILLO": [
// Base (con textura de piedra)
{ x1: 20, y1: 70, x2: 80, y2: 70 }, { x1: 20, y1: 70, x2: 20, y2: 40 },
{ x1: 80, y1: 70, x2: 80, y2: 40 }, { x1: 20, y1: 40, x2: 80, y2: 40 },
{ x1: 25, y1: 65, x2: 30, y2: 68 }, { x1: 35, y1: 65, x2: 40, y2: 68 }, // Textura de piedra
// Almenas (más definidas y con grosor)
{ x1: 20, y1: 40, x2: 20, y2: 35 }, { x1: 25, y1: 35, x2: 25, y2: 40 }, { x1: 20, y1: 35, x2: 25, y2: 35 },
{ x1: 30, y1: 40, x2: 30, y2: 35 }, { x1: 35, y1: 35, x2: 35, y2: 40 }, { x1: 30, y1: 35, x2: 35, y2: 35 },
{ x1: 40, y1: 40, x2: 40, y2: 35 }, { x1: 45, y1: 35, x2: 45, y2: 40 }, { x1: 40, y1: 35, x2: 45, y2: 35 },
{ x1: 75, y1: 40, x2: 75, y2: 35 }, { x1: 80, y1: 35, x2: 80, y2: 40 }, { x1: 75, y1: 35, x2: 80, y2: 35 },
// Torre central (más alta y con almenas)
{ x1: 50, y1: 40, x2: 50, y2: 20 }, { x1: 60, y1: 40, x2: 60, y2: 20 }, { x1: 50, y1: 20, x2: 60, y2: 20 },
{ x1: 50, y1: 20, x2: 50, y2: 15 }, { x1: 55, y1: 15, x2: 55, y2: 20 }, { x1: 50, y1: 15, x2: 55, y2: 15 } // Almenas de torre
],
"ESPADA": [
// Hoja (Afilada y con canal central)
{ x1: 50, y1: 10, x2: 45, y2: 15 }, { x1: 50, y1: 10, x2: 55, y2: 15 }, // Punta
{ x1: 45, y1: 15, x2: 45, y2: 60 }, { x1: 55, y1: 15, x2: 55, y2: 60 },
{ x1: 50, y1: 15, x2: 50, y2: 60 }, // Canal central (sangradera)
// Guarda (Cross-guard más elaborada)
{ x1: 40, y1: 60, x2: 60, y2: 60 },
{ x1: 40, y1: 60, x2: 35, y2: 63 }, { x1: 60, y1: 60, x2: 65, y2: 63 },
{ x1: 45, y1: 60, x2: 45, y2: 65 }, { x1: 55, y1: 60, x2: 55, y2: 65 },
{ x1: 45, y1: 65, x2: 55, y2: 65 }, // Línea horizontal que une la guarda con la empuñadura (added)
// Empuñadura (Grip con textura)
{ x1: 48, y1: 65, x2: 48, y2: 80 }, { x1: 52, y1: 65, x2: 52, y2: 80 },
{ x1: 48, y1: 70, x2: 52, y2: 70 }, { x1: 48, y1: 75, x2: 52, y2: 75 }, // Textura
// Pomo (Pommel)
{ type: "circle", x1: 50, y1: 82, radius: 3 }
],
"ESCUDO": [
// Escudo (con borde y forma más orgánica)
{ x1: 30, y1: 40, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 70, y2: 60 },
{ x1: 70, y1: 60, x2: 50, y2: 70 }, { x1: 50, y1: 70, x2: 30, y2: 60 },
{ x1: 30, y1: 60, x2: 30, y2: 40 },
{ x1: 32, y1: 42, x2: 68, y2: 42 }, { x1: 68, y1: 42, x2: 68, y2: 58 }, // Borde interior
{ x1: 68, y1: 58, x2: 50, y2: 68 }, { x1: 50, y1: 68, x2: 32, y2: 58 }, { x1: 32, y1: 58, x2: 32, y2: 42 },
// Emblema (cruz con centro)
{ x1: 40, y1: 45, x2: 60, y2: 45 }, { x1: 50, y1: 45, x2: 50, y2: 65 },
{ type: "circle", x1: 50, y1: 55, radius: 3 } // Centro de la cruz
],
"ARCO": [
// Arco (con grosor y curvatura)
{ x1: 30, y1: 60, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 70, y2: 60 },
{ x1: 32, y1: 58, x2: 50, y2: 32 }, { x1: 50, y1: 32, x2: 68, y2: 58 }, // Grosor
// Cuerda (más fina y tensa)
{ x1: 30, y1: 60, x2: 70, y2: 60 },
// Flecha (con punta y plumas)
{ x1: 50, y1: 45, x2: 80, y2: 45 }, // Cuerpo de la flecha
{ x1: 75, y1: 42, x2: 80, y2: 45 }, { x1: 80, y1: 45, x2: 75, y2: 48 }, // Punta
{ x1: 25, y1: 45, x2: 20, y2: 42 }, { x1: 20, y2: 42, x2: 20, y2: 48 }, { x1: 20, y2: 48, x2: 25, y2: 45 } // Plumas
],
"FLECHA": [
// Cuerpo de la flecha (más detallado)
{ x1: 20, y1: 50, x2: 80, y2: 50 },
{ x1: 20, y1: 50, x2: 22, y2: 48 }, { x1: 20, y1: 50, x2: 22, y2: 52 }, // Base del asta
// Punta (más aguda y con base)
{ x1: 75, y1: 45, x2: 80, y2: 50 }, { x1: 80, y1: 50, x2: 75, y2: 55 },
{ x1: 75, y1: 45, x2: 75, y2: 55 }, // Base de la punta
// Cola (plumas, más detalladas y en ángulo)
{ x1: 25, y1: 45, x2: 20, y2: 40 }, { x1: 20, y1: 40, x2: 20, y2: 50 }, { x1: 20, y1: 50, x2: 25, y2: 55 },
{ x1: 25, y1: 55, x2: 20, y2: 60 }, { x1: 20, y2: 60, x2: 20, y2: 50 }, { x1: 20, y2: 50, x2: 25, y2: 45 }
],
"POCION": [
// Frasco (más curvo y con etiqueta)
{ x1: 40, y1: 70, x2: 60, y2: 70 }, // Base
{ x1: 40, y1: 70, x2: 38, y2: 55 }, { x1: 60, y1: 70, x2: 62, y2: 55 }, // Curvas del cuerpo
{ x1: 38, y1: 55, x2: 38, y2: 45 }, { x1: 62, y1: 55, x2: 62, y2: 45 }, // Lados del cuerpo
// Cuello
{ x1: 45, y1: 45, x2: 45, y2: 35 }, { x1: 55, y1: 45, x2: 55, y2: 35 },
// Boca y tapón
{ x1: 48, y1: 35, x2: 52, y2: 35 }, { x1: 48, y1: 35, x2: 48, y2: 30 }, { x1: 52, y1: 35, x2: 52, y2: 30 }, // Tapón
{ x1: 48, y1: 30, x2: 52, y2: 30 },
// Etiqueta (rectángulo en el cuerpo)
{ x1: 42, y1: 58, x2: 58, y2: 58 }, { x1: 42, y1: 58, x2: 42, y2: 65 },
{ x1: 58, y1: 58, x2: 58, y2: 65 }, { x1: 42, y1: 65, x2: 58, y2: 65 }
],
"ANILLO": [
// Anillo (con grosor y más forma)
{ type: "circle", x1: 50, y1: 50, radius: 20 },
{ type: "circle", x1: 50, y1: 50, radius: 18 }, // Grosor del anillo
// Gema (más facetada y con brillo)
{ x1: 45, y1: 35, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 55, y2: 35 }, { x1: 55, y1: 35, x2: 45, y2: 35 }, // Forma de diamante
{ x1: 50, y1: 30, x2: 48, y2: 33 }, { x1: 50, y1: 30, x2: 52, y2: 33 } // Facetas
],
"COLLAR": [
// Cadena (más fina y con eslabones sutiles)
{ x1: 30, y1: 40, x2: 70, y2: 40 },
{ x1: 32, y1: 40, x2: 32, y2: 42 }, { x1: 35, y1: 40, x2: 35, y2: 42 }, // Eslabones
{ x1: 65, y1: 40, x2: 65, y2: 42 }, { x1: 68, y1: 40, x2: 68, y2: 42 },
// Pendiente (más detallado)
{ x1: 45, y1: 40, x2: 50, y2: 50 }, { x1: 50, y1: 50, x2: 55, y2: 40 }, // Forma principal
{ x1: 50, y1: 50, x2: 50, y2: 60 }, { x1: 48, y1: 60, x2: 52, y2: 60 }, // Base del pendiente
{ x1: 47, y1: 52, x2: 53, y2: 52 } // Gema simulada
],
"CORONA": [
// Base (con grosor)
{ x1: 30, y1: 60, x2: 70, y2: 60 }, { x1: 30, y1: 60, x2: 32, y2: 63 }, // Grosor de la base
{ x1: 70, y1: 60, x2: 68, y2: 63 }, { x1: 32, y1: 63, x2: 68, y2: 63 },
// Puntas (más agudas y con detalles)
{ x1: 30, y1: 60, x2: 35, y2: 45 }, { x1: 35, y1: 45, x2: 40, y2: 60 }, // Punta 1
{ x1: 35, y1: 50, x2: 38, y2: 48 }, // Detalle de punta
{ x1: 45, y1: 60, x2: 50, y2: 45 }, { x1: 50, y1: 45, x2: 55, y2: 60 }, // Punta 2
{ x1: 50, y1: 50, x2: 50, y2: 48 },
{ x1: 60, y1: 60, x2: 65, y2: 45 }, { x1: 65, y1: 45, x2: 70, y2: 60 } // Punta 3
],
"GEMA": [
// Gema facetada (más compleja)
{ x1: 50, y1: 30, x2: 30, y2: 50 }, { x1: 30, y1: 50, x2: 50, y2: 70 },
{ x1: 50, y1: 70, x2: 70, y2: 50 }, { x1: 70, y1: 50, x2: 50, y2: 30 }, // Contorno exterior
// Facetas internas (líneas de división)
{ x1: 50, y1: 30, x2: 50, y2: 70 }, { x1: 30, y1: 50, x2: 70, y2: 50 },
{ x1: 40, y1: 35, x2: 40, y2: 65 }, { x1: 60, y1: 35, x2: 60, y2: 65 },
{ x1: 35, y1: 40, x2: 65, y2: 40 }, { x1: 35, y1: 60, x2: 65, y2: 60 },
// Brillo (pequeños puntos)
{ type: "circle", x1: 45, y1: 45, radius: 0.5 }, { type: "circle", x1: 55, y1: 45, radius: 0.5 }
],
"TESORO": [
// Cofre (con grosor y tapa semi-abierta)
{ x1: 30, y1: 60, x2: 70, y2: 60 }, { x1: 30, y1: 60, x2: 30, y2: 40 },
{ x1: 70, y1: 60, x2: 70, y2: 40 }, { x1: 30, y1: 40, x2: 70, y2: 40 }, // Cuerpo
{ x1: 32, y1: 62, x2: 68, y2: 62 }, { x1: 32, y1: 42, x2: 68, y2: 42 }, // Grosor del cuerpo
{ x1: 32, y1: 62, x2: 32, y2: 42 }, { x1: 68, y1: 62, x2: 68, y2: 42 },
// Tapa (curva y con bisagras)
{ x1: 30, y1: 40, x2: 35, y2: 35 }, { x1: 70, y1: 40, x2: 75, y2: 35 },
{ x1: 35, y1: 35, x2: 75, y2: 35 }, // Borde de la tapa
{ x1: 30, y1: 40, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 70, y2: 40 }, // Curva de la tapa
// Cerradura (más detallada)
{ x1: 50, y1: 45, x2: 50, y2: 55 }, { x1: 48, y1: 55, x2: 52, y2: 55 }, // Base de la cerradura
{ type: "circle", x1: 50, y1: 50, radius: 1 } // Agujero de la cerradura
],
"MONEDA": [
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Contorno exterior
{ type: "circle", x1: 50, y1: 50, radius: 18 }, // Borde interior
// Signo de dólar (con más detalle y sombreado)
{ x1: 48, y1: 40, x2: 48, y2: 60 }, { x1: 52, y1: 40, x2: 52, y2: 60 }, // Barras verticales
{ x1: 45, y1: 45, x2: 55, y2: 45 }, { x1: 45, y1: 55, x2: 55, y2: 55 }, // Barras horizontales
{ x1: 49, y1: 40, x2: 49, y2: 60 }, { x1: 51, y1: 40, x2: 51, y2: 60 }, // Sombreado del dólar
// Textura (pequeños puntos en el fondo)
{ type: "circle", x1: 40, y1: 40, radius: 0.2 }, { type: "circle", x1: 60, y1: 40, radius: 0.2 },
{ type: "circle", x1: 40, y1: 60, radius: 0.2 }, { type: "circle", x1: 60, y1: 60, radius: 0.2 }
],
"MAPA": [
// Mapa enrollado (con relieve y textura de papel)
{ x1: 30, y1: 40, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 70, y2: 60 },
{ x1: 70, y1: 60, x2: 30, y2: 60 }, { x1: 30, y1: 60, x2: 30, y2: 40 }, // Contorno
// Bordes enrollados (más gruesos y con pliegues)
{ x1: 25, y1: 40, x2: 30, y2: 45 }, { x1: 25, y1: 55, x2: 30, y2: 60 }, // Lado izq
{ x1: 25, y1: 40, x2: 25, y2: 60 },
{ x1: 70, y1: 45, x2: 75, y2: 40 }, { x1: 70, y1: 55, x2: 75, y2: 60 }, // Lado der
{ x1: 75, y1: 40, x2: 75, y2: 60 },
// Detalles internos (líneas de relieve o continentes)
{ x1: 40, y1: 45, x2: 50, y2: 50 }, { x1: 50, y1: 50, x2: 60, y2: 45 }, // Contorno de tierra
{ x1: 40, y1: 55, x2: 50, y2: 52 }, { x1: 50, y1: 52, x2: 60, y2: 55 }
],
"BRUJULA": [
{ type: "circle", x1: 50, y1: 50, radius: 20 }, // Contorno exterior
{ type: "circle", x1: 50, y1: 50, radius: 18 }, // Borde interior
// Aguja (más detallada, con dos colores simulados)
{ x1: 50, y1: 35, x2: 48, y2: 45 }, { x1: 48, y1: 45, x2: 50, y2: 50 }, // Punta norte
{ x1: 50, y1: 65, x2: 52, y2: 55 }, { x1: 52, y1: 55, x2: 50, y2: 50 }, // Punta sur
// Marcas de dirección (N, S, E, O)
{ x1: 50, y1: 30, x2: 50, y2: 32 }, { x1: 50, y1: 68, x2: 50, y2: 70 },
{ x1: 30, y1: 50, x2: 32, y2: 50 }, { x1: 68, y1: 50, x2: 70, y2: 50 }
],
"PERGAMINO": [
// Pergamino enrollado (con textura de arrugas)
{ x1: 30, y1: 40, x2: 70, y2: 40 }, { x1: 70, y1: 40, x2: 70, y2: 60 },
{ x1: 70, y1: 60, x2: 30, y2: 60 }, { x1: 30, y1: 60, x2: 30, y2: 40 }, // Cuerpo principal
// Enrollado (más detallado)
{ x1: 25, y1: 40, x2: 30, y2: 45 }, { x1: 25, y1: 55, x2: 30, y2: 60 }, // Lado izq
{ x1: 25, y1: 40, x2: 25, y2: 60 }, { x1: 28, y1: 43, x2: 28, y2: 57 }, // Interior del enrollado
{ x1: 70, y1: 45, x2: 75, y2: 40 }, { x1: 70, y1: 55, x2: 75, y2: 60 }, // Lado der
{ x1: 75, y1: 40, x2: 75, y2: 60 }, { x1: 72, y1: 43, x2: 72, y2: 57 },
// Texto simulado (líneas horizontales cortas)
{ x1: 35, y1: 48, x2: 65, y2: 48 }, { x1: 35, y1: 52, x2: 65, y2: 52 }
],
"ANTORCHA": [
// Palo (con textura de madera y más irregular)
{ x1: 50, y1: 80, x2: 50, y2: 60 }, { x1: 48, y1: 80, x2: 52, y2: 80 }, // Grosor de la base
{ x1: 49, y1: 75, x2: 49, y2: 70 }, { x1: 51, y1: 75, x2: 51, y2: 70 }, // Nudos de madera
{ x1: 47, y1: 65, x2: 47, y2: 60 }, { x1: 53, y1: 65, x2: 53, y2: 60 },
// Base de la llama (con tela)
{ x1: 45, y1: 60, x2: 55, y2: 60 },
{ x1: 45, y1: 60, x2: 42, y2: 58 }, { x1: 55, y1: 60, x2: 58, y2: 58 }, // Pliegues de tela
// Llama (más dinámica y con múltiples picos, simulando transparencia)
{ x1: 50, y1: 50, x2: 45, y2: 40 }, { x1: 45, y1: 40, x2: 50, y2: 30 }, { x1: 50, y1: 30, x2: 55, y2: 40 }, { x1: 55, y1: 40, x2: 50, y2: 50 }, // Contorno principal
{ x1: 48, y1: 45, x2: 50, y2: 40 }, { x1: 50, y1: 40, x2: 52, y2: 45 } // Llama interior
]
};
class IntelligentArtist extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
// Valores por defecto
#currentBrushSize = 5;
#currentSketchColor = "#888888";
constructor() {
super("Artista Inteligente (necesita bot)", '<i class="fas fa-brain"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
this.#row1(); // Generación de Bocetos Asistida
this.#row2(); // Limpiar Lienzo
this.#row3(); // Configuración de Color y Tamaño del Boceto
this.#row4(); // Lista de Bocetos Disponibles (con slider)
}
#row1() {
const row = domMake.Row();
{
const sketchInput = domMake.Tree("input", { type: "text", placeholder: "Concepto de boceto (ej. 'árbol')" });
const generateSketchButton = domMake.Button("Generar Boceto");
generateSketchButton.title = "Dibuja un boceto predefinido para la palabra ingresada.";
generateSketchButton.addEventListener("click", () => {
this.simulateAISketch(sketchInput.value.toUpperCase()); // Convertir a mayúsculas
});
row.appendAll(sketchInput, generateSketchButton);
}
this.htmlElements.section.appendChild(row);
}
#row2() {
const row = domMake.Row();
{
const clearCanvasButton = domMake.Button("Limpiar Lienzo");
clearCanvasButton.title = "Limpia el lienzo con una línea blanca muy grande.";
clearCanvasButton.addEventListener("click", () => {
const botManager = this.findGlobal("BotClientManager")?.siblings[0];
if (botManager && botManager.children.length > 0) {
const activeBot = botManager.children[0].bot; // Usar el primer bot activo
if (activeBot && activeBot.getReadyState()) {
// Dibuja dos líneas blancas que cubren todo el canvas con un grosor muy grande
activeBot.emit("line", -1, 0, 0, 100, 100, true, 900, "#FFFFFF", false); // Línea diagonal 1
activeBot.emit("line", -1, 100, 0, 0, 100, true, 900, "#FFFFFF", false); // Línea diagonal 2
this.notify("success", "El lienzo ha sido limpiado.");
} else {
this.notify("warning", "El bot seleccionado no está conectado.");
}
} else {
this.notify("warning", "Se necesita al menos 1 bot activo para limpiar el lienzo.");
}
});
row.appendChild(clearCanvasButton);
}
this.htmlElements.section.appendChild(row);
}
#row3() {
const row = domMake.Row();
{
const sketchColorLabel = domMake.Tree("label", {}, ["Color Boceto:"]);
const sketchColorInput = domMake.Tree("input", { type: "color", value: this.#currentSketchColor });
sketchColorInput.title = "Define el color del boceto.";
sketchColorInput.addEventListener("change", (e) => {
this.#currentSketchColor = e.target.value;
this.notify("info", `Color del boceto cambiado a: ${this.#currentSketchColor}`);
});
const brushSizeLabel = domMake.Tree("label", {}, ["Tamaño Pincel:"]);
const brushSizeInput = domMake.Tree("input", { type: "number", min: 1, max: 50, value: this.#currentBrushSize });
brushSizeInput.title = "Define el tamaño del pincel para el boceto.";
brushSizeInput.addEventListener("change", (e) => {
this.#currentBrushSize = parseInt(e.target.value);
this.notify("info", `Tamaño del pincel para boceto cambiado a: ${this.#currentBrushSize}`);
});
row.appendAll(sketchColorLabel, sketchColorInput, brushSizeLabel, brushSizeInput);
}
this.htmlElements.section.appendChild(row);
}
#row4() {
const row = domMake.Row(); // Este row contendrá la etiqueta y el contenedor con scroll
const sketchListContainer = domMake.IconList(); // IconList es un flex container
sketchListContainer.classList.add('nowrap'); // Clase para forzar no-wrap y añadir scroll horizontal
// Crear botones para cada palabra en la base de datos
Object.keys(SKETCH_DATABASE).forEach(word => {
const sketchButton = domMake.Button(word);
sketchButton.title = `Generar boceto para: ${word}`;
sketchButton.style.flex = '0 0 auto'; // Impide que los botones se estiren y ocupen todo el ancho disponible
sketchButton.style.margin = '2px';
sketchButton.style.padding = '2px 5px';
sketchButton.style.fontSize = '0.7em';
sketchButton.addEventListener("click", () => {
this.simulateAISketch(word);
});
sketchListContainer.appendChild(sketchButton);
});
// Añadir la etiqueta y el contenedor de scroll al row principal de esta sección
row.appendAll(domMake.Tree("label", {}, ["Bocetos Rápidos (50):"]), sketchListContainer);
this.htmlElements.section.appendChild(row);
}
simulateAISketch(concept) {
const sketchData = SKETCH_DATABASE[concept];
if (!sketchData) {
this.notify("warning", `Boceto no disponible para: "${concept}".`);
return;
}
this.notify("info", `Generando boceto para: "${concept}" (conceptual).`);
const botManager = this.findGlobal("BotClientManager")?.siblings[0];
if (!botManager || botManager.children.length === 0) {
this.notify("warning", "Se necesita al menos 1 bot activo para generar bocetos.");
return;
}
const activeBot = botManager.children[0].bot; // Usar el primer bot activo
if (!activeBot || !activeBot.getReadyState()) {
this.notify("warning", "El bot seleccionado no está conectado.");
return;
}
this.notify("info", "Dibujando el boceto conceptual con el bot...");
sketchData.forEach(line => {
if (line.type === "circle") {
// Simular un círculo con múltiples líneas pequeñas
const centerX = line.x1;
const centerY = line.y1;
const radius = line.radius;
const segments = 24; // Más segmentos para un círculo más suave
for (let i = 0; i < segments; i++) {
const angle1 = (i / segments) * Math.PI * 2;
const angle2 = ((i + 1) / segments) * Math.PI * 2;
const x1_circ = centerX + radius * Math.cos(angle1);
const y1_circ = centerY + radius * Math.sin(angle1);
const x2_circ = centerX + radius * Math.cos(angle2);
const y2_circ = centerY + radius * Math.sin(angle2);
activeBot.emit("line", -1, x1_circ, y1_circ, x2_circ, y2_circ, true, this.#currentBrushSize, this.#currentSketchColor, false);
}
} else {
activeBot.emit("line", -1, line.x1, line.y1, line.x2, line.y2, true, this.#currentBrushSize, this.#currentSketchColor, false);
}
});
this.notify("success", `Boceto de "${concept}" dibujado. ¡Ahora puedes calcarlo o mejorarlo!`);
}
}
})("QBit");
// Drawing Assistant Module
// This module provides drawing tools like grid, symmetry, and pixel-perfect drawing on an overlay canvas
(function DrawingAssistantModule() {
const QBit = globalThis[arguments[0]]; // Corrected access to QBit
// Styles for the overlay canvas and UI elements
QBit.Styles.addRules([
`#drawing-assistant-overlay {
position: absolute;
top: 0;
left: 0;
z-index: 1000; /* Above game canvas, below main UI elements */
pointer-events: none; /* Crucial to allow clicks to pass through to game canvas */
}`,
`#drawing-assistant-grid-toggle.active { background-color: var(--info); color: white; }`,
`#drawing-assistant-symmetry-toggle.active { background-color: var(--info); color: white; }`,
`#drawing-assistant-pixel-perfect-toggle.active { background-color: var(--info); color: white; }`,
`#drawing-assistant-controls input[type="number"],
#drawing-assistant-controls input[type="color"] {
width: 100%; padding: 5px; box-sizing: border-box;
border: 1px solid var(--CE-color); border-radius: .25rem;
background-color: var(--CE-bg_color); color: var(--CE-color);
}`,
`#drawing-assistant-controls ._row > * { margin: 0 2px; }`
]);
class DrawingAssistant extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#overlayCanvas;
#overlayCtx;
#gameCanvas; // Reference to the actual game canvas
#isSymmetryActive = false;
#isPixelPerfectActive = false;
#isGridVisible = false;
#drawingColor = "#000000"; // Default drawing color
#drawingThickness = 5; // Default drawing thickness
// Event listeners (need to manage their lifecycle)
#mouseMoveListener = null;
#mouseDownListener = null;
#mouseUpListener = null;
constructor() {
super("Asistente de Dibujo", '<i class="fas fa-drafting-compass"></i>');
this.#onStartup();
}
#onStartup() {
this.#findGameCanvas();
this.#setupOverlayCanvas();
this.#loadInterface();
this.#hookGameDrawingEvents();
this.notify("info", "Asistente de Dibujo cargado. Asegúrate de tener un bot activo para usar las herramientas de dibujo.");
}
#findGameCanvas() {
this.#gameCanvas = document.getElementById('canvas');
if (!this.#gameCanvas) {
this.notify("error", "Canvas del juego no encontrado. Algunas funciones de dibujo no estarán disponibles.");
}
}
#setupOverlayCanvas() {
this.#overlayCanvas = domMake.Tree('canvas', { id: 'drawing-assistant-overlay' });
if (this.#gameCanvas) {
this.#overlayCanvas.width = this.#gameCanvas.width;
this.#overlayCanvas.height = this.#gameCanvas.height;
this.#overlayCanvas.style.width = this.#gameCanvas.style.width; // Match CSS size
this.#overlayCanvas.style.height = this.#gameCanvas.style.height;
this.#gameCanvas.parentNode.insertBefore(this.#overlayCanvas, this.#gameCanvas.nextSibling); // Place right after game canvas
} else {
// Fallback dimensions, but core functions won't work well without game canvas
this.#overlayCanvas.width = 1000;
this.#overlayCanvas.height = 1000;
this.#overlayCanvas.style.width = '700px';
this.#overlayCanvas.style.height = '700px';
document.body.appendChild(this.#overlayCanvas);
}
this.#overlayCtx = this.#overlayCanvas.getContext('2d');
this.#updateOverlaySizeAndPosition();
// Handle window resize to keep overlay aligned
window.addEventListener('resize', this.#updateOverlaySizeAndPosition.bind(this));
}
#updateOverlaySizeAndPosition() {
if (!this.#gameCanvas || !this.#overlayCanvas) return;
const gameCanvasRect = this.#gameCanvas.getBoundingClientRect();
this.#overlayCanvas.style.top = `${gameCanvasRect.top}px`;
this.#overlayCanvas.style.left = `${gameCanvasRect.left}px`;
this.#overlayCanvas.style.width = `${gameCanvasRect.width}px`;
this.#overlayCanvas.style.height = `${gameCanvasRect.height}px`;
// If game canvas dimensions change (e.g. initial load), update actual canvas resolution
if (this.#overlayCanvas.width !== this.#gameCanvas.width) {
this.#overlayCanvas.width = this.#gameCanvas.width;
}
if (this.#overlayCanvas.height !== this.#gameCanvas.height) {
this.#overlayCanvas.height = this.#gameCanvas.height;
}
this.#clearOverlay();
if (this.#isGridVisible) {
this.#drawGrid();
}
if (this.#isSymmetryActive) {
this.#drawSymmetryLines();
}
}
#clearOverlay() {
this.#overlayCtx.clearRect(0, 0, this.#overlayCanvas.width, this.#overlayCanvas.height);
}
#loadInterface() {
const container = domMake.Tree("div", { id: "drawing-assistant-controls" });
// Drawing parameters (Color, Thickness)
const paramsRow = domMake.Row();
paramsRow.style.gap = "5px";
const colorInput = domMake.Tree("input", { type: "color", value: this.#drawingColor, title: "Color de Dibujo" });
colorInput.addEventListener("change", (e) => this.#drawingColor = e.target.value);
paramsRow.appendAll(domMake.Tree("label", {}, ["Color:"]), colorInput);
const thicknessInput = domMake.Tree("input", { type: "number", min: "1", max: "100", value: this.#drawingThickness, title: "Grosor de Dibujo" });
thicknessInput.addEventListener("change", (e) => this.#drawingThickness = parseInt(e.target.value));
paramsRow.appendAll(domMake.Tree("label", {}, ["Grosor:"]), thicknessInput);
container.appendChild(paramsRow);
// Geometric Shapes
const shapesRow = domMake.Row();
shapesRow.style.gap = "5px";
const drawLineButton = domMake.Button('<i class="fas fa-grip-lines"></i> Línea');
drawLineButton.addEventListener("click", () => this.#enableShapeDrawing('line'));
shapesRow.appendChild(drawLineButton);
const drawRectButton = domMake.Button('<i class="fas fa-vector-square"></i> Rect.');
drawRectButton.addEventListener("click", () => this.#enableShapeDrawing('rect'));
shapesRow.appendChild(drawRectButton);
const drawCircleButton = domMake.Button('<i class="fas fa-circle"></i> Círculo');
drawCircleButton.addEventListener("click", () => this.#enableShapeDrawing('circle'));
shapesRow.appendChild(drawCircleButton);
container.appendChild(shapesRow);
// Toggles (Grid, Symmetry, Pixel Perfect)
const togglesRow = domMake.Row();
togglesRow.style.gap = "5px";
const toggleGridButton = domMake.Button('<i class="fas fa-th"></i> Cuadrícula');
toggleGridButton.id = "drawing-assistant-grid-toggle";
toggleGridButton.addEventListener("click", () => this.#toggleGrid(toggleGridButton));
togglesRow.appendChild(toggleGridButton);
const toggleSymmetryButton = domMake.Button('<i class="fas fa-arrows-alt-h"></i> Simetría');
toggleSymmetryButton.id = "drawing-assistant-symmetry-toggle";
toggleSymmetryButton.addEventListener("click", () => this.#toggleSymmetry(toggleSymmetryButton));
togglesRow.appendChild(toggleSymmetryButton);
const togglePixelPerfectButton = domMake.Button('<i class="fas fa-compress-arrows-alt"></i> Píxel Perfect');
togglePixelPerfectButton.id = "drawing-assistant-pixel-perfect-toggle";
togglePixelPerfectButton.addEventListener("click", () => this.#togglePixelPerfect(togglePixelPerfectButton));
togglesRow.appendChild(togglePixelPerfectButton);
container.appendChild(togglesRow);
// Collaborative Drawing Trigger
const collabRow = domMake.Row();
const startCollabDrawButton = domMake.Button('<i class="fas fa-users-cog"></i> Iniciar Dibujo Colaborativo');
startCollabDrawButton.title = "Activa el módulo Autodraw V2 para que los bots colaboren en el dibujo (requiere imagen cargada en Autodraw V2).";
startCollabDrawButton.addEventListener("click", () => this.#triggerAutodrawV2Collab());
collabRow.appendChild(startCollabDrawButton);
container.appendChild(collabRow);
this.htmlElements.section.appendChild(container);
}
/**
* Generic helper to get the active bot.
* @returns {object|null} The active bot instance, or null.
*/
#getBot() {
const botManagerClass = this.findGlobal("BotClientManager");
if (!botManagerClass || !botManagerClass.siblings || botManagerClass.siblings.length === 0) {
this.notify("warning", "No hay instancias activas de 'BotClientManager'. Por favor, crea uno desde 'CubeEngine'.");
return null;
}
// Access the first (and likely only) instance of BotClientManager
const botManagerInstance = botManagerClass.siblings[0];
// if (!botManagerInstance || !botManagerInstance.children || botManagerInstance.children.length === 0) {
// this.notify("warning", "No hay bots activos en 'BotClientManager'. Por favor, crea y conecta uno.");
// return null;
// }
const botClientInterfaces = botManagerInstance.children;
let activeBotClientInterface = null;
const selectedBotInput = document.querySelector('input[name="botClient"]:checked');
if (selectedBotInput) {
activeBotClientInterface = botClientInterfaces.find(bci => bci.htmlElements.input === selectedBotInput);
}
// Fallback to the first available bot if no specific bot is selected or found
if (!activeBotClientInterface && botClientInterfaces.length > 0) {
activeBotClientInterface = botClientInterfaces[0]; // Corrected to get the first instance
this.notify("info", `No se seleccionó un bot. Usando el primer bot disponible: ${activeBotClientInterface.getName()}.`);
}
// if (!activeBotClientInterface || !activeBotClientInterface.bot || !activeBotClientInterface.bot.getReadyState()) {
// this.notify("warning", `El bot "${activeBotClientInterface ? activeBotClientInterface.getName() : 'desconocido'}" no está conectado y listo para enviar comandos.`);
// return null;
// }
return activeBotClientInterface.bot;
}
#hookGameDrawingEvents() {
if (!this.#gameCanvas) return;
// Remove existing listeners to prevent duplicates
this.#gameCanvas.removeEventListener('mousedown', this.#mouseDownListener);
this.#gameCanvas.removeEventListener('mousemove', this.#mouseMoveListener);
this.#gameCanvas.removeEventListener('mouseup', this.#mouseUpListener);
this.#gameCanvas.removeEventListener('mouseout', this.#mouseUpListener);
let isDrawing = false;
let lastX = 0, lastY = 0;
const sendLineCommand = (startX, startY, endX, endY) => {
const bot = this.#getBot();
if (!bot) return;
// Scale coordinates from canvas pixels to game's 0-100 range
const rect = this.#gameCanvas.getBoundingClientRect();
const scaleX = 100 / rect.width;
const scaleY = 100 / rect.height;
let gameX1 = startX * scaleX;
let gameY1 = startY * scaleY;
let gameX2 = endX * scaleX;
let gameY2 = endY * scaleY;
// Apply Pixel Perfect snapping if active
if (this.#isPixelPerfectActive) {
const snapSize = 2; // Snap to 2% grid, adjust as needed
gameX1 = Math.round(gameX1 / snapSize) * snapSize;
gameY1 = Math.round(gameY1 / snapSize) * snapSize;
gameX2 = Math.round(gameX2 / snapSize) * snapSize;
gameY2 = Math.round(gameY2 / snapSize) * snapSize;
}
// Ensure coordinates are within 0-100 bounds
gameX1 = Math.max(0, Math.min(100, gameX1));
gameY1 = Math.max(0, Math.min(100, gameY1));
gameX2 = Math.max(0, Math.min(100, gameX2));
gameY2 = Math.max(0, Math.min(100, gameY2));
bot.emit("line", -1, gameX1, gameY1, gameX2, gameY2, true, this.#drawingThickness, this.#drawingColor, false);
// If symmetry is active, send mirrored commands
if (this.#isSymmetryActive) {
const midX = 50; // Center of the canvas (game's 0-100 scale)
const mirroredGameX1 = midX + (midX - gameX1);
const mirroredGameX2 = midX + (midX - gameX2);
bot.emit("line", -1, mirroredGameX1, gameY1, mirroredGameX2, gameY2, true, this.#drawingThickness, this.#drawingColor, false);
}
};
this.#mouseDownListener = (e) => {
isDrawing = true;
const rect = this.#gameCanvas.getBoundingClientRect();
lastX = e.clientX - rect.left;
lastY = e.clientY - rect.top;
// Start a new path on overlay if drawing for visualization
this.#overlayCtx.beginPath();
this.#overlayCtx.moveTo(lastX, lastY);
};
this.#mouseMoveListener = (e) => {
if (!isDrawing) return;
const rect = this.#gameCanvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
sendLineCommand(lastX, lastY, currentX, currentY);
// Draw on overlay for visual feedback
this.#overlayCtx.lineTo(currentX, currentY);
this.#overlayCtx.strokeStyle = this.#drawingColor;
this.#overlayCtx.lineWidth = this.#drawingThickness;
this.#overlayCtx.lineCap = 'round';
this.#overlayCtx.lineJoin = 'round';
this.#overlayCtx.stroke();
this.#overlayCtx.beginPath(); // Start new path to prevent connecting old points
this.#overlayCtx.moveTo(currentX, currentY);
lastX = currentX;
lastY = currentY;
};
this.#mouseUpListener = () => {
isDrawing = false;
this.#clearOverlay(); // Clear temporary drawing from overlay
if (this.#isGridVisible) this.#drawGrid(); // Redraw static guides
if (this.#isSymmetryActive) this.#drawSymmetryLines(); // Redraw static guides
};
this.#gameCanvas.addEventListener('mousedown', this.#mouseDownListener);
this.#gameCanvas.addEventListener('mousemove', this.#mouseMoveListener);
this.#gameCanvas.addEventListener('mouseup', this.#mouseUpListener);
this.#gameCanvas.addEventListener('mouseout', this.#mouseUpListener); // Stop drawing if mouse leaves canvas
}
// --- Geometric Shape Drawing ---
#enableShapeDrawing(shapeType) {
this.notify("info", `Modo '${shapeType}' activado. Haz clic en el lienzo para dibujar.`);
const bot = this.#getBot();
if (!bot) return;
let startCoords = null;
let currentShapeOverlayListener = null; // Renamed to avoid confusion with `currentShapeOverlay` variable
const drawShapePreview = (x1, y1, x2, y2) => {
this.#clearOverlay(); // Clear previous preview
if (this.#isGridVisible) this.#drawGrid(); // Redraw static guides
if (this.#isSymmetryActive) this.#drawSymmetryLines();
const ctx = this.#overlayCtx;
ctx.strokeStyle = this.#drawingColor;
ctx.lineWidth = 1; // Thinner for preview
ctx.setLineDash([5, 5]); // Dashed line for preview
ctx.beginPath();
const width = x2 - x1;
const height = y2 - y1;
if (shapeType === 'line') {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
} else if (shapeType === 'rect') {
ctx.rect(x1, y1, width, height);
} else if (shapeType === 'circle') {
// Calculate radius, ensuring it's not negative
const dx = x2 - x1;
const dy = y2 - y1;
const radius = Math.sqrt(dx * dx + dy * dy);
ctx.arc(x1, y1, radius, 0, 2 * Math.PI);
}
ctx.stroke();
ctx.setLineDash([]); // Reset line dash after drawing preview
};
const onClick = (e) => {
const rect = this.#gameCanvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
if (!startCoords) {
startCoords = { x: currentX, y: currentY };
this.notify("info", "Haz clic de nuevo para definir el final de la forma.");
// Start live preview on mousemove
currentShapeOverlayListener = (moveEvent) => {
const moveRect = this.#gameCanvas.getBoundingClientRect();
const moveX = moveEvent.clientX - moveRect.left;
const moveY = moveEvent.clientY - moveRect.top;
drawShapePreview(startCoords.x, startCoords.y, moveX, moveY);
};
this.#gameCanvas.addEventListener('mousemove', currentShapeOverlayListener);
} else {
// Second click: draw the shape
const bot = this.#getBot();
if (!bot) { // Check if bot is still available
this.notify("warning", "Bot no disponible, no se puede dibujar la forma.");
this.#gameCanvas.removeEventListener('click', onClick);
this.#gameCanvas.removeEventListener('mousemove', currentShapeOverlayListener);
this.#clearOverlay();
return;
}
const scaleX = 100 / rect.width;
const scaleY = 100 / rect.height;
const x1 = startCoords.x * scaleX;
const y1 = startCoords.y * scaleY;
const x2 = currentX * scaleX;
const y2 = currentY * scaleY;
const thickness = this.#drawingThickness;
const color = this.#drawingColor;
if (shapeType === 'line') {
bot.emit("line", -1, x1, y1, x2, y2, true, thickness, color, false);
} else if (shapeType === 'rect') {
// Draw 4 lines for rectangle
bot.emit("line", -1, x1, y1, x2, y1, true, thickness, color, false); // Top
bot.emit("line", -1, x2, y1, x2, y2, true, thickness, color, false); // Right
bot.emit("line", -1, x2, y2, x1, y2, true, thickness, color, false); // Bottom
bot.emit("line", -1, x1, y2, x1, y1, true, thickness, color, false); // Left
} else if (shapeType === 'circle') {
const centerX = x1;
const centerY = y1;
const dx = x2 - x1;
const dy = y2 - y1;
const radius = Math.sqrt(dx * dx + dy * dy); // Radius in game coordinates (0-100)
const segments = 48; // More segments for a smoother circle
for (let i = 0; i < segments; i++) {
const angle1 = (i / segments) * Math.PI * 2;
const angle2 = ((i + 1) / segments) * Math.PI * 2;
const cx1 = centerX + radius * Math.cos(angle1);
const cy1 = centerY + radius * Math.sin(angle1);
const cx2 = centerX + radius * Math.cos(angle2);
const cy2 = centerY + radius * Math.sin(angle2);
bot.emit("line", -1, cx1, cy1, cx2, cy2, true, thickness, color, false);
}
}
// Clean up and disable shape drawing
this.#gameCanvas.removeEventListener('click', onClick);
this.#gameCanvas.removeEventListener('mousemove', currentShapeOverlayListener);
this.#clearOverlay(); // Clear final preview
this.notify("success", `${shapeType} dibujado.`);
startCoords = null; // Reset for next shape
}
};
this.#gameCanvas.addEventListener('click', onClick);
}
// --- Grid Overlay ---
#toggleGrid(button) {
this.#isGridVisible = !this.#isGridVisible;
button.classList.toggle("active", this.#isGridVisible);
this.#clearOverlay();
if (this.#isGridVisible) {
this.#drawGrid();
this.notify("info", "Cuadrícula visible.");
} else {
this.notify("info", "Cuadrícula oculta.");
}
// Redraw symmetry lines if active
if (this.#isSymmetryActive) this.#drawSymmetryLines();
}
#drawGrid() {
if (!this.#gameCanvas) return;
const ctx = this.#overlayCtx;
const rect = this.#gameCanvas.getBoundingClientRect();
const cellSize = 50; // px, adjust for desired grid density
ctx.strokeStyle = "rgba(100, 100, 100, 0.5)";
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]); // Dashed grid lines
// Vertical lines
for (let x = 0; x <= rect.width; x += cellSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, rect.height);
ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= rect.height; y += cellSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(rect.width, y);
ctx.stroke();
}
ctx.setLineDash([]); // Reset line dash
}
// --- Symmetry Mode ---
#toggleSymmetry(button) {
this.#isSymmetryActive = !this.#isSymmetryActive;
button.classList.toggle("active", this.#isSymmetryActive);
this.#clearOverlay();
if (this.#isGridVisible) this.#drawGrid(); // Redraw grid if active
if (this.#isSymmetryActive) {
this.#drawSymmetryLines();
this.notify("info", "Modo Simetría Activo.");
} else {
this.notify("info", "Modo Simetría Inactivo.");
}
}
#drawSymmetryLines() {
if (!this.#gameCanvas) return;
const ctx = this.#overlayCtx;
const rect = this.#gameCanvas.getBoundingClientRect();
// Center vertical line
ctx.strokeStyle = "rgba(255, 0, 0, 0.7)"; // Red for symmetry line
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]); // Dashed line
ctx.beginPath();
ctx.moveTo(rect.width / 2, 0);
ctx.lineTo(rect.width / 2, rect.height);
ctx.stroke();
ctx.setLineDash([]); // Reset line dash
}
// --- Pixel Perfect / Snap to Grid Drawing ---
#togglePixelPerfect(button) {
this.#isPixelPerfectActive = !this.#isPixelPerfectActive;
button.classList.toggle("active", this.#isPixelPerfectActive);
this.notify("info", `Modo 'Píxel Perfect' ${this.#isPixelPerfectActive ? 'Activado' : 'Desactivado'}.`);
}
// --- Collaborative Drawing Trigger ---
#triggerAutodrawV2Collab() {
const autodrawV2Class = this.findGlobal("AutodrawV2");
if (!autodrawV2Class || !autodrawV2Class.siblings || autodrawV2Class.siblings.length === 0) {
this.notify("warning", "El módulo 'Autodraw V2' no está activo o no se encontró. No se puede iniciar el dibujo colaborativo.");
return;
}
const autodrawV2Instance = autodrawV2Class.siblings[0];
if (autodrawV2Instance && typeof autodrawV2Instance.startDrawing === 'function') {
// Assuming AutodrawV2.startDrawing handles bot distribution internally
autodrawV2Instance.startDrawing();
this.notify("info", "Iniciando dibujo colaborativo a través del módulo Autodraw V2.");
} else {
this.notify("warning", "La instancia del módulo 'Autodraw V2' no está lista. Asegúrate de que Autodraw V2 se inicializó correctamente.");
}
}
}
})("QBit");
// --- START NEW MODULE: PaletteMaster (CORREGIDO: Event Handlers para StrokeMaster) ---
(function PaletteMasterModule() {
const QBit = globalThis[arguments[0]];
const LOCAL_STORAGE_KEY = 'cubicEngineCustomColors';
const DEFAULT_CUSTOM_COLORS = [
{ name: "Teal", hex: "#008080" }, { name: "Lime", hex: "#AAFF00" },
{ name: "Cyan", hex: "#00FFFF" }, { name: "Magenta", hex: "#FF00FF" },
{ name: "Olive", hex: "#808000" }, { name: "Maroon", hex: "#800000" }
];
QBit.Styles.addRules([
// General section styling
`#${QBit.identifier} .palette-master-section {
border: 1px solid var(--CE-color);
border-radius: .25rem;
padding: 5px;
margin-bottom: 10px;
background-color: var(--CE-bg_color);
}`,
`#${QBit.identifier} .palette-master-section-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--dark-blue-title);
text-align: center;
}`,
// MoreColorPalettes specific
`#${QBit.identifier} .custom-color-button {
box-shadow: 0 0 2px rgba(0,0,0,0.3);
cursor: pointer;
border: 1px solid transparent;
}`,
`#${QBit.identifier} .custom-color-button.custom-active-color {
box-shadow: 0 0 5px 2px var(--info);
border: 1px solid var(--info);
}`,
// StrokeMaster specific
`#${QBit.identifier} .stroke-master-toggle-button.active {
background-color: var(--info);
color: white;
}`,
`#${QBit.identifier} .stroke-master-control-group {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(0,0,0,0.1);
}`,
`#${QBit.identifier} .stroke-master-control-group > div {
flex: 1 1 48%; /* For responsiveness */
display: flex;
flex-direction: column;
align-items: flex-start;
}`,
`#${QBit.identifier} .stroke-master-control-group input[type="number"],
#${QBit.identifier} .stroke-master-control-group input[type="range"] {
width: 100%;
}`
]);
class PaletteMaster extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
// MoreColorPalettes properties
#customColors = [];
#colorButtonsContainer;
#colorInput;
#colorPaletteObserver;
#gameTriangleElement = null;
#proxyGameButton = null;
// StrokeMaster properties
#isPressureActive = false;
#isTextureActive = false;
#lastMousePos = { x: 0, y: 0 };
#lastTimestamp = 0;
#lastDrawThickness = 5;
// MODIFICADO: Declarar los handlers como propiedades de la clase, enlazadas una única vez
#boundMouseDownHandler;
#boundMouseMoveHandler;
#boundMouseUpHandler; // This will also handle mouseout
constructor() {
super("Maestro de Paletas", '<i class="fas fa-brush"></i>');
// MODIFICADO: Enlazar los handlers una única vez en el constructor
this.#boundMouseDownHandler = this.#handleMouseDown.bind(this);
this.#boundMouseMoveHandler = this.#handleMouseMove.bind(this);
this.#boundMouseUpHandler = this.#handleMouseUp.bind(this);
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#loadCustomColors();
this.#setupColorPaletteObserver();
this.#hookDrawingEvents(); // Attach event listeners
}
#loadInterface() {
const container = domMake.Tree("div");
// --- Section: Gestión de Paletas ---
const paletteSection = domMake.Tree("div", { class: "palette-master-section" });
paletteSection.appendChild(domMake.Tree("div", { style: "width: 100%; text-align: center; font-weight: bold; margin-bottom: 5px;" }, ["Gestión de Paletas"]));
const addColorRow = domMake.Row();
const addColorLabel = domMake.Tree("label", {}, ["Añadir Color:"]);
this.#colorInput = domMake.Tree("input", { type: "color", value: "#FF0000" });
const addColorButton = domMake.Button("Añadir");
addColorButton.addEventListener("click", () => this.#addNewCustomColor(this.#colorInput.value));
addColorRow.appendAll(addColorLabel, this.#colorInput, addColorButton);
paletteSection.appendChild(addColorRow);
const clearColorsRow = domMake.Row();
const clearAllColorsButton = domMake.Button("Limpiar Todos");
clearAllColorsButton.addEventListener("click", () => this.#clearAllCustomColors());
clearColorsRow.appendChild(clearAllColorsButton);
paletteSection.appendChild(clearColorsRow);
const customColorsDisplayRow = domMake.Row();
this.#colorButtonsContainer = domMake.IconList();
customColorsDisplayRow.appendChild(this.#colorButtonsContainer);
paletteSection.appendChild(customColorsDisplayRow);
container.appendChild(paletteSection);
// --- Section: Herramientas de Trazo ---
const strokeSection = domMake.Tree("div", { class: "palette-master-section" });
strokeSection.appendChild(domMake.Tree("div", { class: "palette-master-section-title" }, [""]));
// Pressure Control
const pressureRow = domMake.Row();
const pressureButton = domMake.Button("Control de Presión");
pressureButton.classList.add("stroke-master-toggle-button");
pressureButton.addEventListener("click", () => this.#togglePressureControl(pressureButton));
// pressureRow.appendChild(pressureButton);
strokeSection.appendChild(pressureRow);
// Texture Brush
const textureRow = domMake.Row();
const textureButton = domMake.Button("Pincel Texturizado");
textureButton.classList.add("stroke-master-toggle-button");
textureButton.addEventListener("click", () => this.#toggleTextureBrush(textureButton));
//s textureRow.appendChild(textureButton);
strokeSection.appendChild(textureRow);
// Gradient Fills (conceptual buttons)
const gradientGroup = domMake.Tree("div", { class: "stroke-master-control-group" });
gradientGroup.appendChild(domMake.Tree("label", { style: "width: 100%; text-align: center; font-weight: bold; margin-bottom: 5px;" }, ["Rellenos Degradados"]));
const diamondGradientButton = domMake.Button('<i class="fas fa-gem"></i> Degradado Diamante');
diamondGradientButton.addEventListener("click", () => this.#simulateGradientFill("diamond"));
gradientGroup.appendChild(domMake.Tree("div", {}, [diamondGradientButton]));
const radialGradientButton = domMake.Button('<i class="fas fa-bullseye"></i> Degradado Radial');
radialGradientButton.addEventListener("click", () => this.#simulateGradientFill("radial"));
gradientGroup.appendChild(domMake.Tree("div", {}, [radialGradientButton]));
const linearGradientButton = domMake.Button('<i class="fas fa-grip-lines"></i> Degradado Lineal');
linearGradientButton.addEventListener("click", () => this.#simulateGradientFill("linear"));
gradientGroup.appendChild(domMake.Tree("div", {}, [linearGradientButton]));
const verticalGradientButton = domMake.Button('<i class="fas fa-arrows-alt-v"></i> Degradado Vertical');
verticalGradientButton.addEventListener("click", () => this.#simulateGradientFill("vertical"));
gradientGroup.appendChild(domMake.Tree("div", {}, [verticalGradientButton]));
const conicalGradientButton = domMake.Button('<i class="fas fa-circle-notch"></i> Degradado Cónico');
conicalGradientButton.addEventListener("click", () => this.#simulateGradientFill("conical"));
gradientGroup.appendChild(domMake.Tree("div", {}, [conicalGradientButton]));
const waveGradientButton = domMake.Button('<i class="fas fa-water"></i> Degradado Ondulado');
waveGradientButton.addEventListener("click", () => this.#simulateGradientFill("wave"));
gradientGroup.appendChild(domMake.Tree("div", {}, [waveGradientButton]));
strokeSection.appendChild(gradientGroup);
container.appendChild(strokeSection);
this.htmlElements.section.appendChild(container);
}
// --- MoreColorPalettes Methods ---
#loadCustomColors() {
try {
const storedColors = localStorage.getItem(LOCAL_STORAGE_KEY);
this.#customColors = storedColors ? JSON.parse(storedColors) : [...DEFAULT_CUSTOM_COLORS];
} catch (e) {
this.notify("error", `Error al cargar colores: ${e.message}. Usando colores por defecto.`);
this.#customColors = [...DEFAULT_CUSTOM_COLORS];
}
this.#renderCustomColorButtons();
}
#saveCustomColors() {
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.#customColors));
this.notify("success", "Colores personalizados guardados.");
} catch (e) {
this.notify("error", `Error al guardar colores: ${e.message}`);
}
}
#renderCustomColorButtons() {
this.#colorButtonsContainer.innerHTML = '';
this.#customColors.forEach(color => {
this.#createColorButton(color.hex, color.name);
});
}
#addNewCustomColor(hexColor) {
if (this.#customColors.some(color => color.hex.toLowerCase() === hexColor.toLowerCase())) {
this.notify("info", `El color ${hexColor} ya existe en tu paleta.`);
return;
}
const newColor = { name: `Custom-${hexColor}`, hex: hexColor };
this.#customColors.push(newColor);
this.#saveCustomColors();
this.#createColorButton(newColor.hex, newColor.name);
this.notify("info", `Color ${hexColor} añadido.`);
}
addCustomColorFromExternal(hexColor) { // Public method for other modules
this.#addNewCustomColor(hexColor);
}
#clearAllCustomColors() {
if (confirm("¿Estás seguro de que quieres eliminar todos los colores personalizados?")) {
this.#customColors = [...DEFAULT_CUSTOM_COLORS];
this.#saveCustomColors();
this.#renderCustomColorButtons();
this.notify("info", "Colores personalizados reiniciados a los valores por defecto.");
}
}
#createColorButton(hexColor, name) {
const newButton = domMake.Tree("div", {
class: "drawcontrols-button drawcontrols-color custom-color-button",
style: `background-color: ${hexColor};`,
title: name,
"data-hex": hexColor
});
newButton.addEventListener('click', (event) => this.#handleCustomColorClick(newButton));
this.#colorButtonsContainer.appendChild(newButton);
}
#findGameElementsForColorPalette() {
if (!this.#gameTriangleElement || !document.body.contains(this.#gameTriangleElement)) {
this.#gameTriangleElement = document.getElementById('colorpicker-cursor');
}
if (!this.#proxyGameButton || !document.body.contains(this.#proxyGameButton)) {
const drawControls = document.getElementById('drawcontrols-colors') || document.getElementById('drawcontrols');
if (drawControls) {
this.#proxyGameButton = drawControls.querySelector('.drawcontrols-button.drawcontrols-color:not(.drawcontrols-colorpicker):not(.custom-color-button)');
}
}
}
#handleCustomColorClick(clickedButton) {
this.#findGameElementsForColorPalette();
if (!this.#proxyGameButton) {
this.notify("warning", "No se encontró un botón de color de juego para proxy. La funcionalidad de paleta puede ser limitada.");
return;
}
const customColor = clickedButton.dataset.hex;
const originalProxyColor = this.#proxyGameButton.style.backgroundColor;
this.#proxyGameButton.style.backgroundColor = customColor;
this.#proxyGameButton.click();
requestAnimationFrame(() => {
this.#proxyGameButton.style.backgroundColor = originalProxyColor;
this.#updateTrianglePosition(clickedButton);
document.querySelectorAll('.custom-color-button.custom-active-color').forEach(btn => {
btn.classList.remove('custom-active-color');
});
clickedButton.classList.add('custom-active-color');
});
}
#updateTrianglePosition(targetButton) {
const triangle = this.#gameTriangleElement;
if (!triangle || !targetButton) return;
const buttonContainer = document.getElementById('drawcontrols-colors') || document.getElementById('drawcontrols');
if (!buttonContainer) return;
const buttonRect = targetButton.getBoundingClientRect();
const containerRect = buttonContainer.getBoundingClientRect();
const buttonCenterRelativeToContainer = (buttonRect.left - containerRect.left) + (buttonRect.width / 2);
const triangleWidth = triangle.offsetWidth || 8;
const newLeft = buttonCenterRelativeToContainer - (triangleWidth / 2);
triangle.style.left = `${newLeft}px`;
}
#setupColorPaletteObserver() {
const observerTarget = document.getElementById('drawcontrols-colors') || document.getElementById('drawcontrols');
if (!observerTarget) {
this.notify("warning", "Contenedor de controles de dibujo no encontrado. Los colores personalizados pueden no funcionar bien.");
setTimeout(() => this.#setupColorPaletteObserver(), 1000);
return;
}
this.#colorPaletteObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
this.#addListenersToGameColorButtons();
}
});
});
this.#colorPaletteObserver.observe(observerTarget, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] });
this.#addListenersToGameColorButtons();
}
#addListenersToGameColorButtons() {
this.#findGameElementsForColorPalette();
const gameColorButtons = document.querySelectorAll('.drawcontrols-button.drawcontrols-color:not(.custom-color-button)');
gameColorButtons.forEach(gameBtn => {
gameBtn.removeEventListener('click', this.#handleGameColorClick);
gameBtn.addEventListener('click', this.#handleGameColorClick.bind(this));
});
}
#handleGameColorClick() {
document.querySelectorAll('.custom-color-button.custom-active-color').forEach(customBtn => {
customBtn.classList.remove('custom-active-color');
});
}
// --- StrokeMaster Methods ---
#getBot() {
const botManagerClass = this.findGlobal("BotClientManager");
if (!botManagerClass || !botManagerClass.siblings || botManagerClass.siblings.length === 0) {
this.notify("warning", "No hay instancias activas de 'BotClientManager'. Por favor, crea uno desde 'CubeEngine'.");
return null;
}
const botManagerInstance = botManagerClass.siblings[0];
const botClientInterfaces = botManagerInstance.children;
let activeBotClientInterface = null;
const selectedBotInput = document.querySelector('input[name="botClient"]:checked');
if (selectedBotInput) {
activeBotClientInterface = botClientInterfaces.find(bci => bci.htmlElements.input === selectedBotInput);
}
if (!activeBotClientInterface && botClientInterfaces.length > 0) {
activeBotClientInterface = botClientInterfaces[0];
this.notify("info", `No se seleccionó un bot. Usando el primer bot disponible: ${activeBotClientInterface.getName()}.`);
}
if (!activeBotClientInterface || !activeBotClientInterface.bot || !activeBotClientInterface.bot.getReadyState()) {
this.notify("warning", `El bot "${activeBotClientInterface ? activeBotClientInterface.getName() : 'desconocido'}" no está conectado y listo para enviar comandos.`);
return null;
}
return activeBotClientInterface.bot;
}
#hookDrawingEvents() {
const gameCanvas = document.querySelector("#canvas");
if (!gameCanvas) return;
// Remove previous listeners to prevent duplicates
gameCanvas.removeEventListener("mousedown", this.#boundMouseDownHandler);
gameCanvas.removeEventListener("mousemove", this.#boundMouseMoveHandler);
gameCanvas.removeEventListener("mouseup", this.#boundMouseUpHandler);
gameCanvas.removeEventListener("mouseout", this.#boundMouseUpHandler); // mouseout also stops drawing
// Attach listeners using the pre-bound handlers
gameCanvas.addEventListener("mousedown", this.#boundMouseDownHandler);
gameCanvas.addEventListener("mousemove", this.#boundMouseMoveHandler);
gameCanvas.addEventListener("mouseup", this.#boundMouseUpHandler);
gameCanvas.addEventListener("mouseout", this.#boundMouseUpHandler);
}
// MODIFICADO: Handlers ahora son métodos de la clase
#handleMouseDown(e) {
this.isDrawingLocal = true;
const rect = e.target.getBoundingClientRect(); // Use e.target for robustness
this.#lastMousePos.x = ((e.clientX - rect.left) / rect.width) * 100;
this.#lastMousePos.y = ((e.clientY - rect.top) / rect.height) * 100;
this.#lastTimestamp = performance.now();
}
#handleMouseMove(e) {
if (!this.isDrawingLocal) return;
const rect = e.target.getBoundingClientRect();
const currentX = ((e.clientX - rect.left) / rect.width) * 100;
const currentY = ((e.clientY - rect.top) / rect.height) * 100;
let currentThickness = this.#lastDrawThickness;
let currentColor = this.#getCurrentBrushColor();
if (this.#isPressureActive) {
const currentTimestamp = performance.now();
const dx = e.clientX - this.#lastMousePos.x;
const dy = e.clientY - this.#lastMousePos.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const timeDelta = currentTimestamp - this.#lastTimestamp;
const speed = distance / timeDelta;
const minThickness = 2;
const maxThickness = 20;
const speedFactor = 0.5;
currentThickness = maxThickness - (speed * speedFactor);
currentThickness = Math.max(minThickness, Math.min(maxThickness, currentThickness));
this.#lastDrawThickness = currentThickness;
}
if (this.#isTextureActive) {
currentColor = this.#applyColorNoise(currentColor, 10);
}
const bot = this.#getBot();
if (bot && bot.getReadyState()) {
bot.emit("line", -1, this.#lastMousePos.x, this.#lastMousePos.y, currentX, currentY, true, currentThickness, currentColor, false);
}
this.#lastMousePos.x = currentX;
this.#lastMousePos.y = currentY;
this.#lastTimestamp = performance.now();
}
#handleMouseUp() {
this.isDrawingLocal = false;
this.#lastDrawThickness = 5;
}
#getCurrentBrushColor() {
const colorPicker = document.querySelector('.drawcontrols-color.active');
if (colorPicker) {
const rgb = colorPicker.style.backgroundColor;
if (rgb) return rgb;
}
return "#000000";
}
#applyColorNoise(color, noiseAmount) {
let r, g, b;
if (color.startsWith("rgb")) {
const parts = color.match(/\d+/g).map(Number);
r = parts[0]; g = parts[1]; b = parts[2];
} else if (color.startsWith("#")) {
const hex = color.slice(1);
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
} else {
return color;
}
const addNoise = (value) => {
const noise = (Math.random() - 0.5) * 2 * noiseAmount;
return Math.max(0, Math.min(255, Math.floor(value + noise)));
};
return `rgb(${addNoise(r)},${addNoise(g)},${addNoise(b)})`;
}
#togglePressureControl(button) {
this.#isPressureActive = !this.#isPressureActive;
button.classList.toggle("active", this.#isPressureActive);
button.textContent = this.#isPressureActive ? "Control de Presión Activo" : "Control de Presión";
this.notify("info", `Control de Presión ${this.#isPressureActive ? 'activado' : 'desactivado'}.`);
}
#toggleTextureBrush(button) {
this.#isTextureActive = !this.#isTextureActive;
button.classList.toggle("active", this.#isTextureActive);
button.textContent = this.#isTextureActive ? "Pincel Texturizado Activo" : "Pincel Texturizado";
this.notify("info", `Pincel Texturizado ${this.#isTextureActive ? 'activado' : 'desactivado'}.`);
}
async #simulateGradientFill(type) {
this.notify("info", `Simulando relleno degradado tipo '${type}' (conceptual).`);
const bot = this.#getBot();
if (!bot) return;
const startX = 20, endX = 80;
const startY = 20, endY = 80;
const steps = 20;
const thickness = 25;
const delayMs = 50;
for (let i = 0; i <= steps; i++) {
const ratio = i / steps;
let r, g, b;
let currentColor;
switch (type) {
case "diamond": { // <-- Añadido bloque
r = Math.floor(0 + (255 - 0) * (1 - ratio));
g = Math.floor(0 + (215 - 0) * (1 - ratio));
b = Math.floor(139 + (0 - 139) * (1 - ratio));
currentColor = `rgb(${r},${g},${b})`;
const currentDistance = 40 * ratio;
const centerX = 50, centerY = 50;
const p1x = centerX, p1y = centerY - currentDistance;
const p2x = centerX + currentDistance, p2y = centerY;
const p3x = centerX, p3y = centerY + currentDistance;
const p4x = centerX - currentDistance, p4y = centerY;
bot.emit("line", -1, p1x, p1y, p2x, p2y, true, thickness, currentColor, false);
bot.emit("line", -1, p2x, p2y, p3x, p3y, true, thickness, currentColor, false);
bot.emit("line", -1, p3x, p3y, p4x, p4y, true, thickness, currentColor, false);
bot.emit("line", -1, p4x, p4y, p1x, p1y, true, thickness, currentColor, false);
break;
} // <-- Cierre de bloque
case "radial": { // <-- Añadido bloque
r = 255;
g = Math.floor(165 + (255 - 165) * (1 - ratio));
b = 0;
currentColor = `rgb(${r},${g},${b})`;
const currentRadius = 30 * ratio;
const numSegments = 36;
for (let j = 0; j < numSegments; j++) {
const angle = (j / numSegments) * 2 * Math.PI;
const x = 50 + currentRadius * Math.cos(angle);
const y = 50 + currentRadius * Math.sin(angle);
// Cambiado ligeramente para que simule mejor un punto con una línea muy pequeña
bot.emit("line", -1, x, y, x + 0.1, y + 0.1, true, thickness, currentColor, false);
}
break;
} // <-- Cierre de bloque
case "linear": { // <-- Añadido bloque
r = Math.floor(255 * (1 - ratio));
g = 0;
b = Math.floor(255 * ratio);
currentColor = `rgb(${r},${g},${b})`;
const currentY = startY + (endY - startY) * ratio;
bot.emit("line", -1, startX, currentY, endX, currentY, true, thickness, currentColor, false);
break;
} // <-- Cierre de bloque
case "vertical": { // <-- Añadido bloque
r = Math.floor(128 + (255 - 128) * ratio);
g = Math.floor(0 + (192 - 0) * ratio);
b = Math.floor(128 + (203 - 128) * ratio);
currentColor = `rgb(${r},${g},${b})`;
const vertY = startY + (endY - startY) * ratio;
bot.emit("line", -1, startX, vertY, endX, vertY, true, thickness, currentColor, false);
break;
} // <-- Cierre de bloque
case "conical": { // <-- Añadido bloque
r = Math.floor(0 + (255 - 0) * ratio);
g = 0;
b = 255;
currentColor = `rgb(${r},${g},${b})`;
const angle = (ratio * 2 * Math.PI);
const radius = 40;
const cx = 50, cy = 50;
const x2 = cx + radius * Math.cos(angle);
const y2 = cy + radius * Math.sin(angle);
bot.emit("line", -1, cx, cy, x2, y2, true, thickness, currentColor, false);
break;
} // <-- Cierre de bloque
case "wave": { // <-- Añadido bloque
r = Math.floor(0 + (255 - 0) * ratio);
g = Math.floor(255 + (127 - 255) * ratio);
b = Math.floor(255 + (80 - 255) * ratio);
currentColor = `rgb(${r},${g},${b})`;
const waveAmplitude = 10;
const waveFrequency = 0.1;
const wavyY = startY + (endY - startY) * ratio + waveAmplitude * Math.sin(ratio * Math.PI * 2 * waveFrequency);
bot.emit("line", -1, startX, wavyY, endX, wavyY, true, thickness, currentColor, false);
break;
} // <-- Cierre de bloque
}
await new Promise(resolve => setTimeout(resolve, delayMs));
}
this.notify("success", `Degradado tipo '${type}' dibujado.`);
}
}
})("QBit");
// --- END NEW MODULE: PaletteMaster ---
// --- START NEW MODULE: SwarmCommander (COMBINED MODULE: TacticalBotSwarm + BugGeneratorModule) ---
(function SwarmCommanderModule() {
const QBit = globalThis[arguments[0]];
QBit.Styles.addRules([
`#${QBit.identifier} .swarm-commander-section {
border: 1px solid var(--CE-color);
border-radius: .25rem;
padding: 5px;
margin-bottom: 10px;
background-color: var(--CE-bg_color);
}`,
`#${QBit.identifier} .swarm-commander-section-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--dark-blue-title);
text-align: center;
}`,
`#${QBit.identifier} .swarm-commander-toggle-button.active {
background-color: var(--info);
color: white;
}`,
`#${QBit.identifier} .swarm-commander-control-group {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(0,0,0,0.1);
}`,
`#${QBit.identifier} .swarm-commander-control-group > div {
flex: 1 1 48%; /* For responsiveness */
display: flex;
flex-direction: column;
align-items: flex-start;
}`,
`#${QBit.identifier} .swarm-commander-control-group input,
#${QBit.identifier} .swarm-commander-control-group select {
width: 100%;
}`
]);
class SwarmCommander extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
// BugGeneratorModule properties
#lagInterval = null;
#secretSpamInterval = null;
#bugExperienceInterval = null; // Renamed to avoid generic 'bugInterval'
#playerChaosInterval = null;
#visualGlitchInterval = null;
constructor() {
super("Manipulacion avanzada de Bots", '<i class="fas fa-gamepad"></i>'); // New icon
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
const container = domMake.Tree("div");
// --- Section: Tácticas de Enjambre (TacticalBotSwarm) ---
const swarmTacticsSection = domMake.Tree("div", { class: "swarm-commander-section" });
swarmTacticsSection.appendChild(domMake.Tree("div", { style: "width: 100%; text-align: center; font-weight: bold; margin-bottom: 5px;" }, ["Modos de bot"]));
// Collaborative Drawing
const collaborativeDrawRow = domMake.Row();
const collaborativeDrawButton = domMake.Button("Dibujo Colaborativo");
collaborativeDrawButton.title = "Divide el lienzo en zonas para que cada bot dibuje una parte (usa Autodraw V2 para cargar imagen).";
collaborativeDrawButton.addEventListener("click", () => this.#startCollaborativeDrawing());
collaborativeDrawRow.appendChild(collaborativeDrawButton);
swarmTacticsSection.appendChild(collaborativeDrawRow);
// Smart Guessing
const smartGuessRow = domMake.Row();
const smartGuessButton = domMake.Button("Bot de Adivinanza");
smartGuessButton.title = "Un bot intentará adivinar la palabra (simulado).";
smartGuessButton.addEventListener("click", () => this.#smartGuess());
smartGuessRow.appendChild(smartGuessButton);
swarmTacticsSection.appendChild(smartGuessRow);
// Bot Personality Controls
const personalityGroup = domMake.Tree("div", { class: "swarm-commander-control-group" });
personalityGroup.appendChild(domMake.Tree("label", { style: "width: 100%; text-align: center; font-weight: bold; margin-bottom: 5px;" }, ["Personalidad del Bot"]));
const drawingSpeedDiv = domMake.Tree("div");
drawingSpeedDiv.appendChild(domMake.Tree("label", {}, ["Velocidad Dibujo (ms/línea):"]));
const drawingSpeedInput = domMake.Tree("input", { type: "number", min: "1", max: "100", value: "10" });
drawingSpeedInput.addEventListener("change", (e) => this.#setBotDrawingSpeed(parseInt(e.target.value)));
drawingSpeedDiv.appendChild(drawingSpeedInput);
personalityGroup.appendChild(drawingSpeedDiv);
const verbosityDiv = domMake.Tree("div");
verbosityDiv.appendChild(domMake.Tree("label", {}, ["Verbosidad Mensajes:"]));
const verbositySelect = domMake.Tree("select");
['Silencioso', 'Normal', 'Charlatán'].forEach(level => {
verbositySelect.appendChild(domMake.Tree("option", { value: level }, [level]));
});
verbositySelect.addEventListener("change", (e) => this.#setBotChatVerbosity(e.target.value));
verbosityDiv.appendChild(verbositySelect);
personalityGroup.appendChild(verbosityDiv);
swarmTacticsSection.appendChild(personalityGroup);
container.appendChild(swarmTacticsSection);
// --- Section: Herramientas de Disrupción (BugGeneratorModule) ---
const disruptionSection = domMake.Tree("div", { class: "swarm-commander-section" });
disruptionSection.appendChild(domMake.Tree("div", { style: "width: 100%; text-align: center; font-weight: bold; margin-bottom: 5px;" }, ["Herramientas de Caos"]));
const lagRow = domMake.Row();
const lagButton = domMake.Button('<i class="fas fa-dizzy"></i> Generar Lag');
lagButton.classList.add("swarm-commander-toggle-button");
lagButton.addEventListener("click", () => this.#toggleLag(lagButton));
lagRow.appendChild(lagButton);
disruptionSection.appendChild(lagRow);
const bugExperienceRow = domMake.Row();
const bugExperienceButton = domMake.Button('<i class="fas fa-gamepad"></i> Bugear Experiencia');
bugExperienceButton.classList.add("swarm-commander-toggle-button");
bugExperienceButton.addEventListener("click", () => this.#toggleBugExperience(bugExperienceButton));
bugExperienceRow.appendChild(bugExperienceButton);
disruptionSection.appendChild(bugExperienceRow);
const playerChaosRow = domMake.Row();
const playerChaosButton = domMake.Button('<i class="fas fa-running"></i> Caos de Jugador');
playerChaosButton.classList.add("swarm-commander-toggle-button");
playerChaosButton.addEventListener("click", () => this.#togglePlayerChaos(playerChaosButton));
playerChaosRow.appendChild(playerChaosButton);
disruptionSection.appendChild(playerChaosRow);
const visualGlitchRow = domMake.Row();
const visualGlitchButton = domMake.Button('<i class="fas fa-ghost"></i> Glitch Visual');
visualGlitchButton.classList.add("swarm-commander-toggle-button");
visualGlitchButton.addEventListener("click", () => this.#toggleVisualGlitch(visualGlitchButton));
visualGlitchRow.appendChild(visualGlitchButton);
disruptionSection.appendChild(visualGlitchRow);
const secretSpamRow = domMake.Row();
const secretSpamButton = domMake.Button('<i class="fas fa-mask"></i> Spam Visual Secreto');
secretSpamButton.classList.add("swarm-commander-toggle-button");
secretSpamButton.addEventListener("click", () => this.#toggleSecretSpam(secretSpamButton));
secretSpamRow.appendChild(secretSpamButton);
disruptionSection.appendChild(secretSpamRow);
container.appendChild(disruptionSection);
this.htmlElements.section.appendChild(container);
}
// --- Helper: Get Bot Instance ---
#getBot() {
const botManagerClass = this.findGlobal("BotClientManager");
if (!botManagerClass || !botManagerClass.siblings || botManagerClass.siblings.length === 0) {
this.notify("warning", "No hay instancias activas de 'BotClientManager'. Por favor, crea uno desde 'CubeEngine'.");
return null;
}
const botManagerInstance = botManagerClass.siblings[0];
const botClientInterfaces = botManagerInstance.children;
let activeBotClientInterface = null;
const selectedBotInput = document.querySelector('input[name="botClient"]:checked');
if (selectedBotInput) {
activeBotClientInterface = botClientInterfaces.find(bci => bci.htmlElements.input === selectedBotInput);
}
if (!activeBotClientInterface && botClientInterfaces.length > 0) {
activeBotClientInterface = botClientInterfaces[0];
this.notify("info", `No se seleccionó un bot. Usando el primer bot disponible: ${activeBotClientInterface.getName()}.`);
}
if (!activeBotClientInterface || !activeBotClientInterface.bot || !activeBotClientInterface.bot.getReadyState()) {
this.notify("warning", `El bot "${activeBotClientInterface ? activeBotClientInterface.getName() : 'desconocido'}" no está conectado y listo para enviar comandos.`);
return null;
}
return activeBotClientInterface.bot;
}
// --- TacticalBotSwarm Methods ---
#startCollaborativeDrawing() {
const autodrawV2Class = this.findGlobal("AutodrawV2");
if (!autodrawV2Class || !autodrawV2Class.siblings || autodrawV2Class.siblings.length === 0) {
this.notify("warning", "El módulo 'Autodraw V2' no está activo. No se puede iniciar el dibujo colaborativo.");
return;
}
const autodrawV2Instance = autodrawV2Class.siblings[0];
if (autodrawV2Instance && typeof autodrawV2Instance.startDrawing === 'function') {
autodrawV2Instance.startDrawing();
this.notify("info", "Iniciando dibujo colaborativo a través del módulo Autodraw V2.");
} else {
this.notify("warning", "La instancia del módulo 'Autodraw V2' no está lista. Asegúrate de que Autodraw V2 se inicializó correctamente.");
}
}
#smartGuess() {
const bot = this.#getBot();
if (!bot) return;
const commonWords = ["casa", "flor", "mesa", "sol", "perro", "gato", "arbol", "coche", "libro"]; // Expanded words
const randomWord = commonWords[Math.floor(Math.random() * commonWords.length)];
bot.emit("chatmsg", randomWord);
this.notify("info", `Bot ${bot.name} intentó adivinar: "${randomWord}" (simulado).`);
}
#setBotDrawingSpeed(speed) {
const botManagerClass = this.findGlobal("BotClientManager");
if (botManagerClass) {
botManagerClass.siblings.forEach(botManagerInstance => {
if (botManagerInstance && botManagerInstance.children) {
botManagerInstance.children.forEach(botI => {
if (botI.bot) {
botI.bot.drawingDelay = speed; // Assuming BotClient has this property
this.notify("log", `Velocidad de dibujo de ${botI.getName()}: ${speed}ms/línea.`);
}
});
}
});
}
}
#setBotChatVerbosity(verbosity) {
const botManagerClass = this.findGlobal("BotClientManager");
if (botManagerClass) {
botManagerClass.siblings.forEach(botManagerInstance => {
if (botManagerInstance && botManagerInstance.children) {
botManagerInstance.children.forEach(botI => {
if (botI.bot) {
botI.bot.chatVerbosity = verbosity; // Assuming BotClient has this property
this.notify("log", `Verbosidad de ${botI.getName()}: ${verbosity}.`);
}
});
}
});
}
}
// --- BugGeneratorModule Methods ---
#toggleLag(button) {
if (this.#lagInterval) {
clearInterval(this.#lagInterval);
this.#lagInterval = null;
button.classList.remove("active");
button.textContent = '<i class="fas fa-dizzy"></i> Generar Lag';
this.notify("info", "Generador de Lag Detenido.");
} else {
const bot = this.#getBot();
if (!bot) return;
button.classList.add("active");
button.textContent = '<i class="fas fa-stop"></i> Detener Lag';
this.notify("info", "Generador de Lag Iniciado (enviando comandos de dibujo agresivos).");
let counter = 0;
this.#lagInterval = setInterval(() => {
if (!bot.getReadyState()) {
this.notify("warning", "Bot desconectado, deteniendo Lag.");
this.#toggleLag(button);
return;
}
if (counter % 50 === 0) {
bot.emit("clear");
this.notify("log", "Lag: Enviando comando 'Clear'!");
}
for (let i = 0; i < 5; i++) {
const x1 = Math.random() * 100; const y1 = Math.random() * 100;
const x2 = Math.random() * 100; const y2 = Math.random() * 100;
const size = Math.floor(Math.random() * 70) + 20;
const color = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
bot.emit("line", -1, x1, y1, x2, y2, true, size, color, false);
}
counter++;
if (counter % 20 === 0) {
this.notify("log", `Lag: ${counter * 5} líneas enviadas.`);
}
}, 25);
}
}
#toggleBugExperience(button) {
if (this.#bugExperienceInterval) {
clearInterval(this.#bugExperienceInterval);
this.#bugExperienceInterval = null;
button.classList.remove("active");
button.textContent = '<i class="fas fa-gamepad"></i> Bugear Experiencia';
this.notify("info", "Deteniendo 'Bugear Experiencia'.");
} else {
const bot = this.#getBot();
if (!bot) return;
button.classList.add("active");
button.textContent = '<i class="fas fa-stop"></i> Detener Bug';
this.notify("info", "Iniciando 'Bugear Experiencia': El bot enviará ráfagas de movimientos y gestos. Esto puede ser molesto para otros.");
let count = 0;
this.#bugExperienceInterval = setInterval(() => {
if (!bot.getReadyState()) {
this.notify("warning", "Bot desconectado, deteniendo 'Bugear Experiencia'.");
this.#toggleBugExperience(button);
return;
}
const randomX = Math.random() * 100; const randomY = Math.random() * 100;
bot.emit("moveavatar", randomX, randomY);
const randomGesture = Math.floor(Math.random() * 32);
bot.emit("sendgesture", randomGesture);
count++;
if (count >= 9000) {
this.notify("info", "'Bugear Experiencia' completado (200 acciones). Deteniendo.");
this.#toggleBugExperience(button);
}
}, 100);
}
}
#togglePlayerChaos(button) {
if (this.#playerChaosInterval) {
clearInterval(this.#playerChaosInterval);
this.#playerChaosInterval = null;
button.classList.remove("active");
button.textContent = '<i class="fas fa-running"></i> Caos de Jugador';
this.notify("info", "Deteniendo 'Caos de Jugador'.");
} else {
const bot = this.#getBot();
if (!bot) return;
button.classList.add("active");
button.textContent = '<i class="fas fa-stop"></i> Detener Caos';
this.notify("info", "Iniciando 'Caos de Jugador' (Agresivo): El bot enviará ráfagas de movimientos, gestos, AFK y cambios de estado. Esto puede ser muy molesto para otros.");
let count = 0;
this.#playerChaosInterval = setInterval(() => {
if (!bot.getReadyState()) {
this.notify("warning", "Bot desconectado, deteniendo 'Caos de Jugador'.");
this.#togglePlayerChaos(button);
return;
}
const randomX = Math.random() * 100; const randomY = Math.random() * 100;
bot.emit("moveavatar", randomX, randomY);
const randomGesture = Math.floor(Math.random() * 32);
bot.emit("sendgesture", randomGesture);
bot.emit("playerafk");
const randomFlagId = Math.floor(Math.random() * 5);
const randomFlagState = Math.random() < 0.5;
bot.emit("setstatusflag", randomFlagId, randomFlagState);
count++;
if (count >= 750) {
this.notify("info", "'Caos de Jugador' completado (750 acciones). Deteniendo.");
this.#togglePlayerChaos(button);
}
}, 100);
}
}
#toggleVisualGlitch(button) {
if (this.#visualGlitchInterval) {
clearInterval(this.#visualGlitchInterval);
this.#visualGlitchInterval = null;
button.classList.remove("active");
button.textContent = '<i class="fas fa-ghost"></i> Glitch Visual';
this.notify("info", "Deteniendo 'Glitch Visual'.");
} else {
const bot = this.#getBot();
if (!bot) return;
button.classList.add("active");
button.textContent = '<i class="fas fa-stop"></i> Detener Glitch';
this.notify("info", "Iniciando 'Glitch Visual' (Extremo): El bot forzará cambios de avatar, spam de chat y interrupciones de UI. Puede causar freezes.");
let count = 0;
this.#visualGlitchInterval = setInterval(() => {
if (!bot.getReadyState()) {
this.notify("warning", "Bot desconectado, deteniendo 'Glitch Visual'.");
this.#toggleVisualGlitch(button);
return;
}
bot.emit("spawnavatar");
bot.emit("setavatarprop");
const chatMessages = [
"!! GLITCH DETECTED !!", "ERROR CODE 404: REALITY NOT FOUND", "SYSTEM OVERLOAD",
"// VISUAL ANOMALY //", "PACKET CORRUPTION", "DISCONNECTING...", "RECALIBRATING... X_X",
"SCREEN_SHAKE_ACTIVE", "DATA STREAM INTERRUPTED", "SERVER REBOOTING (FAKE)"
];
const randomChatMessage = chatMessages[Math.floor(Math.random() * chatMessages.length)];
bot.emit("chatmsg", randomChatMessage);
const playersInRoom = bot.room.players || [];
const otherPlayers = playersInRoom.filter(p => p.id !== bot.id && p.id !== 0);
if (otherPlayers.length > 0) {
const randomPlayer = otherPlayers[Math.floor(Math.random() * otherPlayers.length)];
bot.emit("sendvotekick", randomPlayer.id);
this.notify("log", `Glitch: Votekick spam a ${randomPlayer.name}`);
}
bot.emit("sendvote");
if (otherPlayers.length > 0) {
const randomPlayerForToken = otherPlayers[Math.floor(Math.random() * otherPlayers.length)];
const randomTokenId = Math.floor(Math.random() * 9);
bot.emit("settoken", randomPlayerForToken.id, randomTokenId);
this.notify("log", `Glitch: Token spam a ${randomPlayerForToken.name}`);
}
count++;
if (count >= 1000) {
this.notify("info", "'Glitch Visual' completado (1000 acciones). Deteniendo.");
this.#toggleVisualGlitch(button);
}
}, 200);
}
}
#toggleSecretSpam(button) {
if (this.#secretSpamInterval) {
clearInterval(this.#secretSpamInterval);
this.#secretSpamInterval = null;
button.classList.remove("active");
button.textContent = '<i class="fas fa-mask"></i> Spam Visual Secreto';
this.notify("info", "Spam Visual 'Secreto' Detenido.");
} else {
const bot = this.#getBot();
if (!bot) return;
button.classList.add("active");
button.textContent = '<i class="fas fa-stop"></i> Detener Spam';
this.notify("info", "Spam Visual 'Secreto' Iniciado (inundando la sala con dibujos).");
let counter = 0;
this.#secretSpamInterval = setInterval(() => {
if (!bot.getReadyState()) {
this.notify("warning", "Bot desconectado, deteniendo Spam Visual 'Secreto'.");
this.#toggleSecretSpam(button);
return;
}
for (let i = 0; i < 10; i++) {
const x1 = Math.random() * 100; const y1 = Math.random() * 100;
const x2 = x1 + (Math.random() * 2 - 1) * 5;
const y2 = y1 + (Math.random() * 2 - 1) * 5;
const size = Math.floor(Math.random() * 3) + 1;
const color = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
bot.emit("line", -1, x1, y1, x2, y2, true, size, color, false);
}
counter += 10;
if (counter % 1000 === 0) {
this.notify("log", `Secreto: ${counter} comandos enviados.`);
}
}, 100);
}
}
}
})("QBit");
// --- END NEW MODULE: SwarmCommander ---
(function PlayerProfileExtractorModule() {
const QBit = globalThis[arguments[0]];
// Define token names for better readability. These are from Drawaria's common.js.
const TOKEN_NAMES = {
0: "Thumbs Up",
1: "Heart",
2: "Paint Brush",
3: "Cocktail",
4: "Peace Sign",
5: "Feather",
6: "Trophy",
7: "Mug",
8: "Gift"
};
class PlayerProfileExtractor extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#profileUrlInput;
#extractedDataDisplay;
#downloadButton;
#lastExtractedData = null; // Store data for download
constructor() {
super("Extractor de Perfil (Pegar URL de perfil)", '<i class="fas fa-id-card"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.notify("info", "Módulo Extractor de Perfil cargado.");
}
#loadInterface() {
const container = domMake.Tree("div");
// URL Input Row
const urlRow = domMake.Row();
this.#profileUrlInput = domMake.Tree("input", {
type: "text",
placeholder: "https://drawaria.online/profile/?uid=...)",
style: "width: 100%; padding: 5px; box-sizing: border-box;"
});
urlRow.appendChild(this.#profileUrlInput);
container.appendChild(urlRow);
// Search Button Row
const searchRow = domMake.Row();
const searchButton = domMake.Button('<i class="fas fa-search"></i> Buscar Perfil');
searchButton.addEventListener("click", () => this.#fetchAndExtractProfile());
searchRow.appendChild(searchButton);
container.appendChild(searchRow);
// Extracted Data Display Area
const displayRow = domMake.Row();
this.#extractedDataDisplay = domMake.Tree("div", {
style: `
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--CE-color);
padding: 8px;
font-size: 0.85em;
background-color: var(--CE-bg_color);
color: var(--CE-color);
margin-top: 5px;
white-space: pre-wrap; /* Preserve whitespace and wrap lines */
font-family: monospace; /* For better readability of structured data */
`
}, ["Datos del perfil aparecerán aquí."]);
displayRow.appendChild(this.#extractedDataDisplay);
container.appendChild(displayRow);
// Download Button Row
const downloadRow = domMake.Row();
this.#downloadButton = domMake.Button('<i class="fas fa-download"></i> Descargar Datos (JSON)');
this.#downloadButton.disabled = true; // Disabled until data is extracted
this.#downloadButton.addEventListener("click", () => this.#downloadExtractedData());
downloadRow.appendChild(this.#downloadButton);
container.appendChild(downloadRow);
this.htmlElements.section.appendChild(container);
}
/**
* Fetches the profile page HTML and extracts data.
*/
async #fetchAndExtractProfile() {
const profileUrl = this.#profileUrlInput.value.trim();
if (!profileUrl) {
this.notify("warning", "Por favor, introduce una URL de perfil.");
return;
}
// Basic URL validation
const uidMatch = profileUrl.match(/uid=([a-f0-9-]+)/i);
if (!uidMatch || !uidMatch[1]) {
this.notify("error", "URL inválida. No se pudo extraer el UID. Asegúrate de que es una URL de perfil de Drawaria (ej. https://drawaria.online/profile/?uid=...).");
return;
}
const uid = uidMatch[1];
this.notify("info", `Extrayendo datos de: ${profileUrl}...`);
this.#extractedDataDisplay.textContent = "Cargando...";
this.#downloadButton.disabled = true;
// Declare DOMParser once here so it's accessible to all inner parsing blocks
const parser = new DOMParser();
try {
// --- 1. Fetch Main Profile Page ---
const profileResponse = await fetch(profileUrl);
if (!profileResponse.ok) {
throw new Error(`Error HTTP (${profileUrl}): ${profileResponse.status} ${profileResponse.statusText}`);
}
const profileHtmlContent = await profileResponse.text();
// Pass the parser instance to the parsing function
const extracted = this.#parseMainProfileHTML(profileHtmlContent, parser);
extracted.uid = uid; // Ensure UID is set
// --- 2. Fetch Gallery Count (from HTML page) ---
const galleryPageUrl = `https://drawaria.online/gallery/?uid=${uid}`;
try {
const galleryResponse = await fetch(galleryPageUrl);
if (galleryResponse.ok) {
const galleryHtmlContent = await galleryResponse.text();
// Use the shared parser instance
const galleryDoc = parser.parseFromString(galleryHtmlContent, 'text/html');
// Count all .grid-item elements within the .grid container
extracted.galleryImagesCount = galleryDoc.querySelectorAll('.grid .grid-item.galleryimage').length;
} else {
extracted.galleryImagesCount = `Error (${galleryResponse.status})`;
this.notify("warning", `Fallo al cargar la página de galería: ${galleryResponse.status}`);
}
} catch (e) {
extracted.galleryImagesCount = `Error al parsear galería (${e.message.substring(0, 50)})`;
this.notify("warning", `Fallo al consultar página de galería: ${e.message}`);
}
// --- 3. Fetch Friends Count (from HTML page) ---
const friendsPageUrl = `https://drawaria.online/friends/?uid=${uid}`;
try {
const friendsResponse = await fetch(friendsPageUrl);
if (friendsResponse.ok) {
const friendsHtmlContent = await friendsResponse.text();
// Use the shared parser instance
const friendsDoc = parser.parseFromString(friendsHtmlContent, 'text/html');
extracted.friendsCount = friendsDoc.querySelectorAll('#friendscontainer .friendcard').length || 0;
} else {
extracted.friendsCount = `Error (${friendsResponse.status})`;
this.notify("warning", `Fallo al cargar la página de amigos: ${friendsResponse.status}`);
}
} catch (e) {
extracted.friendsCount = `Error al parsear amigos (${e.message.substring(0, 50)})`;
this.notify("warning", `Fallo al consultar página de amigos: ${e.message}`);
}
// --- 4. Fetch Palettes Count (from HTML page) ---
const palettesPageUrl = `https://drawaria.online/palettes/?uid=${uid}`;
try {
const palettesResponse = await fetch(palettesPageUrl);
if (palettesResponse.ok) {
const palettesHtmlContent = await palettesResponse.text();
// Use the shared parser instance
const palettesDoc = parser.parseFromString(palettesHtmlContent, 'text/html');
extracted.palettesCount = palettesDoc.querySelectorAll('.palettelist .rowitem').length || 0;
} else {
extracted.palettesCount = `Error (${palettesResponse.status})`;
this.notify("warning", `Fallo al cargar la página de paletas: ${palettesResponse.status}`);
}
} catch (e) {
extracted.palettesCount = `Error al parsear paletas (${e.message.substring(0, 50)})`;
this.notify("warning", `Fallo al consultar página de paletas: ${e.message}`);
}
// Final check: if primary player name was not found, notify as invalid profile
// We re-parse here just for this specific final check, using the same parser instance.
const doc = parser.parseFromString(profileHtmlContent, 'text/html');
if (extracted.playerName === 'N/A' && !doc.querySelector('h1')) {
this.#extractedDataDisplay.textContent = "No se pudo encontrar el perfil o extraer datos. Asegúrate de que la URL es correcta y el perfil existe.";
this.#lastExtractedData = null;
this.#downloadButton.disabled = true;
this.notify("error", "Perfil no encontrado o datos no extraíbles.");
return;
}
// Display extracted data
this.#lastExtractedData = extracted;
this.#displayExtractedData();
this.#downloadButton.disabled = false;
this.notify("success", "Datos del perfil extraídos exitosamente.");
} catch (error) {
this.notify("error", `Fallo general al cargar o procesar el perfil: ${error.message}`);
this.#extractedDataDisplay.textContent = `Error: ${error.message}`;
this.#lastExtractedData = null;
}
}
/**
* Parses the HTML content of the main profile page and extracts relevant information.
* @param {string} htmlContent - The HTML content of the profile page.
* @param {DOMParser} parser - The shared DOMParser instance.
* @returns {object} Extracted data.
*/
#parseMainProfileHTML(htmlContent, parser) { // Accept parser instance
const doc = parser.parseFromString(htmlContent, 'text/html');
const extracted = {};
// --- Player Info (from profile page) ---
const playerInfoAnchor = doc.querySelector('h1 a[href*="profile/?uid="]');
if (playerInfoAnchor) {
extracted.avatarUrl = playerInfoAnchor.querySelector('img.turnresults-avatar')?.src || 'N/A';
// Player name is the text content of the anchor AFTER the image
const playerNameNode = Array.from(playerInfoAnchor.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0);
extracted.playerName = playerNameNode?.textContent.trim() || 'N/A';
} else {
extracted.playerName = doc.querySelector('h1')?.textContent.trim() || 'N/A'; // Fallback to just H1 text if no anchor
extracted.avatarUrl = 'N/A';
}
// --- Level & Experience (from profile page) ---
extracted.level = doc.getElementById('levelval')?.textContent.trim() || 'N/A'; // Kept for JSON export, not displayed in text output
extracted.experience = doc.getElementById('exp-val')?.textContent.trim() || 'N/A';
// --- Pictionary / Guessing Stats (from profile page) ---
extracted.pictionaryStats = {};
const playStatsTableBody = doc.querySelector('#playstats table tbody');
if (playStatsTableBody) {
playStatsTableBody.querySelectorAll('tr').forEach(row => {
const label = row.querySelector('td:first-child')?.textContent.trim();
const value = row.querySelector('td:last-child')?.textContent.trim();
if (label && value) {
const cleanLabel = label.replace(/[:\s]/g, '').toLowerCase(); // "Total score:" -> "totalscore"
extracted.pictionaryStats[cleanLabel] = value;
}
});
}
// --- Playground Accolades (Tokens) (from profile page) ---
extracted.accusedTokens = {};
const tokensTableBody = doc.querySelector('#tokens table tbody');
if (tokensTableBody) {
tokensTableBody.querySelectorAll('i[data-tokenid]').forEach(iconElement => {
const tokenId = parseInt(iconElement.dataset.tokenid);
const tokenName = TOKEN_NAMES[tokenId] || `Unknown Token ${tokenId}`;
const count = parseInt(iconElement.textContent.trim()) || 0; // Text content holds the number
extracted.accusedTokens[tokenName] = count;
});
}
return extracted;
}
/**
* Formats and displays the extracted data in the UI.
*/
#displayExtractedData() {
if (!this.#lastExtractedData) {
this.#extractedDataDisplay.textContent = "No hay datos para mostrar.";
return;
}
let displayTxt = `--- Datos del Perfil ---\n`;
displayTxt += ` UID: ${this.#lastExtractedData.uid}\n`;
displayTxt += ` Nombre del Jugador: ${this.#lastExtractedData.playerName}\n`;
// Level is removed from text display, but still in #lastExtractedData for JSON export
displayTxt += ` Experiencia: ${this.#lastExtractedData.experience}\n`;
displayTxt += ` URL del Avatar: ${this.#lastExtractedData.avatarUrl}\n`;
// GalleryImagesCount is removed from text display, but still in #lastExtractedData for JSON export
displayTxt += ` Cantidad de Amigos: ${this.#lastExtractedData.friendsCount}\n`;
displayTxt += ` Cantidad de Paletas: ${this.#lastExtractedData.palettesCount}\n\n`;
displayTxt += `--- Estadísticas de Pictionary / Adivinanza ---\n`;
if (Object.keys(this.#lastExtractedData.pictionaryStats).length > 0) {
for (const key in this.#lastExtractedData.pictionaryStats) {
const displayKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
displayTxt += ` ${displayKey}: ${this.#lastExtractedData.pictionaryStats[key]}\n`;
}
} else {
displayTxt += ` No se encontraron estadísticas detalladas de Pictionary.\n`;
}
displayTxt += `\n`;
displayTxt += `--- Menciones de Homenaje (Tokens) ---\n`;
if (Object.keys(this.#lastExtractedData.accusedTokens).length > 0) {
for (const tokenName in this.#lastExtractedData.accusedTokens) {
displayTxt += ` ${tokenName}: ${this.#lastExtractedData.accusedTokens[tokenName]}\n`;
}
} else {
displayTxt += ` No se encontraron Menciones de Homenaje.\n`;
}
this.#extractedDataDisplay.textContent = displayTxt;
}
/**
* Downloads the extracted data as a JSON file.
*/
#downloadExtractedData() {
if (!this.#lastExtractedData) {
this.notify("warning", "No hay datos extraídos para descargar.");
return;
}
const dataStr = JSON.stringify(this.#lastExtractedData, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const playerNameForFilename = this.#lastExtractedData.playerName.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30); // Clean for filename
const filename = `drawaria_profile_${playerNameForFilename}_${this.#lastExtractedData.uid ? this.#lastExtractedData.uid.substring(0, 8) : 'data'}.json`;
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.notify("success", `Datos del perfil descargados como ${filename}.`);
}
}
})("QBit");
// --- START NEW MODULE: ImageAnalyzer (ACTUALIZADO con Copy/Download/Add to Palette) ---
(function ImageAnalyzerModule() {
const QBit = globalThis[arguments[0]];
QBit.Styles.addRules([
`#image-analyzer-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 5px;
border: 1px solid var(--CE-color);
border-radius: .25rem;
background-color: var(--CE-bg_color);
}`,
`#image-analyzer-container input[type="text"],
#image-analyzer-container button {
width: 100%;
padding: 5px;
box-sizing: border-box;
}`,
`#image-preview-canvas {
border: 1px dashed var(--CE-color);
max-width: 100%;
height: auto;
display: block;
margin: 5px auto;
}`,
`#dominant-colors-display {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 5px;
}`,
`#dominant-colors-display .color-swatch {
width: 30px;
height: 30px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
box-shadow: 0 0 3px rgba(0,0,0,0.2);
}`,
`#analysis-results {
background-color: var(--CE-bg_color);
border: 1px solid var(--CE-color);
padding: 8px;
margin-top: 10px;
font-size: 0.8em;
max-height: 150px;
overflow-y: auto;
white-space: pre-wrap;
font-family: monospace;
}`,
`#image-analyzer-container .action-buttons {
display: flex;
gap: 5px;
margin-top: 5px;
}`,
`#image-analyzer-container .action-buttons button {
flex: 1;
}`
]);
class ImageAnalyzer extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#imageUrlInput;
#loadAndAnalyzeButton;
#imagePreviewCanvas;
#imagePreviewCtx;
#dominantColorsDisplay;
#analysisResultsDisplay;
#copyResultsButton;
#downloadResultsButton;
constructor() {
super("Analizador de Imágenes", '<i class="fas fa-camera-retro"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.notify("info", "Módulo Analizador de Imágenes cargado.");
this.#loadAndAnalyzeButton.disabled = false;
this.#loadAndAnalyzeButton.textContent = 'Cargar y Analizar';
this.#copyResultsButton.disabled = true;
this.#downloadResultsButton.disabled = true;
}
#loadInterface() {
const container = domMake.Tree("div", { id: "image-analyzer-container" });
this.#imageUrlInput = domMake.Tree("input", {
type: "text",
placeholder: "Pegar URL de imagen (ej. avatar)",
value: "https://drawaria.online/avatar/cache/1a5f4450-7153-11ef-acaf-250da20bac69.jpg"
});
container.appendChild(this.#imageUrlInput);
this.#loadAndAnalyzeButton = domMake.Button('<i class="fas fa-chart-pie"></i> Cargar y Analizar');
this.#loadAndAnalyzeButton.addEventListener("click", () => this.#loadImageAndAnalyze());
container.appendChild(this.#loadAndAnalyzeButton);
this.#imagePreviewCanvas = domMake.Tree("canvas", { id: "image-preview-canvas" });
container.appendChild(this.#imagePreviewCanvas);
this.#imagePreviewCtx = this.#imagePreviewCanvas.getContext('2d');
container.appendChild(domMake.Tree("div", {}, ["Colores Dominantes (clic para añadir a Paletas):"]));
this.#dominantColorsDisplay = domMake.Tree("div", { id: "dominant-colors-display" });
container.appendChild(this.#dominantColorsDisplay);
this.#analysisResultsDisplay = domMake.Tree("pre", { id: "analysis-results" }, ["Resultados del análisis aparecerán aquí."]);
container.appendChild(this.#analysisResultsDisplay);
// Action Buttons Row (Copy/Download)
const actionButtonsRow = domMake.Row({ class: "action-buttons" });
this.#copyResultsButton = domMake.Button('<i class="fas fa-copy"></i> Copiar');
this.#copyResultsButton.addEventListener("click", () => this.#copyResultsToClipboard());
actionButtonsRow.appendChild(this.#copyResultsButton);
this.#downloadResultsButton = domMake.Button('<i class="fas fa-download"></i> Descargar');
this.#downloadResultsButton.addEventListener("click", () => this.#downloadResults());
actionButtonsRow.appendChild(this.#downloadResultsButton);
container.appendChild(actionButtonsRow);
this.htmlElements.section.appendChild(container);
}
async #loadImageAndAnalyze() {
const imageUrl = this.#imageUrlInput.value.trim();
if (!imageUrl) {
this.notify("warning", "Por favor, introduce una URL de imagen.");
return;
}
this.notify("info", "Cargando imagen para análisis...");
this.#analysisResultsDisplay.textContent = "Cargando...";
this.#dominantColorsDisplay.innerHTML = '';
this.#imagePreviewCtx.clearRect(0, 0, this.#imagePreviewCanvas.width, this.#imagePreviewCanvas.height);
this.#copyResultsButton.disabled = true;
this.#downloadResultsButton.disabled = true;
const img = new window.Image();
img.crossOrigin = "Anonymous"; // Crucial for CORS
img.src = imageUrl;
img.onload = async () => {
try {
const maxWidth = 300;
const maxHeight = 300;
let width = img.width;
let height = img.height;
if (width > maxWidth || height > maxHeight) {
if (width / maxWidth > height / maxHeight) {
height = height * (maxWidth / width);
width = maxWidth;
} else {
width = width * (maxHeight / height);
height = maxHeight;
}
}
this.#imagePreviewCanvas.width = width;
this.#imagePreviewCanvas.height = height;
this.#imagePreviewCtx.drawImage(img, 0, 0, width, height);
const imageData = this.#imagePreviewCtx.getImageData(0, 0, width, height);
const pixels = imageData.data;
const results = {};
const colorMap = new Map();
const sampleStep = Math.max(1, Math.floor(pixels.length / 4 / 5000));
for (let i = 0; i < pixels.length; i += 4 * sampleStep) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
if (a > 20) {
const colorKey = `${r},${g},${b}`;
colorMap.set(colorKey, (colorMap.get(colorKey) || 0) + 1);
}
}
const sortedColors = Array.from(colorMap.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
results.dominantColors = sortedColors.map(([rgbStr, count]) => {
const [r, g, b] = rgbStr.split(',').map(Number);
return { r, g, b, hex: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` };
});
this.#dominantColorsDisplay.innerHTML = '';
results.dominantColors.forEach(color => {
const swatch = domMake.Tree("div", {
class: "color-swatch",
style: `background-color: ${color.hex};`,
title: `Clic para añadir ${color.hex} a Paletas de Color.`
});
swatch.addEventListener('click', () => this.#addDominantColorToPalette(color.hex));
this.#dominantColorsDisplay.appendChild(swatch);
});
let totalBrightness = 0;
let nonTransparentPixelCount = 0;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
if (a > 0) {
totalBrightness += (0.299 * r + 0.587 * g + 0.114 * b);
nonTransparentPixelCount++;
}
}
results.averageBrightness = nonTransparentPixelCount > 0 ? (totalBrightness / nonTransparentPixelCount).toFixed(2) : 'N/A';
results.imageDimensions = `${width}x${height}`;
results.totalPixels = width * height;
results.nonTransparentPixels = nonTransparentPixelCount;
let resultsText = "--- Resultados del Análisis de Imagen ---\n";
resultsText += `Dimensiones: ${results.imageDimensions} píxeles\n`;
resultsText += `Píxeles no transparentes: ${results.nonTransparentPixels}\n`;
resultsText += `Brillo promedio: ${results.averageBrightness}\n`;
resultsText += `Colores Dominantes (HEX): ${results.dominantColors.map(c => c.hex).join(', ')}\n`;
resultsText += "\n¡Análisis completado!";
this.#analysisResultsDisplay.textContent = resultsText;
this.notify("success", "Análisis de imagen completado.");
this.#copyResultsButton.disabled = false;
this.#downloadResultsButton.disabled = false;
} catch (e) {
if (e.name === "SecurityError" || (e.message && e.message.includes("tainted"))) {
this.notify("error", "Error de CORS: No se pudo acceder a los píxeles de la imagen. La imagen debe estar en el mismo origen o permitir CORS.");
this.#analysisResultsDisplay.textContent = "Error: No se pudo leer la imagen debido a restricciones de seguridad (CORS).";
} else {
this.notify("error", `Error al analizar la imagen: ${e.message}`);
this.#analysisResultsDisplay.textContent = `Error: ${e.message}`;
console.error("Image analysis error:", e);
}
}
};
img.onerror = () => {
this.notify("error", "Fallo al cargar la imagen. ¿URL correcta o problema de red?");
this.#analysisResultsDisplay.textContent = "Error: Fallo al cargar la imagen.";
};
}
// --- Nuevas funciones de Utilidad ---
async #copyResultsToClipboard() {
const resultsText = this.#analysisResultsDisplay.textContent;
if (!resultsText.trim()) {
this.notify("warning", "No hay resultados para copiar.");
return;
}
try {
await navigator.clipboard.writeText(resultsText);
this.notify("success", "Resultados copiados al portapapeles.");
} catch (err) {
this.notify("error", `Error al copiar: ${err.message}`);
console.error("Copy to clipboard failed:", err);
}
}
#downloadResults() {
const resultsText = this.#analysisResultsDisplay.textContent;
if (!resultsText.trim()) {
this.notify("warning", "No hay resultados para descargar.");
return;
}
const blob = new Blob([resultsText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `drawaria_image_analysis_${Date.now()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.notify("success", "Resultados descargados como TXT.");
}
#addDominantColorToPalette(hexColor) {
const moreColorPalettesModule = this.findGlobal("MoreColorPalettes");
if (moreColorPalettesModule && moreColorPalettesModule.siblings && moreColorPalettesModule.siblings.length > 0) {
const paletteInstance = moreColorPalettesModule.siblings[0]; // Assuming first instance
if (paletteInstance && typeof paletteInstance.addCustomColorFromExternal === 'function') {
paletteInstance.addCustomColorFromExternal(hexColor);
this.notify("info", `Color ${hexColor} enviado a 'Paletas de Color'.`);
} else {
this.notify("warning", "El módulo 'Paletas de Color' no está listo o le falta la función para añadir colores.");
}
} else {
this.notify("warning", "El módulo 'Paletas de Color' no se encontró o no está activo.");
}
}
}
})("QBit");
// --- END NEW MODULE: ImageAnalyzer ---
(function RoomNavigatorModule() {
const QBit = globalThis[arguments[0]]; // Corrected access to QBit
// Helper to fetch JSON (re-declaring if not globally accessible or just for clarity)
function fetchJson(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
let parsedData = JSON.parse(xhr.responseText);
// Handle potential double-encoded JSON
if (typeof parsedData === 'string') {
parsedData = JSON.parse(parsedData);
}
resolve(parsedData);
} catch (e) {
reject(new Error('Fallo al analizar la respuesta JSON: ' + e.message + ` (Raw: ${xhr.responseText.substring(0, 100)}...)`));
}
} else {
reject(new Error(`La respuesta de la red no fue correcta, estado: ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Fallo la solicitud de red. Revisa tu conexión o el servidor.'));
xhr.send();
});
}
class RoomNavigator extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#roomListContainer;
#refreshButton;
#loadingIndicator;
#filterNameInput;
#filterMinPlayersInput;
#filterMaxPlayersInput;
#roomDataCache = []; // Store fetched raw room data
constructor() {
super("Navegador de Salas", '<i class="fas fa-search-location"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#fetchAndApplyFilters(); // Initial fetch and display
}
#loadInterface() {
const container = domMake.Tree("div");
// Refresh and Loading Row
const headerRow = domMake.Row();
headerRow.style.marginBottom = "10px";
this.#refreshButton = domMake.Button('<i class="fas fa-sync-alt"></i> Actualizar Salas');
this.#refreshButton.title = "Actualiza la lista de salas disponibles.";
this.#refreshButton.addEventListener("click", () => this.#fetchAndApplyFilters());
headerRow.appendChild(this.#refreshButton);
this.#loadingIndicator = domMake.Tree("span", { style: "margin-left: 10px; color: var(--info); display: none;" }, ['Cargando...']);
headerRow.appendChild(this.#loadingIndicator);
container.appendChild(headerRow);
// Filters Row
const filtersRow = domMake.Row();
filtersRow.style.flexWrap = "wrap";
filtersRow.style.gap = "5px";
this.#filterNameInput = domMake.Tree("input", { type: "text", placeholder: "Filtrar por nombre", style: "flex: 1 1 120px;" });
this.#filterNameInput.addEventListener("input", () => this.#applyFiltersAndDisplayRooms());
filtersRow.appendChild(this.#filterNameInput);
this.#filterMinPlayersInput = domMake.Tree("input", { type: "number", min: "0", placeholder: "Min Jugadores", style: "width: 80px;" });
this.#filterMinPlayersInput.addEventListener("input", () => this.#applyFiltersAndDisplayRooms());
filtersRow.appendChild(this.#filterMinPlayersInput);
this.#filterMaxPlayersInput = domMake.Tree("input", { type: "number", min: "0", placeholder: "Max Jugadores", style: "width: 80px;" });
this.#filterMaxPlayersInput.addEventListener("input", () => this.#applyFiltersAndDisplayRooms());
filtersRow.appendChild(this.#filterMaxPlayersInput);
container.appendChild(filtersRow);
// Room List Display Area
this.#roomListContainer = domMake.Tree("div", {
class: "room-list-display",
style: `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
padding: 5px;
max-height: 400px; /* Limits height and adds scrollbar */
overflow-y: auto;
border: 1px solid var(--CE-color);
border-radius: .25rem;
margin-top: 10px;
`
});
container.appendChild(this.#roomListContainer);
this.htmlElements.section.appendChild(container);
}
async #fetchAndApplyFilters() {
this.#roomListContainer.innerHTML = '';
this.#loadingIndicator.style.display = 'inline';
this.#refreshButton.disabled = true;
try {
const roomData = await fetchJson('https://drawaria.online/getroomlist');
if (!Array.isArray(roomData)) {
throw new Error('La respuesta no es un array de salas válido.');
}
this.#roomDataCache = roomData; // Cache the raw data
this.notify("info", `Se encontraron ${roomData.length} salas.`);
this.#applyFiltersAndDisplayRooms();
} catch (error) {
this.notify("error", `Error al cargar la lista de salas: ${error.message}`);
this.#roomListContainer.appendChild(domMake.TextNode("Error al cargar las salas. Inténtalo de nuevo."));
console.error("Fetch room list error:", error);
} finally {
this.#loadingIndicator.style.display = 'none';
this.#refreshButton.disabled = false;
}
}
#applyFiltersAndDisplayRooms() {
let filteredRooms = [...this.#roomDataCache]; // Start with cached data
const nameFilter = this.#filterNameInput.value.toLowerCase();
const minPlayers = parseInt(this.#filterMinPlayersInput.value);
const maxPlayers = parseInt(this.#filterMaxPlayersInput.value);
if (nameFilter) {
filteredRooms = filteredRooms.filter(room => {
const roomName = room[3] ? room[3].toLowerCase() : ''; // Access roomName by index 3
return roomName.includes(nameFilter);
});
}
if (!isNaN(minPlayers) && minPlayers >= 0) {
filteredRooms = filteredRooms.filter(room => room[1] >= minPlayers); // Access currentPlayers by index 1
}
if (!isNaN(maxPlayers) && maxPlayers >= 0) {
filteredRooms = filteredRooms.filter(room => room[1] <= maxPlayers); // Access currentPlayers by index 1
}
this.#displayRooms(filteredRooms);
}
#displayRooms(rooms) {
this.#roomListContainer.innerHTML = '';
if (rooms.length === 0) {
this.#roomListContainer.appendChild(domMake.TextNode("No hay salas que coincidan con los filtros."));
return;
}
rooms.forEach(roomArray => {
// Structure observed from the provided JSON example:
// [0] roomId: string (e.g., "f0e1c44b-fc49-4160-91c9-b297fb662692" or "f49bf487-e96a-45a9-9616-c4b71662ac50.3")
// [1] currentPlayers: number
// [2] maxPlayers: number
// [3] roomName: string (e.g., "жду друзей", "sos :)", or "")
// [4] gameModeType: number (2 for public, 3 for private/friend/custom)
// [5] unknown_number: number (e.g., 273052, 4176 - seems like a server-side counter or ID)
// [6] flags_array: Array (e.g., [null,true,null,null,null,true] or [null,true])
// - flags_array[0]: boolean (possibly password protected if true)
// - flags_array[1]: boolean (this flag seems always true in sample, not a public/private indicator)
// [7] roundsPlayed: number
// [8] serverId: number or null (e.g., 5 or null if main server, part of room ID for specific servers)
const roomId = roomArray[0];
const currentPlayers = roomArray[1];
const maxPlayers = roomArray[2];
const roomName = roomArray[3];
const gameModeType = roomArray[4];
const flags = roomArray[6]; // Flags are at index 6
const roundsPlayed = roomArray[7];
const serverId = roomArray[8];
let roomModeLabel = 'Desconocido';
if (gameModeType === 2) {
roomModeLabel = 'Público';
} else if (gameModeType === 3) {
roomModeLabel = 'Amigos/Privado';
}
// Access flags_array[0] for password protected status
const isPasswordProtected = flags && flags.length > 0 && flags[0] === true;
const isFull = currentPlayers >= maxPlayers;
const statusColor = isFull ? 'var(--danger)' : 'var(--success)';
const joinableStatus = isFull ? 'LLENA' : 'DISPONIBLE';
const roomCard = domMake.Tree("div", {
class: "room-card",
style: `
border: 1px solid var(--CE-color);
border-radius: .25rem;
padding: 8px;
background-color: var(--CE-bg_color);
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
font-size: 0.85em;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`
});
const nodesToAppend = [
domMake.Tree("div", { style: "font-weight: bold; color: var(--dark-blue-title); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;", title: roomName }, [`Sala: ${roomName || '(Sin Nombre)'}`]),
domMake.Tree("div", {}, [`Jugadores: ${currentPlayers}/${maxPlayers}`]),
domMake.Tree("div", {}, [`Rondas jugadas: ${roundsPlayed || 'N/A'}`]),
domMake.Tree("div", {}, [`Modo: ${roomModeLabel}`]),
isPasswordProtected ? domMake.Tree("div", {style: "color: var(--warning);"}, [`Contraseña: Sí`]) : null,
domMake.Tree("div", { style: `color: ${statusColor}; font-weight: bold;` }, [`Estado: ${joinableStatus}`])
].filter(node => node instanceof Node);
roomCard.appendAll(...nodesToAppend);
const joinButton = domMake.Button("Unirse");
joinButton.classList.add("btn-primary");
joinButton.style.width = "100%";
joinButton.style.marginTop = "5px";
// joinButton.disabled = isFull; // REMOVED: Allow joining even if full
joinButton.addEventListener("click", () => this.#joinRoom(roomId, serverId));
roomCard.appendChild(joinButton);
this.#roomListContainer.appendChild(roomCard);
});
}
#joinRoom(roomId, serverId) {
let fullRoomIdentifier = roomId;
// Construct full room ID, e.g., "uuid.serverId"
if (serverId !== null && serverId !== undefined && !String(roomId).includes(`.${serverId}`)) {
fullRoomIdentifier = `${roomId}.${serverId}`;
}
const roomUrl = `https://drawaria.online/room/${fullRoomIdentifier}`;
window.location.assign(roomUrl); // Navigate to the room
}
}
})("QBit");
(function ClientCommandSender() {
const QBit = globalThis[arguments[0]];
class ClientCommandSender extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#cmdIdInput;
#param1Input;
#param2Input;
#param3Input;
constructor() {
super("Client Cmd", '<i class="fas fa-code"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
}
#loadInterface() {
const row1 = domMake.Row();
this.#cmdIdInput = domMake.Tree("input", { type: "number", placeholder: "Command ID (e.g., 10)" });
row1.appendChild(this.#cmdIdInput);
this.htmlElements.section.appendChild(row1);
const row2 = domMake.Row();
this.#param1Input = domMake.Tree("input", { type: "text", placeholder: "Param 1 (e.g., true)" });
this.#param2Input = domMake.Tree("input", { type: "text", placeholder: "Param 2 (e.g., 1)" });
row2.appendAll(this.#param1Input, this.#param2Input);
this.htmlElements.section.appendChild(row2);
const row3 = domMake.Row();
this.#param3Input = domMake.Tree("input", { type: "text", placeholder: "Param 3 (e.g., 'text')" });
const sendButton = domMake.Button("Send CMD");
sendButton.addEventListener("click", () => this.sendCommand());
row3.appendAll(this.#param3Input, sendButton);
this.htmlElements.section.appendChild(row3);
}
parseParam(paramStr) {
if (paramStr.toLowerCase() === 'true') return true;
if (paramStr.toLowerCase() === 'false') return false;
if (!isNaN(paramStr) && paramStr.trim() !== '') return Number(paramStr);
if (paramStr.startsWith('[') && paramStr.endsWith(']')) {
try {
return JSON.parse(paramStr); // For arrays
} catch (e) {
return paramStr;
}
}
if (paramStr.startsWith('{') && paramStr.endsWith('}')) {
try {
return JSON.parse(paramStr); // For objects
} catch (e) {
return paramStr;
}
}
return paramStr; // Default to string
}
sendCommand() {
const cmdId = parseInt(this.#cmdIdInput.value);
if (isNaN(cmdId)) {
this.notify("warning", "Command ID must be a number.");
return;
}
const params = [];
const param1 = this.#param1Input.value.trim();
const param2 = this.#param2Input.value.trim();
const param3 = this.#param3Input.value.trim();
if (param1) params.push(this.parseParam(param1));
if (param2) params.push(this.parseParam(param2));
if (param3) params.push(this.parseParam(param3));
if (globalThis.sockets && globalThis.sockets.length > 0) {
const payload = ["clientcmd", cmdId, params];
const dataToSend = `${42}${JSON.stringify(payload)}`;
globalThis.sockets[0].send(dataToSend);
this.notify("info", `Custom clientcmd ${cmdId} sent with params: ${JSON.stringify(params)}.`);
} else {
this.notify("warning", "No active WebSocket connection found.");
}
}
}
})("QBit");
(function AdvancedTelemetry() {
const QBit = globalThis[arguments[0]];
class AdvancedTelemetry extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#playerMetricsContainer;
#snapshotContainer;
#snapshots = [];
#maxSnapshots = 3;
constructor() {
super("Telemetría Avanzada", '<i class="fas fa-chart-line"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#listenToGameEvents();
}
#loadInterface() {
this.#row1(); // Panel de Control de Jugadores
this.#row2(); // Historial Visual de Rondas
this.#row3(); // Temas Dinámicos del HUD
}
#row1() {
const row = domMake.Row();
this.#playerMetricsContainer = domMake.Tree("div", { class: "player-metrics-list" });
row.appendChild(domMake.Tree("label", {}, ["Métricas de Jugadores:"]));
row.appendChild(this.#playerMetricsContainer);
this.htmlElements.section.appendChild(row);
this.updatePlayerMetrics(); // Initial update
}
#row2() {
const row = domMake.Row();
const captureSnapshotButton = domMake.Button("Capturar Lienzo");
captureSnapshotButton.title = "Guarda una imagen del lienzo actual.";
captureSnapshotButton.addEventListener("click", () => this.captureCanvasSnapshot());
this.#snapshotContainer = domMake.Tree("div", { class: "snapshot-previews icon-list" });
row.appendAll(captureSnapshotButton, this.#snapshotContainer);
this.htmlElements.section.appendChild(row);
}
#row3() {
const row = domMake.Row();
const hudColorLabel = domMake.Tree("label", {}, ["Color del HUD:"]);
const hudColorInput = domMake.Tree("input", { type: "color", value: "#007bff" }); // Default Bootstrap primary
hudColorInput.addEventListener("change", (e) => {
const newColor = e.target.value;
document.documentElement.style.setProperty('--primary', newColor);
document.documentElement.style.setProperty('--success', newColor); // Apply to success as well for consistency
this.notify("info", `Color del HUD cambiado a: ${newColor}`);
});
row.appendAll(hudColorLabel, hudColorInput);
this.htmlElements.section.appendChild(row);
}
#listenToGameEvents() {
// Update player metrics whenever player list changes
const playerListElement = document.getElementById("playerlist");
if (playerListElement) {
const observer = new MutationObserver(() => this.updatePlayerMetrics());
observer.observe(playerListElement, { childList: true, subtree: true });
}
// Listen for chat messages for conceptual "heatmap"
if (globalThis._io && globalThis._io.events) {
// This is a placeholder as direct binding to _io.events.bc_chatmessage might not always work without a bot.
// A more robust solution would be to observe the #chatbox_messages div.
const chatboxMessages = document.getElementById("chatbox_messages");
if (chatboxMessages) {
const chatObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.classList && node.classList.contains('chatmessage') && !node.classList.contains('systemchatmessage5')) {
const playerNameElement = node.querySelector('.playerchatmessage-name a');
const playerName = playerNameElement ? playerNameElement.textContent : 'Unknown';
}
});
});
});
chatObserver.observe(chatboxMessages, { childList: true });
}
}
}
updatePlayerMetrics() {
this.#playerMetricsContainer.innerHTML = '';
const playerRows = document.querySelectorAll("#playerlist .playerlist-row");
if (playerRows.length === 0) {
this.#playerMetricsContainer.appendChild(domMake.TextNode("No hay jugadores en la sala."));
return;
}
playerRows.forEach(playerRow => {
const playerId = playerRow.dataset.playerid;
const playerName = playerRow.querySelector(".playerlist-name a")?.textContent || `Player ${playerId}`;
const score = playerRow.querySelector(".playerlist-rank")?.textContent || 'N/A';
const turnScore = playerRow.querySelector(".playerlist-turnscore")?.textContent || 'N/A';
const metricItem = domMake.Tree("div", { style: "margin: 2px 0; font-size: 0.8rem;" }, [
domMake.Tree("strong", {}, [`${playerName} (ID: ${playerId}): `]),
domMake.TextNode(`Puntuación: ${score}, Turno: ${turnScore}`)
]);
this.#playerMetricsContainer.appendChild(metricItem);
});
}
updatePlayerActivity(playerName) {
// This is a conceptual update. In a real scenario, this would update
// a dedicated "activity heatmap" visual.
this.notify("debug", ``);
const playerElements = document.querySelectorAll(`#playerlist .playerlist-row .playerlist-name a`);
playerElements.forEach(el => {
if (el.textContent === playerName) {
el.closest('.playerlist-row').style.backgroundColor = 'rgba(0, 255, 0, 0.1)'; // Flash green
setTimeout(() => {
el.closest('.playerlist-row').style.backgroundColor = '';
}, 500);
}
});
}
captureCanvasSnapshot() {
const gameCanvas = document.body.querySelector("canvas#canvas");
if (!gameCanvas) {
this.notify("error", "Lienzo de juego no encontrado para capturar.");
return;
}
try {
const base64Image = gameCanvas.toDataURL("image/png");
const timestamp = new Date().toLocaleString();
this.#snapshots.push({ data: base64Image, timestamp: timestamp });
if (this.#snapshots.length > this.#maxSnapshots) {
this.#snapshots.shift(); // Keep only the last N snapshots
}
this.updateSnapshotPreviews();
this.notify("success", `Instantánea del lienzo capturada: ${timestamp}`);
} catch (e) {
this.notify("error", `Error al capturar el lienzo: ${e.message}`);
console.error("Canvas snapshot error:", e);
}
}
updateSnapshotPreviews() {
this.#snapshotContainer.innerHTML = '';
if (this.#snapshots.length === 0) {
this.#snapshotContainer.appendChild(domMake.TextNode("No hay instantáneas guardadas."));
return;
}
this.#snapshots.forEach((snapshot, index) => {
const img = domMake.Tree("img", {
src: snapshot.data,
style: "width: 50px; height: 50px; border: 1px solid #ccc; margin: 2px; cursor: pointer;",
title: `Instantánea ${index + 1}: ${snapshot.timestamp}`
});
img.addEventListener("click", () => this.displaySnapshot(snapshot.data));
this.#snapshotContainer.appendChild(img);
});
}
displaySnapshot(imageData) {
// Create a temporary overlay to display the full snapshot
const overlay = domMake.Tree("div", {
style: `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8); z-index: 10000;
display: flex; justify-content: center; align-items: center;
`
});
const img = domMake.Tree("img", {
src: imageData,
style: `max-width: 90%; max-height: 90%; border: 2px solid white;`
});
overlay.appendChild(img);
overlay.addEventListener("click", () => overlay.remove()); // Close on click
document.body.appendChild(overlay);
}
}
})("QBit");
(function GameLogModule() {
const QBit = globalThis[arguments[0]]; // Corrected access to QBit
class GameLog extends QBit {
static dummy1 = QBit.register(this);
static dummy2 = QBit.bind(this, "CubeEngine");
#gameLog = []; // Stores log entries
#logDisplayElement;
#isLoggingActive = true; // Default to active logging
constructor() {
super("Registro del Juego", '<i class="fas fa-clipboard-list"></i>');
this.#onStartup();
}
#onStartup() {
this.#loadInterface();
this.#setupLogHooks();
this.notify("info", "Registro de Juego iniciado.");
}
#loadInterface() {
const container = domMake.Tree("div");
// Toggle Logging Button
const toggleRow = domMake.Row();
const toggleButton = domMake.Button("Desactivar Registro");
toggleButton.addEventListener("click", () => this.#toggleLogging(toggleButton));
toggleRow.appendChild(toggleButton);
container.appendChild(toggleRow);
// Log Display Area
const displayRow = domMake.Row();
this.#logDisplayElement = domMake.Tree("div", {
style: `
max-height: 250px;
overflow-y: auto;
border: 1px solid var(--CE-color);
padding: 5px;
font-size: 0.75em;
background-color: var(--CE-bg_color);
color: var(--CE-color);
margin-top: 5px;
`
}, ["Registro de eventos vacío."]);
displayRow.appendChild(this.#logDisplayElement);
container.appendChild(displayRow);
// Control Buttons
const controlRow = domMake.Row();
const clearButton = domMake.Button("Limpiar Log");
clearButton.addEventListener("click", () => this.#clearLog());
controlRow.appendChild(clearButton);
const exportTxtButton = domMake.Button("Exportar TXT");
exportTxtButton.addEventListener("click", () => this.#exportLog('txt'));
controlRow.appendChild(exportTxtButton);
const exportJsonButton = domMake.Button("Exportar JSON");
exportJsonButton.addEventListener("click", () => this.#exportLog('json'));
controlRow.appendChild(exportJsonButton);
container.appendChild(controlRow);
this.htmlElements.section.appendChild(container);
}
#setupLogHooks() {
// Hook into incoming WebSocket events (using _io.events where possible)
if (globalThis._io && globalThis._io.events) {
// Example of hooking:
const eventsToLog = [
"bc_chatmessage", "uc_turn_begindraw", "uc_turn_selectword",
"bc_round_results", "bc_turn_results", "bc_votekick",
"bc_clientnotify", "bc_announcement", "bc_extannouncement",
"mc_roomplayerschange" // To see player list changes
];
eventsToLog.forEach(eventName => {
const originalEventCallback = globalThis._io.events[eventName];
globalThis._io.events[eventName] = (...args) => {
this.#logEvent(eventName, args);
if (originalEventCallback) {
originalEventCallback.apply(this, args);
}
};
});
}
// Also observe the actual chatbox for messages to ensure all chat is captured,
// as some might not pass through _io.events in a way we can easily intercept.
const chatboxMessages = document.getElementById("chatbox_messages");
if (chatboxMessages) {
const chatObserver = new MutationObserver((mutations) => {
if (!this.#isLoggingActive) return;
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.classList.contains('chatmessage')) {
this.#logChatMessage(node);
}
});
});
});
chatObserver.observe(chatboxMessages, { childList: true });
}
}
#logEvent(type, data) {
if (!this.#isLoggingActive) return;
const timestamp = new Date().toISOString();
this.#gameLog.push({ timestamp, type, data: JSON.parse(JSON.stringify(data)) }); // Deep copy data
this.#updateLogDisplay();
}
#logChatMessage(messageNode) {
if (!this.#isLoggingActive) return;
const timestamp = messageNode.dataset.ts ? new Date(parseInt(messageNode.dataset.ts)).toISOString() : new Date().toISOString();
let entry = { timestamp, type: "chatmsg" };
if (messageNode.classList.contains('systemchatmessage') || messageNode.classList.contains('systemchatmessage5')) {
entry.subtype = "system";
entry.content = messageNode.textContent.trim();
} else {
entry.subtype = "player";
entry.playerName = messageNode.querySelector('.playerchatmessage-name')?.textContent?.trim() || 'Unknown';
entry.playerId = messageNode.querySelector('.playerchatmessage-name')?.parentElement?.dataset?.playerid || 'N/A';
entry.content = messageNode.querySelector('.playerchatmessage-text')?.textContent?.trim() || '';
entry.isSelf = messageNode.classList.contains('playerchatmessage-selfname');
}
this.#gameLog.push(entry);
this.#updateLogDisplay();
}
#updateLogDisplay() {
// Display only the last N messages to avoid performance issues
const maxDisplayEntries = 50;
const entriesToDisplay = this.#gameLog.slice(-maxDisplayEntries);
this.#logDisplayElement.innerHTML = ''; // Clear previous content
entriesToDisplay.forEach(entry => {
const logLine = domMake.Tree("div", {
style: `
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: ${entry.type.includes('error') ? 'var(--danger)' : entry.type.includes('warning') ? 'var(--warning)' : entry.type.includes('system') ? 'var(--info)' : 'var(--CE-color)'};
`,
title: JSON.stringify(entry) // Show full details on hover
});
let displayTxt = `[${new Date(entry.timestamp).toLocaleTimeString()}] `;
if (entry.type === "chatmsg") {
if (entry.subtype === "system") {
displayTxt += `[SISTEMA] ${entry.content}`;
} else {
displayTxt += `[CHAT] ${entry.playerName} (${entry.playerId}): ${entry.content}`;
}
} else if (entry.type === "uc_turn_begindraw" && entry.data && entry.data) {
displayTxt += `[TURNO] Comienza dibujo. Palabra: ${entry.data[1] || 'Desconocida'}`;
} else if (entry.type === "uc_turn_selectword" && entry.data && entry.data) {
displayTxt += `[TURNO] Seleccionar palabra: [${entry.data[2]?.join(', ') || 'N/A'}]`;
} else if (entry.type === "bc_round_results") {
displayTxt += `[RONDA] Resultados de ronda.`; // Too much data to display directly
} else if (entry.type === "bc_turn_results") {
displayTxt += `[TURNO] Resultados de turno.`;
} else {
displayTxt += `[${entry.type}] ${JSON.stringify(entry.data).substring(0, 50)}...`;
}
logLine.textContent = displayTxt;
this.#logDisplayElement.appendChild(logLine);
});
// Scroll to bottom
this.#logDisplayElement.scrollTop = this.#logDisplayElement.scrollHeight;
}
#toggleLogging(button) {
this.#isLoggingActive = !this.#isLoggingActive;
button.classList.toggle("active", !this.#isLoggingActive);
button.textContent = this.#isLoggingActive ? "Desactivar Registro" : "Activar Registro";
this.notify("info", `Registro de Juego: ${this.#isLoggingActive ? 'Activo' : 'Inactivo'}`);
}
#clearLog() {
if (confirm("¿Estás seguro de que quieres limpiar todo el registro del juego?")) {
this.#gameLog = [];
this.#updateLogDisplay();
this.notify("info", "Registro de Juego limpiado.");
}
}
#exportLog(format) {
if (this.#gameLog.length === 0) {
this.notify("warning", "No hay datos en el registro para exportar.");
return;
}
let dataString;
let mimeType;
let filename = `drawaria_game_log_${new Date().toISOString().slice(0, 10)}`;
if (format === 'json') {
dataString = JSON.stringify(this.#gameLog, null, 2);
mimeType = 'application/json';
filename += '.json';
} else { // Default to TXT
dataString = this.#gameLog.map(entry => {
const time = new Date(entry.timestamp).toLocaleTimeString();
if (entry.type === "chatmsg") {
if (entry.subtype === "system") {
return `[${time}] [SISTEMA] ${entry.content}`;
} else {
return `[${time}] [CHAT] ${entry.playerName} (${entry.playerId}): ${entry.content}`;
}
} else if (entry.type === "uc_turn_begindraw" && entry.data && entry.data) {
return `[${time}] [TURNO_INICIO] Palabra: ${entry.data[1] || 'Desconocida'}`;
} else if (entry.type === "uc_turn_selectword" && entry.data && entry.data) {
return `[${time}] [TURNO_PALABRA] Opciones: [${entry.data[2]?.join(', ') || 'N/A'}]`;
} else if (entry.type === "bc_round_results") {
return `[${time}] [RONDA_FIN] Resultados: ${JSON.stringify(entry.data[0].map(p => ({name: p[1], score: p[2]})))}`;
}
return `[${time}] [${entry.type}] ${JSON.stringify(entry.data)}`;
}).join('\n');
mimeType = 'text/plain';
filename += '.txt';
}
const blob = new Blob([dataString], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.notify("success", `Log exportado como ${filename}.`);
}
}
})("QBit");
// END GAME LOG
})();