Muthukumarank commited on
Commit
3fe8701
Β·
verified Β·
1 Parent(s): 4b0c711

Add components/town/TownMap.tsx

Browse files
Files changed (1) hide show
  1. components/town/TownMap.tsx +266 -0
components/town/TownMap.tsx ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AI Town β€” Town Map Renderer (React Native + Skia)
3
+ * ====================================================
4
+ * 2D isometric-style town with pan/zoom, character sprites,
5
+ * and tap-to-interact functionality.
6
+ */
7
+
8
+ import React, { useMemo } from "react";
9
+ import { View, Dimensions, Text, TouchableOpacity } from "react-native";
10
+ import { Canvas, Rect, Circle, Group, RoundedRect, Text as SkiaText, useFont } from "@shopify/react-native-skia";
11
+ import { GestureDetector, Gesture } from "react-native-gesture-handler";
12
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
13
+
14
+ const TILE_SIZE = 32;
15
+ const SCREEN = Dimensions.get("window");
16
+
17
+ // Tile colors
18
+ const TILE_COLORS: Record<string, string> = {
19
+ grass: "#4ade80",
20
+ path: "#d4a574",
21
+ flowers: "#f472b6",
22
+ tree: "#166534",
23
+ water: "#60a5fa",
24
+ sand: "#fde047",
25
+ };
26
+
27
+ // Building colors
28
+ const BUILDING_COLORS: Record<string, string> = {
29
+ house: "#f97316",
30
+ cafe: "#8b5cf6",
31
+ park: "#22c55e",
32
+ library: "#3b82f6",
33
+ shop: "#ec4899",
34
+ gym: "#ef4444",
35
+ restaurant: "#f59e0b",
36
+ office: "#6366f1",
37
+ };
38
+
39
+ interface TownMapProps {
40
+ tiles: string[][];
41
+ buildings: any[];
42
+ characters: any[];
43
+ onTileTap?: (x: number, y: number) => void;
44
+ onCharacterTap?: (character: any) => void;
45
+ onBuildingTap?: (building: any) => void;
46
+ }
47
+
48
+ export function TownMap({
49
+ tiles,
50
+ buildings,
51
+ characters,
52
+ onTileTap,
53
+ onCharacterTap,
54
+ onBuildingTap,
55
+ }: TownMapProps) {
56
+ const offsetX = useSharedValue(0);
57
+ const offsetY = useSharedValue(0);
58
+ const scale = useSharedValue(1);
59
+ const savedOffsetX = useSharedValue(0);
60
+ const savedOffsetY = useSharedValue(0);
61
+ const savedScale = useSharedValue(1);
62
+
63
+ const gridWidth = tiles[0]?.length || 20;
64
+ const gridHeight = tiles.length || 20;
65
+ const canvasWidth = gridWidth * TILE_SIZE;
66
+ const canvasHeight = gridHeight * TILE_SIZE;
67
+
68
+ // Pan gesture
69
+ const panGesture = Gesture.Pan()
70
+ .onStart(() => {
71
+ savedOffsetX.value = offsetX.value;
72
+ savedOffsetY.value = offsetY.value;
73
+ })
74
+ .onUpdate((e) => {
75
+ offsetX.value = savedOffsetX.value + e.translationX;
76
+ offsetY.value = savedOffsetY.value + e.translationY;
77
+ });
78
+
79
+ // Pinch gesture (zoom)
80
+ const pinchGesture = Gesture.Pinch()
81
+ .onStart(() => {
82
+ savedScale.value = scale.value;
83
+ })
84
+ .onUpdate((e) => {
85
+ scale.value = Math.max(0.5, Math.min(3, savedScale.value * e.scale));
86
+ });
87
+
88
+ // Tap gesture (place/interact)
89
+ const tapGesture = Gesture.Tap().onEnd((e) => {
90
+ const tileX = Math.floor((e.x - offsetX.value) / (TILE_SIZE * scale.value));
91
+ const tileY = Math.floor((e.y - offsetY.value) / (TILE_SIZE * scale.value));
92
+
93
+ // Check if tapped on a character
94
+ const tappedChar = characters.find(
95
+ (c) => c.positionX === tileX && c.positionY === tileY
96
+ );
97
+ if (tappedChar) {
98
+ onCharacterTap?.(tappedChar);
99
+ return;
100
+ }
101
+
102
+ // Check if tapped on a building
103
+ const tappedBuilding = buildings.find(
104
+ (b) =>
105
+ tileX >= b.positionX &&
106
+ tileX < b.positionX + b.width &&
107
+ tileY >= b.positionY &&
108
+ tileY < b.positionY + b.height
109
+ );
110
+ if (tappedBuilding) {
111
+ onBuildingTap?.(tappedBuilding);
112
+ return;
113
+ }
114
+
115
+ // Empty tile tap
116
+ onTileTap?.(tileX, tileY);
117
+ });
118
+
119
+ const composed = Gesture.Simultaneous(panGesture, pinchGesture);
120
+ const allGestures = Gesture.Exclusive(tapGesture, composed);
121
+
122
+ const animatedStyle = useAnimatedStyle(() => ({
123
+ transform: [
124
+ { translateX: offsetX.value },
125
+ { translateY: offsetY.value },
126
+ { scale: scale.value },
127
+ ],
128
+ }));
129
+
130
+ return (
131
+ <GestureDetector gesture={allGestures}>
132
+ <Animated.View style={[{ width: SCREEN.width, height: SCREEN.height * 0.6 }, animatedStyle]}>
133
+ <Canvas style={{ width: canvasWidth, height: canvasHeight }}>
134
+ {/* Render tiles */}
135
+ {tiles.map((row, y) =>
136
+ row.map((tile, x) => (
137
+ <Rect
138
+ key={`${x}-${y}`}
139
+ x={x * TILE_SIZE}
140
+ y={y * TILE_SIZE}
141
+ width={TILE_SIZE - 1}
142
+ height={TILE_SIZE - 1}
143
+ color={TILE_COLORS[tile] || "#4ade80"}
144
+ />
145
+ ))
146
+ )}
147
+
148
+ {/* Render buildings */}
149
+ {buildings.map((building, i) => (
150
+ <Group key={`building-${i}`}>
151
+ <RoundedRect
152
+ x={building.positionX * TILE_SIZE}
153
+ y={building.positionY * TILE_SIZE}
154
+ width={building.width * TILE_SIZE - 2}
155
+ height={building.height * TILE_SIZE - 2}
156
+ r={4}
157
+ color={BUILDING_COLORS[building.type] || "#6b7280"}
158
+ />
159
+ {/* Building label */}
160
+ <Rect
161
+ x={building.positionX * TILE_SIZE}
162
+ y={building.positionY * TILE_SIZE}
163
+ width={building.width * TILE_SIZE - 2}
164
+ height={12}
165
+ color="rgba(0,0,0,0.5)"
166
+ />
167
+ </Group>
168
+ ))}
169
+
170
+ {/* Render characters as colored circles */}
171
+ {characters.map((char, i) => (
172
+ <Group key={`char-${i}`}>
173
+ {/* Shadow */}
174
+ <Circle
175
+ cx={char.positionX * TILE_SIZE + TILE_SIZE / 2}
176
+ cy={char.positionY * TILE_SIZE + TILE_SIZE / 2 + 2}
177
+ r={TILE_SIZE / 3}
178
+ color="rgba(0,0,0,0.3)"
179
+ />
180
+ {/* Character */}
181
+ <Circle
182
+ cx={char.positionX * TILE_SIZE + TILE_SIZE / 2}
183
+ cy={char.positionY * TILE_SIZE + TILE_SIZE / 2}
184
+ r={TILE_SIZE / 3}
185
+ color={getMoodColor(char.mood)}
186
+ />
187
+ {/* Direction indicator */}
188
+ <Circle
189
+ cx={char.positionX * TILE_SIZE + TILE_SIZE / 2}
190
+ cy={char.positionY * TILE_SIZE + TILE_SIZE / 4}
191
+ r={3}
192
+ color="#ffffff"
193
+ />
194
+ </Group>
195
+ ))}
196
+ </Canvas>
197
+ </Animated.View>
198
+ </GestureDetector>
199
+ );
200
+ }
201
+
202
+ function getMoodColor(mood: string): string {
203
+ switch (mood) {
204
+ case "happy": return "#fbbf24";
205
+ case "excited": return "#f97316";
206
+ case "curious": return "#8b5cf6";
207
+ case "anxious": return "#ef4444";
208
+ case "bored": return "#6b7280";
209
+ case "content": return "#34d399";
210
+ default: return "#60a5fa";
211
+ }
212
+ }
213
+
214
+ // ═══ Character Info Panel (shows on tap) ═══
215
+
216
+ interface CharacterPanelProps {
217
+ character: any;
218
+ onChat: () => void;
219
+ onClose: () => void;
220
+ }
221
+
222
+ export function CharacterPanel({ character, onChat, onClose }: CharacterPanelProps) {
223
+ return (
224
+ <View className="absolute bottom-0 left-0 right-0 bg-gray-900/95 rounded-t-3xl p-6 border-t border-gray-700">
225
+ <View className="flex-row items-center justify-between mb-4">
226
+ <View className="flex-row items-center gap-3">
227
+ <View
228
+ className="w-12 h-12 rounded-full items-center justify-center"
229
+ style={{ backgroundColor: getMoodColor(character.mood) }}
230
+ >
231
+ <Text className="text-xl">{character.avatar}</Text>
232
+ </View>
233
+ <View>
234
+ <Text className="text-white font-bold text-lg">{character.name}</Text>
235
+ <Text className="text-gray-400 text-sm">{character.occupation}</Text>
236
+ </View>
237
+ </View>
238
+ <TouchableOpacity onPress={onClose}>
239
+ <Text className="text-gray-400 text-2xl">βœ•</Text>
240
+ </TouchableOpacity>
241
+ </View>
242
+
243
+ <View className="flex-row gap-2 mb-3">
244
+ {character.traits?.slice(0, 3).map((trait: string, i: number) => (
245
+ <View key={i} className="bg-purple-500/20 px-2 py-1 rounded-full">
246
+ <Text className="text-purple-300 text-xs">{trait}</Text>
247
+ </View>
248
+ ))}
249
+ </View>
250
+
251
+ <Text className="text-gray-300 text-sm mb-2">
252
+ 🎭 Mood: <Text className="text-white font-medium">{character.mood}</Text>
253
+ </Text>
254
+ <Text className="text-gray-300 text-sm mb-4">
255
+ πŸ“ Doing: <Text className="text-white font-medium">{character.currentAction}</Text>
256
+ </Text>
257
+
258
+ <TouchableOpacity
259
+ onPress={onChat}
260
+ className="bg-blue-600 py-3 rounded-xl items-center"
261
+ >
262
+ <Text className="text-white font-bold">πŸ’¬ Chat with {character.name}</Text>
263
+ </TouchableOpacity>
264
+ </View>
265
+ );
266
+ }