fix(know-your-world): use getBBox() for consistent takeover positioning
- Add hidden SVG to measure accurate bounding box via getBBox() for part 1 - Both takeover parts now use getBBox() instead of different calculation methods - Add vectorEffect="non-scaling-stroke" to takeover region shapes - Add stroke to takeover region shape for visual consistency - Add vectorEffect to label pointer lines in MapRenderer - Document failure pattern in CLAUDE.md: verify agreed approach is implemented everywhere, not just obvious cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,34 @@
|
||||
- Lose reproduction state for bugs being debugged
|
||||
- Annoy the user with preventable errors
|
||||
|
||||
## CRITICAL: When Agreeing on a Technical Approach, Actually Implement It Everywhere
|
||||
|
||||
**This is a documented failure pattern. Do not repeat it.**
|
||||
|
||||
When you agree with the user on a technical approach (e.g., "use getBBox() for bounding box calculation"):
|
||||
|
||||
1. **Identify ALL code paths affected** - not just the obvious one
|
||||
2. **Explicitly verify each code path uses the agreed approach** before saying it's done
|
||||
3. **When fixes don't work, FIRST verify the agreed approach was actually implemented everywhere** - don't add patches on top of a broken foundation
|
||||
|
||||
**The failure pattern:**
|
||||
- User and Claude agree: "Part 1 and Part 2 should both use method X"
|
||||
- Claude implements method X for Part 2 (the obvious case)
|
||||
- Claude leaves Part 1 using the old method Y
|
||||
- User reports Part 1 is broken
|
||||
- Claude makes superficial fixes (adjust padding, tweak parameters) instead of realizing Part 1 never used method X
|
||||
- Cycle repeats until user is frustrated
|
||||
|
||||
**What to do instead:**
|
||||
- Before implementing: "Part 1 will use [exact method], Part 2 will use [exact method]"
|
||||
- After implementing: Verify BOTH actually use the agreed method
|
||||
- When debugging: First question should be "did I actually implement what we agreed on everywhere?"
|
||||
|
||||
**Why this matters:**
|
||||
- Users cannot verify every line of code you write
|
||||
- They trust that when you agree to do something, you actually do it
|
||||
- Superficial fixes waste everyone's time when the root cause is incomplete implementation
|
||||
|
||||
## CRITICAL: Documentation Graph Requirement
|
||||
|
||||
**ALL documentation must be reachable from the main README via a linked path.**
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useSpring, animated } from '@react-spring/web'
|
||||
import { useViewerId } from '@/lib/arcade/game-sdk'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import {
|
||||
calculateBoundingBox,
|
||||
DEFAULT_DIFFICULTY_CONFIG,
|
||||
getAssistanceLevel,
|
||||
getCountryFlagEmoji,
|
||||
@@ -220,23 +219,45 @@ export function GameInfoPanel({
|
||||
const displayFlagEmoji =
|
||||
selectedMap === 'world' && displayRegionId ? getCountryFlagEmoji(displayRegionId) : ''
|
||||
|
||||
// Get the region's SVG path for the takeover shape display (no padding to match animation)
|
||||
const displayRegionShape = useMemo(() => {
|
||||
// Get the region's SVG path for the takeover shape display
|
||||
const displayRegionPath = useMemo(() => {
|
||||
if (!displayRegionId) return null
|
||||
const region = mapData.regions.find((r) => r.id === displayRegionId)
|
||||
if (!region?.path) return null
|
||||
|
||||
// Calculate bounding box WITHOUT padding - must match puzzlePieceTarget.svgBBox approach
|
||||
// so part 1 (typing) and part 2 (animation) show the region at the same position
|
||||
const bbox = calculateBoundingBox([region.path])
|
||||
const viewBox = `${bbox.minX} ${bbox.minY} ${bbox.width} ${bbox.height}`
|
||||
|
||||
return {
|
||||
path: region.path,
|
||||
viewBox,
|
||||
}
|
||||
return region?.path ?? null
|
||||
}, [displayRegionId, mapData.regions])
|
||||
|
||||
// Ref to hidden path element for accurate getBBox measurement
|
||||
const hiddenPathRef = useRef<SVGPathElement>(null)
|
||||
|
||||
// Accurate bounding box from getBBox() - updated when region changes
|
||||
const [accurateBBox, setAccurateBBox] = useState<{
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
} | null>(null)
|
||||
|
||||
// Measure accurate bounding box using hidden SVG + getBBox()
|
||||
// This ensures part 1 and part 2 use identical positioning
|
||||
useEffect(() => {
|
||||
if (hiddenPathRef.current && displayRegionPath) {
|
||||
// Use requestAnimationFrame to ensure path is rendered
|
||||
requestAnimationFrame(() => {
|
||||
if (hiddenPathRef.current) {
|
||||
const bbox = hiddenPathRef.current.getBBox()
|
||||
setAccurateBBox({
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setAccurateBBox(null)
|
||||
}
|
||||
}, [displayRegionPath])
|
||||
|
||||
// Get the region's SVG path for puzzle piece animation
|
||||
// Uses the exact SVG bounding box from getBBox() passed in the target
|
||||
const puzzlePieceShape = useMemo(() => {
|
||||
@@ -616,6 +637,24 @@ export function GameInfoPanel({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hidden SVG for accurate getBBox measurement - ensures consistent positioning */}
|
||||
{displayRegionPath && (
|
||||
<svg
|
||||
data-element="hidden-bbox-measure"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
top: '-9999px',
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<path ref={hiddenPathRef} d={displayRegionPath} />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* Global keyframes for animations */}
|
||||
<style>{`
|
||||
@keyframes glowPulse {
|
||||
@@ -707,18 +746,13 @@ export function GameInfoPanel({
|
||||
/>
|
||||
|
||||
{/* Region shape silhouette - shown during takeover, until animation starts */}
|
||||
{/* Must check isInTakeoverLocal to prevent flash when animation ends */}
|
||||
{displayRegionShape && isInTakeoverLocal && !isPuzzlePieceAnimating && (() => {
|
||||
// Parse viewBox to get aspect ratio
|
||||
const viewBox = puzzlePieceTarget?.svgBBox
|
||||
? `${puzzlePieceTarget.svgBBox.x} ${puzzlePieceTarget.svgBBox.y} ${puzzlePieceTarget.svgBBox.width} ${puzzlePieceTarget.svgBBox.height}`
|
||||
: displayRegionShape.viewBox
|
||||
|
||||
// Extract width/height from viewBox for aspect ratio calculation
|
||||
const viewBoxParts = viewBox.split(' ').map(Number)
|
||||
const viewBoxWidth = viewBoxParts[2] || 1
|
||||
const viewBoxHeight = viewBoxParts[3] || 1
|
||||
const aspectRatio = viewBoxWidth / viewBoxHeight
|
||||
{/* Uses accurateBBox from getBBox() for consistent positioning between parts 1 and 2 */}
|
||||
{displayRegionPath && accurateBBox && isInTakeoverLocal && !isPuzzlePieceAnimating && (() => {
|
||||
// Use puzzlePieceTarget.svgBBox if available (part 2), otherwise use accurateBBox (part 1)
|
||||
// Both come from getBBox() so positioning should be identical
|
||||
const bbox = puzzlePieceTarget?.svgBBox ?? accurateBBox
|
||||
const viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`
|
||||
const aspectRatio = bbox.width / bbox.height
|
||||
|
||||
// Calculate container size that fits within 60% of viewport while preserving aspect ratio
|
||||
const maxSize = typeof window !== 'undefined'
|
||||
@@ -758,8 +792,11 @@ export function GameInfoPanel({
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d={displayRegionShape.path}
|
||||
d={displayRegionPath}
|
||||
fill={isDark ? 'rgba(59, 130, 246, 0.5)' : 'rgba(59, 130, 246, 0.35)'}
|
||||
stroke={isDark ? '#3b82f6' : '#2563eb'}
|
||||
strokeWidth={2}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -787,6 +824,7 @@ export function GameInfoPanel({
|
||||
fill={isDark ? 'rgba(59, 130, 246, 0.8)' : 'rgba(59, 130, 246, 0.6)'}
|
||||
stroke={isDark ? '#3b82f6' : '#2563eb'}
|
||||
strokeWidth={2}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</animated.svg>
|
||||
)}
|
||||
|
||||
@@ -3520,6 +3520,7 @@ export function MapRenderer({
|
||||
y2={label.lineEndY}
|
||||
stroke={label.isFound ? '#16a34a' : isDark ? '#60a5fa' : '#3b82f6'}
|
||||
strokeWidth={1.5}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
markerEnd={label.isFound ? 'url(#arrowhead-found)' : 'url(#arrowhead)'}
|
||||
/>
|
||||
{/* Debug: Show arrow endpoint (region centroid) */}
|
||||
|
||||
Reference in New Issue
Block a user