diff --git a/public/assets/water.jpg b/public/assets/water.jpg new file mode 100644 index 0000000..c63ac49 Binary files /dev/null and b/public/assets/water.jpg differ diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index 2499f5e..a05b1e2 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -88,6 +88,7 @@ export default function VillageView() { const animsRef = useRef>(new Map()); const bubblesRef = useRef([]); const toolImagesRef = useRef>(new Map()); + const waterImgRef = useRef(null); const hitRectsRef = useRef< Array<{ id: string; x: number; y: number; w: number; h: number }> >([]); @@ -109,6 +110,12 @@ export default function VillageView() { img.src = src; img.onload = () => toolImagesRef.current.set(key as AptitudeKey, img); } + // 강물 텍스처 (로드 전에는 단색 폴백) + { + const img = new Image(); + img.src = "/assets/water.jpg"; + img.onload = () => (waterImgRef.current = img); + } let raf = 0; let last = performance.now(); @@ -188,28 +195,73 @@ export default function VillageView() { ctx.clearRect(0, 0, CANVAS_W, CANVAS_H); // 하늘 배경 없음(투명) — 타일부터 + const tilePath = (i: number, j: number) => { + const x = isoX(i, j); + const y = isoY(i, j); + ctx.moveTo(x, y); + ctx.lineTo(x + TW / 2, y + TH / 2); + ctx.lineTo(x, y + TH); + ctx.lineTo(x - TW / 2, y + TH / 2); + ctx.closePath(); + }; + for (let j = 0; j < ROWS; j++) { for (let i = 0; i < COLS; i++) { - const x = isoX(i, j); - const y = isoY(i, j); + if (isWater(i, j)) continue; ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + TW / 2, y + TH / 2); - ctx.lineTo(x, y + TH); - ctx.lineTo(x - TW / 2, y + TH / 2); - ctx.closePath(); - if (isWater(i, j)) { - const shimmer = 0.85 + 0.15 * Math.sin(now / 700 + j); - ctx.fillStyle = `rgba(96, 165, 250, ${shimmer})`; - } else { - ctx.fillStyle = (i + j) % 2 === 0 ? "#86c66b" : "#7dbd62"; - } + tilePath(i, j); + ctx.fillStyle = (i + j) % 2 === 0 ? "#86c66b" : "#7dbd62"; ctx.fill(); ctx.strokeStyle = "rgba(255,255,255,0.10)"; ctx.stroke(); } } + // 강물 — 일러스트 텍스처를 물 타일 영역에 클리핑해서 채움 + { + ctx.save(); + ctx.beginPath(); + for (let j = 0; j < ROWS; j++) + for (let i = 0; i < COLS; i++) if (isWater(i, j)) tilePath(i, j); + ctx.clip(); + + const water = waterImgRef.current; + if (water) { + // 물 영역 바운딩 박스를 덮도록 cover 배치 + 흐르는 방향으로 느린 드리프트 + const left = isoX(COLS - 1, ROWS - 1) - TW / 2; + const right = isoX(COLS - 1, 0) + TW / 2; + const top = isoY(COLS - 1, 0); + const bottom = isoY(COLS - 1, ROWS - 1) + TH; + const boxW = right - left; + const boxH = bottom - top; + const scale = Math.max(boxW / water.width, boxH / water.height) * 1.15; + const w = water.width * scale; + const h = water.height * scale; + // 강 줄기 방향(좌하향)으로 왕복 드리프트 + const drift = Math.sin(now / 4000) * 14; + ctx.drawImage( + water, + left + (boxW - w) / 2 - drift, + top + (boxH - h) / 2 + drift / 2, + w, + h, + ); + } else { + ctx.fillStyle = "#60a5fa"; + ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); + } + + // 은은한 반짝임 오버레이 + for (let j = 0; j < ROWS; j++) { + const glint = 0.05 + 0.05 * Math.sin(now / 700 + j); + ctx.beginPath(); + tilePath(COLS - 1, j); + ctx.fillStyle = `rgba(255,255,255,${glint})`; + ctx.fill(); + } + ctx.restore(); + } + // 그릴 것들을 y 순으로 정렬해 겹침 처리 type Sprite = { y: number; draw: () => void }; const sprites: Sprite[] = [];