import React, { useState, useEffect, useRef } from 'react'; import { MousePointer2, Move, Plus, Save, Upload, Play, Square, Type, Trash2, Settings, Grid3X3, LayoutGrid, X, Database, Sliders, ArrowRight, Zap, ShoppingCart, CheckSquare, Clock, Timer, Calculator, HelpCircle, Hash, Download } from 'lucide-react'; // --- DECIMAL CLASS --- class Decimal { constructor(value) { if (value instanceof Decimal) { this.m = value.m; this.e = value.e; } else if (typeof value === 'string') { if (value.indexOf('e') !== -1) { const parts = value.split('e'); this.m = parseFloat(parts[0]); this.e = parseFloat(parts[1]); } else { this.m = parseFloat(value); this.e = 0; } } else if (typeof value === 'number') { this.m = value; this.e = 0; } else { this.m = 0; this.e = 0; } this.normalize(); } normalize() { if (this.m === 0) { this.e = 0; return; } if (Math.abs(this.m) >= 10) { const adjust = Math.floor(Math.log10(Math.abs(this.m))); this.m /= Math.pow(10, adjust); this.e += adjust; } else if (Math.abs(this.m) < 1 && this.m !== 0) { const adjust = Math.floor(Math.log10(Math.abs(this.m))); this.m /= Math.pow(10, adjust); this.e += adjust; } } add(other) { other = new Decimal(other); if (this.e - other.e > 15) return new Decimal(this); if (other.e - this.e > 15) return new Decimal(other); const maxE = Math.max(this.e, other.e); const m1 = this.m * Math.pow(10, this.e - maxE); const m2 = other.m * Math.pow(10, other.e - maxE); const res = new Decimal(0); res.m = m1 + m2; res.e = maxE; res.normalize(); return res; } sub(other) { other = new Decimal(other); const negOther = new Decimal(other); negOther.m = -negOther.m; return this.add(negOther); } mul(other) { other = new Decimal(other); const res = new Decimal(0); res.m = this.m * other.m; res.e = this.e + other.e; res.normalize(); return res; } div(other) { other = new Decimal(other); const res = new Decimal(0); res.m = this.m / other.m; res.e = this.e - other.e; res.normalize(); return res; } floor() { if (this.e < 15) return new Decimal(Math.floor(this.toNumber())); return new Decimal(this); } toNumber() { return this.m * Math.pow(10, this.e); } pow(power) { const scalar = parseFloat(power); const res = new Decimal(0); res.m = Math.pow(this.m, scalar); res.e = this.e * scalar; res.normalize(); return res; } log10() { return this.e + Math.log10(this.m); } sqrt() { if (this.m < 0) return new Decimal(0); return this.pow(0.5); } gte(other) { other = new Decimal(other); if (this.m === 0 && other.m === 0) return true; if (this.m === 0) return false; if (other.m === 0) return true; if (this.e > other.e) return true; if (this.e < other.e) return false; return this.m >= other.m; } gt(other) { return this.gte(other) && !this.equals(other); } equals(other) { other = new Decimal(other); return this.m === other.m && this.e === other.e; } toString() { if (this.e < 6 && this.e > -6) return (this.m * Math.pow(10, this.e)).toFixed(2).replace(/\.00$/, ''); return `${this.m.toFixed(2)}e${this.e}`; } static format(val) { const num = new Decimal(val); if (num.e < 4) { const plain = num.m * Math.pow(10, num.e); return Math.abs(plain) < 0.01 && plain !== 0 ? plain.toExponential(2) : plain.toFixed(1).replace(/\.0$/, ''); } else if (num.e < 100000) { return `${num.m.toFixed(2)}e${num.e}`; } else { return `${num.m.toFixed(2)}e${num.e.toExponential(2).replace('+', '')}`; } } } // --- FORMULA PARSER --- const evaluateFormula = (formula, gameState, contextVars = {}) => { if (!formula) return new Decimal(0); const substituted = formula.replace(/{(\w+)}|(\bx\b)/g, (match, id, xVar) => { if (xVar) return (contextVars.x !== undefined ? contextVars.x : 0).toString(); const val = gameState[id] || new Decimal(0); return val.toString(); }); const tokens = substituted.match(/(\d+(\.\d+)?(e[+-]?\d+)?)|[\+\-\*\/\^\(\)]|sqrt|log|floor/g); if (!tokens) return new Decimal(0); const outputQueue = []; const operatorStack = []; const precedence = { '+': 1, '-': 1, '*': 2, '/': 2, '^': 3, 'sqrt': 4, 'log': 4, 'floor': 4 }; tokens.forEach(token => { if (!isNaN(parseFloat(token)) && isFinite(token.split('e')[1] || 0)) { outputQueue.push(new Decimal(token)); } else if (token in precedence) { while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== '(' && precedence[operatorStack[operatorStack.length - 1]] >= precedence[token]) { outputQueue.push(operatorStack.pop()); } operatorStack.push(token); } else if (token === '(') { operatorStack.push(token); } else if (token === ')') { while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== '(') { outputQueue.push(operatorStack.pop()); } operatorStack.pop(); } }); while (operatorStack.length > 0) outputQueue.push(operatorStack.pop()); const evalStack = []; outputQueue.forEach(token => { if (token instanceof Decimal) { evalStack.push(token); } else { if (token === 'sqrt' || token === 'log' || token === 'floor') { const a = evalStack.pop(); if (!a) return; if (token === 'sqrt') evalStack.push(a.sqrt()); if (token === 'log') evalStack.push(new Decimal(a.log10())); if (token === 'floor') evalStack.push(a.floor()); } else { const b = evalStack.pop(); const a = evalStack.pop(); if (!a || !b) return; if (token === '+') evalStack.push(a.add(b)); if (token === '-') evalStack.push(a.sub(b)); if (token === '*') evalStack.push(a.mul(b)); if (token === '/') evalStack.push(a.div(b)); if (token === '^') evalStack.push(a.pow(b.toNumber())); } } }); return evalStack[0] || new Decimal(0); }; // --- CONSTANTS --- const ELEMENT_TYPES = { BUTTON: 'button', LABEL: 'label', PANEL: 'panel' }; const OPERATIONS = { ADD: 'add', SUBTRACT: 'subtract', MULTIPLY: 'multiply', POWER: 'exponent' }; const INTERACTION_TYPES = { GAIN_MULTIPLIER: 'gain_mult' }; const TICKER_SOURCE_TYPES = { FIXED: 'fixed', RESOURCE: 'resource' }; const snapToGrid = (value, gridSize) => Math.round(value / gridSize) * gridSize; // --- MAIN COMPONENT --- export default function IncrementalGameBuilder() { const [mode, setMode] = useState('edit'); const [gridSize, setGridSize] = useState(20); const [showGrid, setShowGrid] = useState(true); const [selectedId, setSelectedId] = useState(null); const [activeTab, setActiveTab] = useState('properties'); const [showMathHelp, setShowMathHelp] = useState(false); const [resources, setResources] = useState([ { id: 'gold', name: 'Gold', initial: '0' }, { id: 'wood', name: 'Wood', initial: '0' }, { id: 'miners', name: 'Miners', initial: '0' }, ]); const [interactions, setInteractions] = useState([]); const [tickers, setTickers] = useState([]); const [gameSettings, setGameSettings] = useState({ enableBuyAmount: false }); const [elements, setElements] = useState([ { id: 'lbl1', type: ELEMENT_TYPES.LABEL, x: 20, y: 20, w: 200, h: 40, text: 'Gold: {gold}', bgColor: 'transparent', textColor: '#ffffff', fontSize: 24 }, { id: 'btn1', type: ELEMENT_TYPES.BUTTON, x: 20, y: 80, w: 140, h: 60, text: 'Mine Gold\nCost: {cost}', bgColor: '#3b82f6', textColor: '#ffffff', effects: [{ resourceId: 'gold', op: OPERATIONS.ADD, value: '1' }], costs: [], isShopButton: false, initialX: '0' } ]); const [gameState, setGameState] = useState({}); const [buyAmount, setBuyAmount] = useState(1); const dragRef = useRef(null); const resizeRef = useRef(null); const canvasRef = useRef(null); const lastTimeRef = useRef(Date.now()); const requestRef = useRef(); const tickersRef = useRef(tickers); const interactionsRef = useRef(interactions); useEffect(() => { tickersRef.current = tickers; }, [tickers]); useEffect(() => { interactionsRef.current = interactions; }, [interactions]); useEffect(() => { if (mode === 'play') { const initialstate = {}; resources.forEach(r => initialstate[r.id] = new Decimal(r.initial)); elements.forEach(el => initialstate[`${el.id}_count`] = new Decimal(0)); setGameState(initialstate); setSelectedId(null); setBuyAmount(1); lastTimeRef.current = Date.now(); const loop = () => { const now = Date.now(); const dt = Math.min((now - lastTimeRef.current) / 1000, 1.0); lastTimeRef.current = now; if (dt > 0) { setGameState(prevState => { const newState = {}; Object.keys(prevState).forEach(k => newState[k] = new Decimal(prevState[k])); tickersRef.current.forEach(ticker => { if (!ticker.targetId) return; let baseAmount = new Decimal(0); if (ticker.sourceType === TICKER_SOURCE_TYPES.FIXED) baseAmount = new Decimal(ticker.value || 0); else if (ticker.sourceType === TICKER_SOURCE_TYPES.RESOURCE && ticker.sourceId) baseAmount = newState[ticker.sourceId] || new Decimal(0); let totalAmount = baseAmount.mul(new Decimal(ticker.multiplier || 1)); let bonusMultiplier = new Decimal(0); interactionsRef.current.forEach(rule => { if (rule.targetId === ticker.targetId && rule.type === INTERACTION_TYPES.GAIN_MULTIPLIER) { const sourceAmount = newState[rule.sourceId] || new Decimal(0); bonusMultiplier = bonusMultiplier.add(sourceAmount.mul(new Decimal(rule.value))); } }); totalAmount = totalAmount.mul(new Decimal(1).add(bonusMultiplier)); const interval = Math.max(parseFloat(ticker.interval) || 1000, 10); const ratePerSec = totalAmount.mul(new Decimal(1000).div(new Decimal(interval))); const gain = ratePerSec.mul(new Decimal(dt)); newState[ticker.targetId] = (newState[ticker.targetId] || new Decimal(0)).add(gain); }); return newState; }); } requestRef.current = requestAnimationFrame(loop); }; requestRef.current = requestAnimationFrame(loop); } else { if (requestRef.current) cancelAnimationFrame(requestRef.current); } return () => { if (requestRef.current) cancelAnimationFrame(requestRef.current); }; }, [mode, resources]); const handleExportHTML = () => { const gameData = { resources, elements, gridSize, interactions, tickers, gameSettings }; // Use raw code strings to ensure variables are defined in the exported scope const decimalClassCode = ` class Decimal { constructor(value) { if (value instanceof Decimal) { this.m = value.m; this.e = value.e; } else if (typeof value === 'string') { if (value.indexOf('e') !== -1) { const p = value.split('e'); this.m = parseFloat(p[0]); this.e = parseFloat(p[1]); } else { this.m = parseFloat(value); this.e = 0; } } else if (typeof value === 'number') { this.m = value; this.e = 0; } else { this.m = 0; this.e = 0; } this.normalize(); } normalize() { if (this.m === 0) { this.e = 0; return; } if (Math.abs(this.m) >= 10) { const adj = Math.floor(Math.log10(Math.abs(this.m))); this.m /= Math.pow(10, adj); this.e += adj; } else if (Math.abs(this.m) < 1 && this.m !== 0) { const adj = Math.floor(Math.log10(Math.abs(this.m))); this.m /= Math.pow(10, adj); this.e += adj; } } add(o) { o = new Decimal(o); if(this.e - o.e > 15) return new Decimal(this); if(o.e - this.e > 15) return new Decimal(o); const max = Math.max(this.e, o.e); const m1 = this.m * Math.pow(10, this.e-max); const m2 = o.m * Math.pow(10, o.e-max); const r = new Decimal(0); r.m = m1+m2; r.e = max; r.normalize(); return r; } sub(o) { o = new Decimal(o); const n = new Decimal(o); n.m = -n.m; return this.add(n); } mul(o) { o = new Decimal(o); const r = new Decimal(0); r.m = this.m * o.m; r.e = this.e + o.e; r.normalize(); return r; } div(o) { o = new Decimal(o); const r = new Decimal(0); r.m = this.m / o.m; r.e = this.e - o.e; r.normalize(); return r; } floor() { if(this.e < 15) return new Decimal(Math.floor(this.toNumber())); return new Decimal(this); } toNumber() { return this.m * Math.pow(10, this.e); } pow(p) { const s = parseFloat(p); const r = new Decimal(0); r.m = Math.pow(this.m, s); r.e = this.e * s; r.normalize(); return r; } log10() { return this.e + Math.log10(this.m); } sqrt() { if(this.m < 0) return new Decimal(0); return this.pow(0.5); } gte(o) { o = new Decimal(o); if(this.m===0 && o.m===0) return true; if(this.m===0) return false; if(o.m===0) return true; if(this.e > o.e) return true; if(this.e < o.e) return false; return this.m >= o.m; } gt(o) { return this.gte(o) && !this.equals(o); } equals(o) { o = new Decimal(o); return this.m === o.m && this.e === o.e; } toString() { if(this.e < 6 && this.e > -6) return (this.m * Math.pow(10, this.e)).toFixed(2).replace(/\\.00$/, ''); return \`\${this.m.toFixed(2)}e\${this.e}\`; } static format(val) { const n = new Decimal(val); if (n.e < 4) { const p = n.m * Math.pow(10, n.e); return Math.abs(p) < 0.01 && p !== 0 ? p.toExponential(2) : p.toFixed(1).replace(/\\.0$/, ''); } else if (n.e < 100000) { return \`\${n.m.toFixed(2)}e\${n.e}\`; } else { return \`\${n.m.toFixed(2)}e\${n.e.toExponential(2).replace('+', '')}\`; } } } `; const evalFormulaCode = ` const evaluateFormula = (formula, gameState, contextVars = {}) => { if (!formula) return new Decimal(0); const substituted = formula.replace(/{(\\w+)}|(\\bx\\b)/g, (match, id, xVar) => { if (xVar) return (contextVars.x !== undefined ? contextVars.x : 0).toString(); const val = gameState[id] || new Decimal(0); return val.toString(); }); const tokens = substituted.match(/(\\d+(\\.\\d+)?(e[+-]?\\d+)?)|[\\+\\-\\*\\/\\^\\(\\)]|sqrt|log|floor/g); if (!tokens) return new Decimal(0); const outputQueue = []; const operatorStack = []; const precedence = { '+': 1, '-': 1, '*': 2, '/': 2, '^': 3, 'sqrt': 4, 'log': 4, 'floor': 4 }; tokens.forEach(token => { if (!isNaN(parseFloat(token)) && isFinite(token.split('e')[1] || 0)) { outputQueue.push(new Decimal(token)); } else if (token in precedence) { while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== '(' && precedence[operatorStack[operatorStack.length - 1]] >= precedence[token]) { outputQueue.push(operatorStack.pop()); } operatorStack.push(token); } else if (token === '(') { operatorStack.push(token); } else if (token === ')') { while (operatorStack.length > 0 && operatorStack[operatorStack.length - 1] !== '(') { outputQueue.push(operatorStack.pop()); } operatorStack.pop(); } }); while (operatorStack.length > 0) outputQueue.push(operatorStack.pop()); const evalStack = []; outputQueue.forEach(token => { if (token instanceof Decimal) { evalStack.push(token); } else { if (token === 'sqrt' || token === 'log' || token === 'floor') { const a = evalStack.pop(); if (!a) return; if (token === 'sqrt') evalStack.push(a.sqrt()); if (token === 'log') evalStack.push(new Decimal(a.log10())); if (token === 'floor') evalStack.push(a.floor()); } else { const b = evalStack.pop(); const a = evalStack.pop(); if (!a || !b) return; if (token === '+') evalStack.push(a.add(b)); if (token === '-') evalStack.push(a.sub(b)); if (token === '*') evalStack.push(a.mul(b)); if (token === '/') evalStack.push(a.div(b)); if (token === '^') evalStack.push(a.pow(b.toNumber())); } } }); return evalStack[0] || new Decimal(0); }; `; const htmlContent = `
Select an element on the canvas to edit its properties.
Starting 'count' for scaling.
Scaling Formulas:
x = (Initial X + Times Bought)10 * 1.1 ^ xsqrt(x), log(x), floor(x){`{miners}`} for other resource values.{`{cost}`} in text to show price.{`{${res.id}}`} in labels/formulas.Adds {ticker.sourceType === TICKER_SOURCE_TYPES.RESOURCE ? `{${ticker.sourceId || '?'}}` : ticker.value} * {ticker.multiplier} every {ticker.interval}ms
Shows "Buy 1 / 10 / Max" controls in play mode. Only affects buttons marked as "Shop Items".