Compare commits
2 Commits
08206e996e
...
00c4ec8e00
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00c4ec8e00 | ||
|
|
e077e7f553 |
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)
|
||||
const findPortAtPoint = (x, y) => {
|
||||
// First try exact hit
|
||||
const el = document.elementFromPoint(x, y);
|
||||
if (el && el.classList.contains('port-dot') && el.dataset.moduleId) {
|
||||
return el;
|
||||
}
|
||||
// Try a small radius around the point
|
||||
for (const [dx, dy] of [[0,0],[-8,0],[8,0],[0,-8],[0,8],[-6,-6],[6,-6],[-6,6],[6,6]]) {
|
||||
const hit = document.elementFromPoint(x + dx, y + dy);
|
||||
if (hit && hit.classList.contains('port-dot') && hit.dataset.moduleId) {
|
||||
return hit;
|
||||
// Robust port detection — searches all port-dots by bounding rect distance
|
||||
// instead of elementFromPoint (which gets blocked by SVG wire overlay)
|
||||
const findPortAtPoint = (clientX, clientY) => {
|
||||
const portDots = document.querySelectorAll('.port-dot[data-module-id]');
|
||||
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;
|
||||
};
|
||||
|
||||
// Canvas pointer events
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback } from 'react';
|
||||
import React, { useRef, useCallback, useState } from 'react';
|
||||
|
||||
const SIZE = 32;
|
||||
const RADIUS = 12;
|
||||
@@ -22,6 +22,9 @@ function describeArc(cx, cy, r, startDeg, endDeg) {
|
||||
export default function Knob({ value, min, max, onChange, color = 'var(--accent)', label, unit, formatValue }) {
|
||||
const ref = useRef(null);
|
||||
const dragRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editText, setEditText] = useState('');
|
||||
|
||||
const norm = Math.max(0, Math.min(1, (value - min) / (max - min)));
|
||||
const angleDeg = START_ANGLE - norm * RANGE;
|
||||
@@ -40,6 +43,7 @@ export default function Knob({ value, min, max, onChange, color = 'var(--accent)
|
||||
value.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
|
||||
|
||||
const handlePointerDown = useCallback((e) => {
|
||||
if (editing) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
dragRef.current = { startY: e.clientY, startValue: value };
|
||||
const handleMove = (me) => {
|
||||
@@ -47,7 +51,6 @@ export default function Knob({ value, min, max, onChange, color = 'var(--accent)
|
||||
const sensitivity = (max - min) / 200;
|
||||
let newVal = dragRef.current.startValue + dy * sensitivity;
|
||||
newVal = Math.max(min, Math.min(max, newVal));
|
||||
// Snap to nice values for integer ranges
|
||||
if (max - min > 10 && Number.isInteger(min) && Number.isInteger(max)) {
|
||||
newVal = Math.round(newVal);
|
||||
}
|
||||
@@ -60,17 +63,66 @@ export default function Knob({ value, min, max, onChange, color = 'var(--accent)
|
||||
};
|
||||
window.addEventListener('pointermove', handleMove);
|
||||
window.addEventListener('pointerup', handleUp);
|
||||
}, [value, min, max, onChange]);
|
||||
}, [value, min, max, onChange, editing]);
|
||||
|
||||
const handleWheel = useCallback((e) => {
|
||||
if (editing) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const step = (max - min) / 100;
|
||||
const newVal = Math.max(min, Math.min(max, value - Math.sign(e.deltaY) * step));
|
||||
onChange(newVal);
|
||||
}, [value, min, max, onChange]);
|
||||
}, [value, min, max, onChange, editing]);
|
||||
|
||||
// Double-click: open inline text input
|
||||
const handleDoubleClick = useCallback((e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
setEditText(String(typeof displayVal === 'number' ? displayVal : value));
|
||||
setEditing(true);
|
||||
// Focus input after render
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}, [value, displayVal]);
|
||||
|
||||
const commitEdit = useCallback(() => {
|
||||
const parsed = parseFloat(editText);
|
||||
if (!isNaN(parsed)) {
|
||||
const clamped = Math.max(min, Math.min(max, parsed));
|
||||
onChange(clamped);
|
||||
}
|
||||
setEditing(false);
|
||||
}, [editText, min, max, onChange]);
|
||||
|
||||
const handleInputKeyDown = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') {
|
||||
commitEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditing(false);
|
||||
}
|
||||
}, [commitEdit]);
|
||||
|
||||
const handleInputBlur = useCallback(() => {
|
||||
commitEdit();
|
||||
}, [commitEdit]);
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div className="knob-container knob-editing" onWheel={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="knob-input"
|
||||
type="text"
|
||||
value={editText}
|
||||
onChange={(e) => setEditText(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="knob-container" onWheel={handleWheel}>
|
||||
<div className="knob-container" onWheel={handleWheel} onDoubleClick={handleDoubleClick}>
|
||||
<svg className="knob-svg" viewBox={`0 0 ${SIZE} ${SIZE}`}
|
||||
onPointerDown={handlePointerDown} ref={ref}>
|
||||
<path className="knob-track" d={trackPath} />
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
// Animate stars appearing one by one
|
||||
const timers = [];
|
||||
for (let i = 1; i <= stars; i++) {
|
||||
timers.push(setTimeout(() => setShowStars(i), i * 400));
|
||||
@@ -13,9 +12,9 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
||||
}, [stars]);
|
||||
|
||||
const messages = [
|
||||
'', // 0 stars
|
||||
'',
|
||||
'Has dado el primer paso...',
|
||||
'Buen trabajo — casi perfecto.',
|
||||
hintPenalty ? 'Reinicia sin pista para conseguir 3 estrellas.' : 'Buen trabajo — casi perfecto.',
|
||||
'Ejecucion impecable.',
|
||||
];
|
||||
|
||||
@@ -27,21 +26,25 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
||||
</h2>
|
||||
<p className="gm-complete-level">{levelTitle}</p>
|
||||
|
||||
{/* Stars */}
|
||||
<div className="gm-complete-stars">
|
||||
{[1, 2, 3].map(i => (
|
||||
<span
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{checks.map((check, i) => (
|
||||
<div key={i} className={`gm-check ${check.passed ? 'passed' : 'failed'}`}>
|
||||
@@ -52,10 +55,11 @@ export default function LevelComplete({ stars, checks, levelTitle, onNext, onRet
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="gm-complete-actions">
|
||||
<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 && (
|
||||
<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 [tempWire, setTempWire] = useState(null);
|
||||
const connectingRef = useRef(null);
|
||||
const [showConcept, setShowConcept] = useState(true);
|
||||
const [result, setResult] = useState(null); // { stars, checks }
|
||||
const [hintUsed, setHintUsed] = useState(false);
|
||||
const [showHint, setShowHint] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
const [targetPlaying, setTargetPlaying] = useState(false);
|
||||
|
||||
// Subscribe to state changes
|
||||
useEffect(() => {
|
||||
const unsub = subscribe(() => forceUpdate(n => n + 1));
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
// Load level on mount
|
||||
useEffect(() => {
|
||||
loadLevel();
|
||||
return () => {
|
||||
@@ -34,31 +33,25 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
}, [level.id]);
|
||||
|
||||
const loadLevel = useCallback(() => {
|
||||
// Clear state and load preplaced modules
|
||||
const data = {
|
||||
modules: (level.preplacedModules || []).map(m => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
x: m.x,
|
||||
y: m.y,
|
||||
params: { ...m.params },
|
||||
id: m.id, type: m.type, x: m.x, y: m.y, params: { ...m.params },
|
||||
})),
|
||||
connections: [],
|
||||
camera: { camX: 0, camY: 0, zoom: 1 },
|
||||
};
|
||||
deserialize(data);
|
||||
setResult(null);
|
||||
setShowConcept(true);
|
||||
setHintUsed(false);
|
||||
setShowHint(false);
|
||||
if (state.isRunning) stopAudio();
|
||||
}, [level]);
|
||||
|
||||
// Port position reporting
|
||||
const handlePortPosition = useCallback((moduleId, portName, direction, el) => {
|
||||
const key = `${moduleId}-${portName}-${direction}`;
|
||||
portPositions.current[key] = el;
|
||||
}, []);
|
||||
|
||||
// Start connecting wire
|
||||
const handleStartConnect = useCallback((info) => {
|
||||
connectingRef.current = info;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
@@ -71,14 +64,23 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
});
|
||||
}, []);
|
||||
|
||||
const findPortAtPoint = (x, y) => {
|
||||
for (const [dx, dy] of [[0,0],[-8,0],[8,0],[0,-8],[0,8],[-6,-6],[6,-6],[-6,6],[6,6]]) {
|
||||
const hit = document.elementFromPoint(x + dx, y + dy);
|
||||
if (hit && hit.classList.contains('port-dot') && hit.dataset.moduleId) {
|
||||
return hit;
|
||||
// Robust port detection — searches all port-dots by bounding rect distance
|
||||
// instead of elementFromPoint (which gets blocked by SVG wire overlay)
|
||||
const findPortAtPoint = (clientX, clientY) => {
|
||||
const portDots = document.querySelectorAll('.port-dot[data-module-id]');
|
||||
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) => {
|
||||
@@ -166,7 +168,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
|
||||
const handleContextMenu = useCallback((e) => e.preventDefault(), []);
|
||||
|
||||
// Add module from palette
|
||||
const handleAddModule = (type) => {
|
||||
const x = (-state.camX + 250) / 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();
|
||||
};
|
||||
|
||||
// Toggle player audio
|
||||
const handleToggleAudio = async () => {
|
||||
if (state.isRunning) {
|
||||
stopAudio();
|
||||
@@ -184,7 +184,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
emit();
|
||||
};
|
||||
|
||||
// Play target sound
|
||||
const handlePlayTarget = async () => {
|
||||
if (isTargetPlaying()) {
|
||||
stopTarget();
|
||||
@@ -192,12 +191,16 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
} else {
|
||||
setTargetPlaying(true);
|
||||
await playTarget(level.target);
|
||||
// Auto-update when target stops
|
||||
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 mods = state.modules;
|
||||
const conns = state.connections;
|
||||
@@ -206,14 +209,16 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
passed: check.test(mods, conns),
|
||||
}));
|
||||
|
||||
// Stars: sequential — need all previous stars to earn next
|
||||
let stars = 0;
|
||||
for (const check of checks) {
|
||||
if (check.passed) stars = check.star;
|
||||
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) {
|
||||
completeLevel(level.id, stars);
|
||||
@@ -251,20 +256,33 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
</div>
|
||||
|
||||
<div className="gm-puzzle-content">
|
||||
{/* Left sidebar: concept + module palette */}
|
||||
{/* Left sidebar */}
|
||||
<div className="gm-puzzle-sidebar">
|
||||
{/* Concept panel */}
|
||||
{/* Description — always visible */}
|
||||
<div className="gm-concept-panel">
|
||||
<div className="gm-concept-header" onClick={() => setShowConcept(!showConcept)}>
|
||||
<span>💡 Concepto</span>
|
||||
<span>{showConcept ? '▼' : '▶'}</span>
|
||||
<div className="gm-concept-header">
|
||||
<span>📖 Mision</span>
|
||||
</div>
|
||||
{showConcept && (
|
||||
<div className="gm-concept-body">
|
||||
<p className="gm-concept-desc">{level.description}</p>
|
||||
<div className="gm-concept-tip">
|
||||
<strong>Pista:</strong> {level.concept}
|
||||
<div className="gm-concept-body">
|
||||
<p className="gm-concept-desc">{level.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<p className="gm-hint-text">{level.concept}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -274,18 +292,27 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
<div className="gm-obj-title">Objetivos</div>
|
||||
{level.checks.map((check, i) => {
|
||||
const passed = result?.checks?.[i]?.passed;
|
||||
const cappedByStar = hintUsed && check.star === 3;
|
||||
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-name">{check.desc}</span>
|
||||
{passed === true && <span className="gm-obj-check">✓</span>}
|
||||
<span className="gm-obj-name">
|
||||
{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>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{hintUsed && (
|
||||
<div className="gm-hint-warning">
|
||||
Pista usada — maximo 2 estrellas. Reinicia para intentar sin pista.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Module palette for this level */}
|
||||
{/* Module palette */}
|
||||
{level.availableModules.length > 0 && (
|
||||
<div className="gm-module-palette">
|
||||
<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);
|
||||
if (!def) return null;
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
className="gm-palette-item"
|
||||
onClick={() => handleAddModule(type)}
|
||||
>
|
||||
<div key={type} className="gm-palette-item" onClick={() => handleAddModule(type)}>
|
||||
<span className="gm-palette-icon">{def.icon}</span>
|
||||
<span className="gm-palette-name">{def.name}</span>
|
||||
<span className="gm-palette-add">+</span>
|
||||
@@ -307,7 +330,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reset button */}
|
||||
<button className="gm-btn danger" onClick={loadLevel} style={{ marginTop: 'auto' }}>
|
||||
↺ Reiniciar Nivel
|
||||
</button>
|
||||
@@ -324,7 +346,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
onWheel={handleWheel}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{/* Grid */}
|
||||
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', zIndex: 0 }}>
|
||||
<defs>
|
||||
<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)" />
|
||||
</svg>
|
||||
|
||||
{/* Wires */}
|
||||
<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 }}>
|
||||
{state.modules.map(mod => (
|
||||
<ModuleNode
|
||||
@@ -353,7 +372,6 @@ export default function PuzzleView({ level, levelIndex, worldLevels, onBack, onN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas hints */}
|
||||
{state.modules.length > 0 && state.connections.length === 0 && (
|
||||
<div className="gm-canvas-hint">
|
||||
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}
|
||||
levelTitle={level.title}
|
||||
isLastLevel={isLastLevel}
|
||||
hintPenalty={result.hintPenalty}
|
||||
onRetry={loadLevel}
|
||||
onMap={onBack}
|
||||
onNext={onNextLevel}
|
||||
|
||||
@@ -168,6 +168,15 @@ html, body, #root {
|
||||
.knob-fill { fill: none; stroke-width: 3; stroke-linecap: round; }
|
||||
.knob-dot { fill: var(--text); }
|
||||
|
||||
.knob-editing { display: flex; align-items: center; justify-content: center; }
|
||||
.knob-input {
|
||||
width: 48px; height: 22px; padding: 0 4px;
|
||||
background: var(--bg); border: 1px solid var(--accent); border-radius: 3px;
|
||||
color: var(--accent); font-size: 11px; font-family: 'JetBrains Mono', monospace;
|
||||
text-align: center; outline: none;
|
||||
}
|
||||
.knob-input:focus { box-shadow: 0 0 6px rgba(0,229,255,0.3); }
|
||||
|
||||
.param-value {
|
||||
font-size: 10px; color: var(--accent); font-family: 'JetBrains Mono', monospace;
|
||||
min-width: 40px; text-align: right;
|
||||
@@ -421,6 +430,54 @@ html, body, #root {
|
||||
.gm-obj.failed .gm-obj-name { color: var(--text2); }
|
||||
.gm-obj-check { color: var(--green); 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) */
|
||||
.gm-module-palette {
|
||||
|
||||
Reference in New Issue
Block a user