fix: wire connections + hint system with star penalty
- Replace elementFromPoint with bounding rect distance search for port detection. The SVG wire overlay was intercepting pointer events, requiring users to wait for animations before connecting. Now finds the closest port-dot within 18px radius regardless of z-index. - Add hint system: concept text hidden behind "Mostrar Pista" button. Using the hint permanently caps the level at 2 stars max. Reiniciar resets the penalty. Visual feedback in objectives and completion screen. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
27
src/App.jsx
27
src/App.jsx
@@ -59,20 +59,23 @@ export default function App({ onSwitchToGame }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Find port-dot element at pointer position (including nearby)
|
// Find port-dot element at pointer position (including nearby)
|
||||||
const findPortAtPoint = (x, y) => {
|
// Robust port detection — searches all port-dots by bounding rect distance
|
||||||
// First try exact hit
|
// instead of elementFromPoint (which gets blocked by SVG wire overlay)
|
||||||
const el = document.elementFromPoint(x, y);
|
const findPortAtPoint = (clientX, clientY) => {
|
||||||
if (el && el.classList.contains('port-dot') && el.dataset.moduleId) {
|
const portDots = document.querySelectorAll('.port-dot[data-module-id]');
|
||||||
return el;
|
let closest = null;
|
||||||
}
|
let closestDist = 18;
|
||||||
// Try a small radius around the point
|
for (const dot of portDots) {
|
||||||
for (const [dx, dy] of [[0,0],[-8,0],[8,0],[0,-8],[0,8],[-6,-6],[6,-6],[-6,6],[6,6]]) {
|
const rect = dot.getBoundingClientRect();
|
||||||
const hit = document.elementFromPoint(x + dx, y + dy);
|
const cx = rect.left + rect.width / 2;
|
||||||
if (hit && hit.classList.contains('port-dot') && hit.dataset.moduleId) {
|
const cy = rect.top + rect.height / 2;
|
||||||
return hit;
|
const dist = Math.sqrt((clientX - cx) ** 2 + (clientY - cy) ** 2);
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist;
|
||||||
|
closest = dot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return closest;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Canvas pointer events
|
// Canvas pointer events
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function LevelComplete({ stars, checks, levelTitle, onNext, onRetry, onMap, isLastLevel }) {
|
export default function LevelComplete({ stars, checks, levelTitle, onNext, onRetry, onMap, isLastLevel, hintPenalty }) {
|
||||||
const [showStars, setShowStars] = useState(0);
|
const [showStars, setShowStars] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Animate stars appearing one by one
|
|
||||||
const timers = [];
|
const timers = [];
|
||||||
for (let i = 1; i <= stars; i++) {
|
for (let i = 1; i <= stars; i++) {
|
||||||
timers.push(setTimeout(() => setShowStars(i), i * 400));
|
timers.push(setTimeout(() => setShowStars(i), i * 400));
|
||||||
@@ -13,9 +12,9 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
|||||||
}, [stars]);
|
}, [stars]);
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
'', // 0 stars
|
'',
|
||||||
'Has dado el primer paso...',
|
'Has dado el primer paso...',
|
||||||
'Buen trabajo — casi perfecto.',
|
hintPenalty ? 'Reinicia sin pista para conseguir 3 estrellas.' : 'Buen trabajo — casi perfecto.',
|
||||||
'Ejecucion impecable.',
|
'Ejecucion impecable.',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -27,21 +26,25 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="gm-complete-level">{levelTitle}</p>
|
<p className="gm-complete-level">{levelTitle}</p>
|
||||||
|
|
||||||
{/* Stars */}
|
|
||||||
<div className="gm-complete-stars">
|
<div className="gm-complete-stars">
|
||||||
{[1, 2, 3].map(i => (
|
{[1, 2, 3].map(i => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className={`gm-big-star ${i <= showStars ? 'earned' : 'empty'}`}
|
className={`gm-big-star ${i <= showStars ? 'earned' : 'empty'} ${i === 3 && hintPenalty ? 'locked' : ''}`}
|
||||||
>
|
>
|
||||||
★
|
{i === 3 && hintPenalty ? '🔒' : '★'}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="gm-complete-msg">{messages[stars] || ''}</p>
|
<p className="gm-complete-msg">{messages[stars] || ''}</p>
|
||||||
|
|
||||||
{/* Checks */}
|
{hintPenalty && (
|
||||||
|
<p className="gm-hint-penalty-msg">
|
||||||
|
Usaste la pista — tercera estrella bloqueada
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="gm-checks">
|
<div className="gm-checks">
|
||||||
{checks.map((check, i) => (
|
{checks.map((check, i) => (
|
||||||
<div key={i} className={`gm-check ${check.passed ? 'passed' : 'failed'}`}>
|
<div key={i} className={`gm-check ${check.passed ? 'passed' : 'failed'}`}>
|
||||||
@@ -52,10 +55,11 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="gm-complete-actions">
|
<div className="gm-complete-actions">
|
||||||
<button className="gm-btn secondary" onClick={onMap}>Mapa</button>
|
<button className="gm-btn secondary" onClick={onMap}>Mapa</button>
|
||||||
<button className="gm-btn secondary" onClick={onRetry}>Reintentar</button>
|
<button className="gm-btn secondary" onClick={onRetry}>
|
||||||
|
{hintPenalty ? '↺ Sin pista' : 'Reintentar'}
|
||||||
|
</button>
|
||||||
{stars >= 1 && !isLastLevel && (
|
{stars >= 1 && !isLastLevel && (
|
||||||
<button className="gm-btn primary" onClick={onNext}>Siguiente →</button>
|
<button className="gm-btn primary" onClick={onNext}>Siguiente →</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,17 +14,16 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
const portPositions = useRef({});
|
const portPositions = useRef({});
|
||||||
const [tempWire, setTempWire] = useState(null);
|
const [tempWire, setTempWire] = useState(null);
|
||||||
const connectingRef = useRef(null);
|
const connectingRef = useRef(null);
|
||||||
const [showConcept, setShowConcept] = useState(true);
|
const [hintUsed, setHintUsed] = useState(false);
|
||||||
const [result, setResult] = useState(null); // { stars, checks }
|
const [showHint, setShowHint] = useState(false);
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
const [targetPlaying, setTargetPlaying] = useState(false);
|
const [targetPlaying, setTargetPlaying] = useState(false);
|
||||||
|
|
||||||
// Subscribe to state changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsub = subscribe(() => forceUpdate(n => n + 1));
|
const unsub = subscribe(() => forceUpdate(n => n + 1));
|
||||||
return unsub;
|
return unsub;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load level on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLevel();
|
loadLevel();
|
||||||
return () => {
|
return () => {
|
||||||
@@ -34,31 +33,25 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
}, [level.id]);
|
}, [level.id]);
|
||||||
|
|
||||||
const loadLevel = useCallback(() => {
|
const loadLevel = useCallback(() => {
|
||||||
// Clear state and load preplaced modules
|
|
||||||
const data = {
|
const data = {
|
||||||
modules: (level.preplacedModules || []).map(m => ({
|
modules: (level.preplacedModules || []).map(m => ({
|
||||||
id: m.id,
|
id: m.id, type: m.type, x: m.x, y: m.y, params: { ...m.params },
|
||||||
type: m.type,
|
|
||||||
x: m.x,
|
|
||||||
y: m.y,
|
|
||||||
params: { ...m.params },
|
|
||||||
})),
|
})),
|
||||||
connections: [],
|
connections: [],
|
||||||
camera: { camX: 0, camY: 0, zoom: 1 },
|
camera: { camX: 0, camY: 0, zoom: 1 },
|
||||||
};
|
};
|
||||||
deserialize(data);
|
deserialize(data);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
setShowConcept(true);
|
setHintUsed(false);
|
||||||
|
setShowHint(false);
|
||||||
if (state.isRunning) stopAudio();
|
if (state.isRunning) stopAudio();
|
||||||
}, [level]);
|
}, [level]);
|
||||||
|
|
||||||
// Port position reporting
|
|
||||||
const handlePortPosition = useCallback((moduleId, portName, direction, el) => {
|
const handlePortPosition = useCallback((moduleId, portName, direction, el) => {
|
||||||
const key = `${moduleId}-${portName}-${direction}`;
|
const key = `${moduleId}-${portName}-${direction}`;
|
||||||
portPositions.current[key] = el;
|
portPositions.current[key] = el;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Start connecting wire
|
|
||||||
const handleStartConnect = useCallback((info) => {
|
const handleStartConnect = useCallback((info) => {
|
||||||
connectingRef.current = info;
|
connectingRef.current = info;
|
||||||
const containerRect = containerRef.current.getBoundingClientRect();
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
@@ -71,14 +64,23 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const findPortAtPoint = (x, y) => {
|
// Robust port detection — searches all port-dots by bounding rect distance
|
||||||
for (const [dx, dy] of [[0,0],[-8,0],[8,0],[0,-8],[0,8],[-6,-6],[6,-6],[-6,6],[6,6]]) {
|
// instead of elementFromPoint (which gets blocked by SVG wire overlay)
|
||||||
const hit = document.elementFromPoint(x + dx, y + dy);
|
const findPortAtPoint = (clientX, clientY) => {
|
||||||
if (hit && hit.classList.contains('port-dot') && hit.dataset.moduleId) {
|
const portDots = document.querySelectorAll('.port-dot[data-module-id]');
|
||||||
return hit;
|
let closest = null;
|
||||||
|
let closestDist = 18;
|
||||||
|
for (const dot of portDots) {
|
||||||
|
const rect = dot.getBoundingClientRect();
|
||||||
|
const cx = rect.left + rect.width / 2;
|
||||||
|
const cy = rect.top + rect.height / 2;
|
||||||
|
const dist = Math.sqrt((clientX - cx) ** 2 + (clientY - cy) ** 2);
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist;
|
||||||
|
closest = dot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return closest;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = useCallback((e) => {
|
const handlePointerDown = useCallback((e) => {
|
||||||
@@ -166,7 +168,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
|
|
||||||
const handleContextMenu = useCallback((e) => e.preventDefault(), []);
|
const handleContextMenu = useCallback((e) => e.preventDefault(), []);
|
||||||
|
|
||||||
// Add module from palette
|
|
||||||
const handleAddModule = (type) => {
|
const handleAddModule = (type) => {
|
||||||
const x = (-state.camX + 250) / state.zoom + Math.random() * 30;
|
const x = (-state.camX + 250) / state.zoom + Math.random() * 30;
|
||||||
const y = (-state.camY + 150) / state.zoom + Math.random() * 30;
|
const y = (-state.camY + 150) / state.zoom + Math.random() * 30;
|
||||||
@@ -174,7 +175,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
if (state.isRunning) rebuildGraph();
|
if (state.isRunning) rebuildGraph();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle player audio
|
|
||||||
const handleToggleAudio = async () => {
|
const handleToggleAudio = async () => {
|
||||||
if (state.isRunning) {
|
if (state.isRunning) {
|
||||||
stopAudio();
|
stopAudio();
|
||||||
@@ -184,7 +184,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
emit();
|
emit();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Play target sound
|
|
||||||
const handlePlayTarget = async () => {
|
const handlePlayTarget = async () => {
|
||||||
if (isTargetPlaying()) {
|
if (isTargetPlaying()) {
|
||||||
stopTarget();
|
stopTarget();
|
||||||
@@ -192,12 +191,16 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
} else {
|
} else {
|
||||||
setTargetPlaying(true);
|
setTargetPlaying(true);
|
||||||
await playTarget(level.target);
|
await playTarget(level.target);
|
||||||
// Auto-update when target stops
|
|
||||||
setTimeout(() => setTargetPlaying(false), (level.target.duration || 2) * 1000 + 100);
|
setTimeout(() => setTargetPlaying(false), (level.target.duration || 2) * 1000 + 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate solution
|
// Reveal hint — permanently caps this attempt at 2 stars
|
||||||
|
const handleRevealHint = () => {
|
||||||
|
setHintUsed(true);
|
||||||
|
setShowHint(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCheck = () => {
|
const handleCheck = () => {
|
||||||
const mods = state.modules;
|
const mods = state.modules;
|
||||||
const conns = state.connections;
|
const conns = state.connections;
|
||||||
@@ -206,14 +209,16 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
passed: check.test(mods, conns),
|
passed: check.test(mods, conns),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Stars: sequential — need all previous stars to earn next
|
|
||||||
let stars = 0;
|
let stars = 0;
|
||||||
for (const check of checks) {
|
for (const check of checks) {
|
||||||
if (check.passed) stars = check.star;
|
if (check.passed) stars = check.star;
|
||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
|
|
||||||
setResult({ stars, checks });
|
// Cap at 2 stars if hint was used
|
||||||
|
if (hintUsed && stars > 2) stars = 2;
|
||||||
|
|
||||||
|
setResult({ stars, checks, hintPenalty: hintUsed && stars >= 2 });
|
||||||
|
|
||||||
if (stars >= 1) {
|
if (stars >= 1) {
|
||||||
completeLevel(level.id, stars);
|
completeLevel(level.id, stars);
|
||||||
@@ -251,20 +256,33 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gm-puzzle-content">
|
<div className="gm-puzzle-content">
|
||||||
{/* Left sidebar: concept + module palette */}
|
{/* Left sidebar */}
|
||||||
<div className="gm-puzzle-sidebar">
|
<div className="gm-puzzle-sidebar">
|
||||||
{/* Concept panel */}
|
{/* Description — always visible */}
|
||||||
<div className="gm-concept-panel">
|
<div className="gm-concept-panel">
|
||||||
<div className="gm-concept-header" onClick={() => setShowConcept(!showConcept)}>
|
<div className="gm-concept-header">
|
||||||
<span>💡 Concepto</span>
|
<span>📖 Mision</span>
|
||||||
<span>{showConcept ? '▼' : '▶'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{showConcept && (
|
<div className="gm-concept-body">
|
||||||
<div className="gm-concept-body">
|
<p className="gm-concept-desc">{level.description}</p>
|
||||||
<p className="gm-concept-desc">{level.description}</p>
|
</div>
|
||||||
<div className="gm-concept-tip">
|
</div>
|
||||||
<strong>Pista:</strong> {level.concept}
|
|
||||||
|
{/* Hint — hidden, reveals with penalty */}
|
||||||
|
<div className="gm-hint-panel">
|
||||||
|
{!showHint ? (
|
||||||
|
<button className="gm-hint-btn" onClick={handleRevealHint}>
|
||||||
|
<span className="gm-hint-icon">💡</span>
|
||||||
|
<span className="gm-hint-label">Mostrar Pista</span>
|
||||||
|
<span className="gm-hint-penalty">max ★★</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="gm-hint-revealed">
|
||||||
|
<div className="gm-hint-header">
|
||||||
|
<span>💡 Pista</span>
|
||||||
|
<span className="gm-hint-penalty-tag">max ★★</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="gm-hint-text">{level.concept}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -274,18 +292,27 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
<div className="gm-obj-title">Objetivos</div>
|
<div className="gm-obj-title">Objetivos</div>
|
||||||
{level.checks.map((check, i) => {
|
{level.checks.map((check, i) => {
|
||||||
const passed = result?.checks?.[i]?.passed;
|
const passed = result?.checks?.[i]?.passed;
|
||||||
|
const cappedByStar = hintUsed && check.star === 3;
|
||||||
return (
|
return (
|
||||||
<div key={i} className={`gm-obj ${passed === true ? 'passed' : passed === false ? 'failed' : ''}`}>
|
<div key={i} className={`gm-obj ${passed === true ? (cappedByStar ? 'capped' : 'passed') : passed === false ? 'failed' : ''}`}>
|
||||||
<span className="gm-obj-star">{'★'.repeat(check.star)}</span>
|
<span className="gm-obj-star">{'★'.repeat(check.star)}</span>
|
||||||
<span className="gm-obj-name">{check.desc}</span>
|
<span className="gm-obj-name">
|
||||||
{passed === true && <span className="gm-obj-check">✓</span>}
|
{check.desc}
|
||||||
|
{cappedByStar && <span className="gm-obj-locked"> 🔒</span>}
|
||||||
|
</span>
|
||||||
|
{passed === true && !cappedByStar && <span className="gm-obj-check">✓</span>}
|
||||||
{passed === false && <span className="gm-obj-x">✗</span>}
|
{passed === false && <span className="gm-obj-x">✗</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{hintUsed && (
|
||||||
|
<div className="gm-hint-warning">
|
||||||
|
Pista usada — maximo 2 estrellas. Reinicia para intentar sin pista.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Module palette for this level */}
|
{/* Module palette */}
|
||||||
{level.availableModules.length > 0 && (
|
{level.availableModules.length > 0 && (
|
||||||
<div className="gm-module-palette">
|
<div className="gm-module-palette">
|
||||||
<div className="gm-palette-title">Modulos Disponibles</div>
|
<div className="gm-palette-title">Modulos Disponibles</div>
|
||||||
@@ -293,11 +320,7 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
const def = getModuleDef(type);
|
const def = getModuleDef(type);
|
||||||
if (!def) return null;
|
if (!def) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={type} className="gm-palette-item" onClick={() => handleAddModule(type)}>
|
||||||
key={type}
|
|
||||||
className="gm-palette-item"
|
|
||||||
onClick={() => handleAddModule(type)}
|
|
||||||
>
|
|
||||||
<span className="gm-palette-icon">{def.icon}</span>
|
<span className="gm-palette-icon">{def.icon}</span>
|
||||||
<span className="gm-palette-name">{def.name}</span>
|
<span className="gm-palette-name">{def.name}</span>
|
||||||
<span className="gm-palette-add">+</span>
|
<span className="gm-palette-add">+</span>
|
||||||
@@ -307,7 +330,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Reset button */}
|
|
||||||
<button className="gm-btn danger" onClick={loadLevel} style={{ marginTop: 'auto' }}>
|
<button className="gm-btn danger" onClick={loadLevel} style={{ marginTop: 'auto' }}>
|
||||||
↺ Reiniciar Nivel
|
↺ Reiniciar Nivel
|
||||||
</button>
|
</button>
|
||||||
@@ -324,7 +346,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
{/* Grid */}
|
|
||||||
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', zIndex: 0 }}>
|
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', zIndex: 0 }}>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="puzzle-grid" width={20 * state.zoom} height={20 * state.zoom}
|
<pattern id="puzzle-grid" width={20 * state.zoom} height={20 * state.zoom}
|
||||||
@@ -336,10 +357,8 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
<rect width="100%" height="100%" fill="url(#puzzle-grid)" />
|
<rect width="100%" height="100%" fill="url(#puzzle-grid)" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* Wires */}
|
|
||||||
<WireLayer portPositions={portPositions} tempWire={tempWire} containerRef={containerRef} zoom={state.zoom} camX={state.camX} camY={state.camY} />
|
<WireLayer portPositions={portPositions} tempWire={tempWire} containerRef={containerRef} zoom={state.zoom} camX={state.camX} camY={state.camY} />
|
||||||
|
|
||||||
{/* Modules */}
|
|
||||||
<div style={{ position: 'absolute', left: state.camX, top: state.camY, zIndex: 2 }}>
|
<div style={{ position: 'absolute', left: state.camX, top: state.camY, zIndex: 2 }}>
|
||||||
{state.modules.map(mod => (
|
{state.modules.map(mod => (
|
||||||
<ModuleNode
|
<ModuleNode
|
||||||
@@ -353,7 +372,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Canvas hints */}
|
|
||||||
{state.modules.length > 0 && state.connections.length === 0 && (
|
{state.modules.length > 0 && state.connections.length === 0 && (
|
||||||
<div className="gm-canvas-hint">
|
<div className="gm-canvas-hint">
|
||||||
Arrastra de un puerto (circulo) a otro para conectar modulos
|
Arrastra de un puerto (circulo) a otro para conectar modulos
|
||||||
@@ -369,6 +387,7 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
|||||||
checks={result.checks}
|
checks={result.checks}
|
||||||
levelTitle={level.title}
|
levelTitle={level.title}
|
||||||
isLastLevel={isLastLevel}
|
isLastLevel={isLastLevel}
|
||||||
|
hintPenalty={result.hintPenalty}
|
||||||
onRetry={loadLevel}
|
onRetry={loadLevel}
|
||||||
onMap={onBack}
|
onMap={onBack}
|
||||||
onNext={onNextLevel}
|
onNext={onNextLevel}
|
||||||
|
|||||||
@@ -421,6 +421,54 @@ html, body, #root {
|
|||||||
.gm-obj.failed .gm-obj-name { color: var(--text2); }
|
.gm-obj.failed .gm-obj-name { color: var(--text2); }
|
||||||
.gm-obj-check { color: var(--green); font-weight: 700; }
|
.gm-obj-check { color: var(--green); font-weight: 700; }
|
||||||
.gm-obj-x { color: var(--red); font-weight: 700; }
|
.gm-obj-x { color: var(--red); font-weight: 700; }
|
||||||
|
.gm-obj.capped .gm-obj-name { color: var(--text2); text-decoration: line-through; }
|
||||||
|
.gm-obj-locked { color: var(--text2); font-size: 10px; }
|
||||||
|
|
||||||
|
.gm-hint-warning {
|
||||||
|
margin-top: 8px; padding: 6px 8px; background: rgba(255,204,0,0.08);
|
||||||
|
border-radius: 4px; font-size: 10px; color: var(--yellow); line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hint panel */
|
||||||
|
.gm-hint-panel { }
|
||||||
|
.gm-hint-btn {
|
||||||
|
width: 100%; display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 10px 12px; border: 1px dashed var(--yellow); border-radius: 8px;
|
||||||
|
background: rgba(255,204,0,0.04); cursor: pointer;
|
||||||
|
font-family: inherit; transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.gm-hint-btn:hover { background: rgba(255,204,0,0.1); border-style: solid; }
|
||||||
|
.gm-hint-icon { font-size: 16px; }
|
||||||
|
.gm-hint-label { font-size: 12px; font-weight: 600; color: var(--yellow); flex: 1; text-align: left; }
|
||||||
|
.gm-hint-penalty {
|
||||||
|
font-size: 9px; color: var(--red); background: rgba(255,68,102,0.15);
|
||||||
|
padding: 2px 6px; border-radius: 3px; font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-hint-revealed {
|
||||||
|
background: var(--surface); border: 1px solid var(--yellow); border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.gm-hint-header {
|
||||||
|
padding: 8px 12px; display: flex; justify-content: space-between; align-items: center;
|
||||||
|
font-size: 12px; font-weight: 600; color: var(--yellow);
|
||||||
|
background: rgba(255,204,0,0.06);
|
||||||
|
}
|
||||||
|
.gm-hint-penalty-tag {
|
||||||
|
font-size: 9px; color: var(--red); background: rgba(255,68,102,0.15);
|
||||||
|
padding: 2px 6px; border-radius: 3px; font-weight: 700;
|
||||||
|
}
|
||||||
|
.gm-hint-text {
|
||||||
|
padding: 8px 12px 12px; font-size: 11px; color: var(--text); line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-hint-penalty-msg {
|
||||||
|
font-size: 11px; color: var(--yellow); margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-big-star.locked {
|
||||||
|
color: var(--border); font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Module palette (game) */
|
/* Module palette (game) */
|
||||||
.gm-module-palette {
|
.gm-module-palette {
|
||||||
|
|||||||
Reference in New Issue
Block a user