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:
Thomas Hallock
2025-12-01 08:32:47 -06:00
parent 7c496525e9
commit f8acc4aa6a
3 changed files with 94 additions and 27 deletions

View File

@@ -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.**

View File

@@ -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>
)}

View File

@@ -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) */}