solanapolis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +518 -0
  2. package/bin/solanapolis.js +197 -0
  3. package/convex/_generated/api.d.ts +175 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/convex/agent/conversation.ts +352 -0
  9. package/convex/agent/embeddingsCache.ts +110 -0
  10. package/convex/agent/memory.ts +450 -0
  11. package/convex/agent/schema.ts +53 -0
  12. package/convex/aiChat.ts +54 -0
  13. package/convex/aiTown/agent.ts +382 -0
  14. package/convex/aiTown/agentDescription.ts +27 -0
  15. package/convex/aiTown/agentInputs.ts +155 -0
  16. package/convex/aiTown/agentOperations.ts +178 -0
  17. package/convex/aiTown/conversation.ts +395 -0
  18. package/convex/aiTown/conversationMembership.ts +38 -0
  19. package/convex/aiTown/game.ts +371 -0
  20. package/convex/aiTown/ids.ts +32 -0
  21. package/convex/aiTown/inputHandler.ts +9 -0
  22. package/convex/aiTown/inputs.ts +25 -0
  23. package/convex/aiTown/insertInput.ts +20 -0
  24. package/convex/aiTown/location.ts +32 -0
  25. package/convex/aiTown/main.ts +154 -0
  26. package/convex/aiTown/movement.ts +189 -0
  27. package/convex/aiTown/player.ts +310 -0
  28. package/convex/aiTown/playerDescription.ts +35 -0
  29. package/convex/aiTown/schema.ts +79 -0
  30. package/convex/aiTown/world.ts +65 -0
  31. package/convex/aiTown/worldMap.ts +74 -0
  32. package/convex/chat.ts +79 -0
  33. package/convex/constants.ts +78 -0
  34. package/convex/convex.config.ts +6 -0
  35. package/convex/crons.ts +89 -0
  36. package/convex/engine/abstractGame.ts +199 -0
  37. package/convex/engine/historicalObject.ts +355 -0
  38. package/convex/engine/schema.ts +56 -0
  39. package/convex/http.ts +36 -0
  40. package/convex/init.ts +110 -0
  41. package/convex/messages.ts +53 -0
  42. package/convex/npcCarAgents.ts +415 -0
  43. package/convex/schema.ts +61 -0
  44. package/convex/streaming.ts +23 -0
  45. package/convex/testing.ts +202 -0
  46. package/convex/tsconfig.json +18 -0
  47. package/convex/util/FastIntegerCompression.ts +221 -0
  48. package/convex/util/assertNever.ts +4 -0
  49. package/convex/util/asyncMap.ts +20 -0
  50. package/convex/util/compression.ts +71 -0
  51. package/convex/util/geometry.ts +132 -0
  52. package/convex/util/isSimpleObject.ts +11 -0
  53. package/convex/util/llm.ts +724 -0
  54. package/convex/util/minheap.ts +38 -0
  55. package/convex/util/object.ts +22 -0
  56. package/convex/util/sleep.ts +3 -0
  57. package/convex/util/types.ts +33 -0
  58. package/convex/util/xxhash.ts +228 -0
  59. package/convex/world.ts +257 -0
  60. package/data/animations/campfire.json +45 -0
  61. package/data/animations/gentlesparkle.json +37 -0
  62. package/data/animations/gentlesplash.json +61 -0
  63. package/data/animations/gentlewaterfall.json +61 -0
  64. package/data/animations/windmill.json +78 -0
  65. package/data/characters.ts +121 -0
  66. package/data/convertMap.js +74 -0
  67. package/data/gentle.js +330 -0
  68. package/data/spritesheets/f1.ts +75 -0
  69. package/data/spritesheets/f2.ts +75 -0
  70. package/data/spritesheets/f3.ts +75 -0
  71. package/data/spritesheets/f4.ts +75 -0
  72. package/data/spritesheets/f5.ts +75 -0
  73. package/data/spritesheets/f6.ts +75 -0
  74. package/data/spritesheets/f7.ts +75 -0
  75. package/data/spritesheets/f8.ts +75 -0
  76. package/data/spritesheets/p1.ts +59 -0
  77. package/data/spritesheets/p2.ts +59 -0
  78. package/data/spritesheets/p3.ts +59 -0
  79. package/data/spritesheets/player.ts +59 -0
  80. package/data/spritesheets/types.ts +26 -0
  81. package/eslint.config.mjs +37 -0
  82. package/next.config.ts +7 -0
  83. package/package.json +85 -0
  84. package/postcss.config.mjs +7 -0
  85. package/public/file.svg +1 -0
  86. package/public/globe.svg +1 -0
  87. package/public/helius-icon.svg +84 -0
  88. package/public/helius-logo.svg +85 -0
  89. package/public/next.svg +1 -0
  90. package/public/plane.glb +0 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/scripts/clear-city.ts +74 -0
  94. package/scripts/seed-wallets.ts +185 -0
  95. package/scripts/setup-webhook.ts +73 -0
  96. package/src/app/api/auth/callback/route.ts +6 -0
  97. package/src/app/api/auth/link-wallet/route.ts +6 -0
  98. package/src/app/api/auth/phantom/route.ts +6 -0
  99. package/src/app/api/broadcast-position/route.ts +59 -0
  100. package/src/app/api/leaderboard/route.ts +85 -0
  101. package/src/app/api/network-stats/route.ts +86 -0
  102. package/src/app/api/parcel-reward/route.ts +181 -0
  103. package/src/app/api/queue-status/route.ts +30 -0
  104. package/src/app/api/snapshots/route.ts +37 -0
  105. package/src/app/api/transactions/enhanced/route.ts +57 -0
  106. package/src/app/api/treasury/route.ts +83 -0
  107. package/src/app/api/wallet/[address]/balances/route.ts +124 -0
  108. package/src/app/api/wallet/[address]/identity/route.ts +32 -0
  109. package/src/app/api/wallet/[address]/route.ts +216 -0
  110. package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
  111. package/src/app/api/wallets/route.ts +68 -0
  112. package/src/app/api/webhooks/helius/route.ts +76 -0
  113. package/src/app/auth/callback/page.tsx +29 -0
  114. package/src/app/favicon.ico +0 -0
  115. package/src/app/globals.css +39 -0
  116. package/src/app/layout.tsx +43 -0
  117. package/src/app/page.tsx +16 -0
  118. package/src/components/AITownNPCs.tsx +206 -0
  119. package/src/components/ActivityFeed.tsx +189 -0
  120. package/src/components/AuthPanel.tsx +163 -0
  121. package/src/components/BeachScene.tsx +280 -0
  122. package/src/components/Building.tsx +138 -0
  123. package/src/components/CesiumFlight.tsx +1768 -0
  124. package/src/components/CesiumGlobe.tsx +616 -0
  125. package/src/components/CitizenCard.tsx +442 -0
  126. package/src/components/CitizenCardModal.tsx +153 -0
  127. package/src/components/CityGrid.tsx +313 -0
  128. package/src/components/CityLandmarks.tsx +427 -0
  129. package/src/components/CityScene.tsx +1289 -0
  130. package/src/components/CitySlotsBadge.tsx +68 -0
  131. package/src/components/CockpitHUD.tsx +460 -0
  132. package/src/components/ConvexWrapper.tsx +19 -0
  133. package/src/components/DubaiDistrict.tsx +630 -0
  134. package/src/components/FlightMiniMap.tsx +133 -0
  135. package/src/components/GameChat.tsx +383 -0
  136. package/src/components/GameHUD.tsx +393 -0
  137. package/src/components/Ground.tsx +14 -0
  138. package/src/components/HowItWorksModal.tsx +251 -0
  139. package/src/components/IngestionBanner.tsx +123 -0
  140. package/src/components/InstancedBuildings.tsx +316 -0
  141. package/src/components/InstancedCars.tsx +504 -0
  142. package/src/components/InstancedCityPlanes.tsx +259 -0
  143. package/src/components/InstancedHouses.tsx +246 -0
  144. package/src/components/InstancedLampPosts.tsx +201 -0
  145. package/src/components/InstancedResidentCars.tsx +357 -0
  146. package/src/components/InstancedRoadDashes.tsx +42 -0
  147. package/src/components/InstancedSkyscrapers.tsx +434 -0
  148. package/src/components/InstancedTrees.tsx +67 -0
  149. package/src/components/LeaderboardPanel.tsx +136 -0
  150. package/src/components/MultiplayerPlanes.tsx +128 -0
  151. package/src/components/NetworkStats.tsx +83 -0
  152. package/src/components/NewBuildingSpotlight.tsx +93 -0
  153. package/src/components/ParcelChallengeBanner.tsx +242 -0
  154. package/src/components/ParcelReward.tsx +191 -0
  155. package/src/components/Park.tsx +42 -0
  156. package/src/components/PhantomWrapper.tsx +22 -0
  157. package/src/components/PixelStreamViewer.tsx +335 -0
  158. package/src/components/PlaneMode.tsx +190 -0
  159. package/src/components/PlayerCar.tsx +211 -0
  160. package/src/components/PlayerPlane.tsx +255 -0
  161. package/src/components/ProjectileRenderer.tsx +249 -0
  162. package/src/components/QueueStatusBanner.tsx +86 -0
  163. package/src/components/RealPlayerTags.tsx +82 -0
  164. package/src/components/SceneLighting.tsx +382 -0
  165. package/src/components/SelectionBeam.tsx +59 -0
  166. package/src/components/SwapPanel.tsx +104 -0
  167. package/src/components/SwapParticles.tsx +237 -0
  168. package/src/components/TreasureGate.tsx +505 -0
  169. package/src/components/WalletPanel.tsx +421 -0
  170. package/src/components/WalletSearch.tsx +244 -0
  171. package/src/components/WelcomeOverlay.tsx +135 -0
  172. package/src/components/WindowTooltip.tsx +498 -0
  173. package/src/context/AuthContext.tsx +230 -0
  174. package/src/lib/bot-detection.ts +125 -0
  175. package/src/lib/building-math.ts +136 -0
  176. package/src/lib/building-shader.ts +253 -0
  177. package/src/lib/car-paths.ts +244 -0
  178. package/src/lib/car-system.ts +182 -0
  179. package/src/lib/city-constants.ts +29 -0
  180. package/src/lib/city-slots.ts +35 -0
  181. package/src/lib/city-zoning.ts +64 -0
  182. package/src/lib/collision-map.ts +147 -0
  183. package/src/lib/day-night.ts +252 -0
  184. package/src/lib/export-card.ts +28 -0
  185. package/src/lib/helius-webhook.ts +90 -0
  186. package/src/lib/helius.ts +74 -0
  187. package/src/lib/house-shader.ts +119 -0
  188. package/src/lib/mock-data.ts +56 -0
  189. package/src/lib/multiplayer-manager.ts +329 -0
  190. package/src/lib/plane-physics.ts +66 -0
  191. package/src/lib/player-car.ts +147 -0
  192. package/src/lib/player-plane.ts +200 -0
  193. package/src/lib/projectile-system.ts +272 -0
  194. package/src/lib/skyscraper-types.ts +52 -0
  195. package/src/lib/sound-engine.ts +464 -0
  196. package/src/lib/supabase-admin.ts +9 -0
  197. package/src/lib/supabase.ts +8 -0
  198. package/src/lib/swap-events.ts +70 -0
  199. package/src/middleware.ts +37 -0
  200. package/src/types/phantom.d.ts +16 -0
  201. package/src/types/wallet.ts +20 -0
  202. package/tsconfig.json +34 -0
@@ -0,0 +1,382 @@
1
+ import { ObjectType, v } from 'convex/values';
2
+ import { GameId, parseGameId } from './ids';
3
+ import { agentId, conversationId, playerId } from './ids';
4
+ import { serializedPlayer } from './player';
5
+ import { Game } from './game';
6
+ import {
7
+ ACTION_TIMEOUT,
8
+ AWKWARD_CONVERSATION_TIMEOUT,
9
+ CONVERSATION_COOLDOWN,
10
+ CONVERSATION_DISTANCE,
11
+ INVITE_ACCEPT_PROBABILITY,
12
+ INVITE_TIMEOUT,
13
+ MAX_CONVERSATION_DURATION,
14
+ MAX_CONVERSATION_MESSAGES,
15
+ MESSAGE_COOLDOWN,
16
+ MIDPOINT_THRESHOLD,
17
+ PLAYER_CONVERSATION_COOLDOWN,
18
+ } from '../constants';
19
+ import { FunctionArgs } from 'convex/server';
20
+ import { MutationCtx, internalMutation, internalQuery } from '../_generated/server';
21
+ import { distance } from '../util/geometry';
22
+ import { internal } from '../_generated/api';
23
+ import { movePlayer } from './movement';
24
+ import { insertInput } from './insertInput';
25
+
26
+ export class Agent {
27
+ id: GameId<'agents'>;
28
+ playerId: GameId<'players'>;
29
+ toRemember?: GameId<'conversations'>;
30
+ lastConversation?: number;
31
+ lastInviteAttempt?: number;
32
+ inProgressOperation?: {
33
+ name: string;
34
+ operationId: string;
35
+ started: number;
36
+ };
37
+
38
+ constructor(serialized: SerializedAgent) {
39
+ const { id, lastConversation, lastInviteAttempt, inProgressOperation } = serialized;
40
+ const playerId = parseGameId('players', serialized.playerId);
41
+ this.id = parseGameId('agents', id);
42
+ this.playerId = playerId;
43
+ this.toRemember =
44
+ serialized.toRemember !== undefined
45
+ ? parseGameId('conversations', serialized.toRemember)
46
+ : undefined;
47
+ this.lastConversation = lastConversation;
48
+ this.lastInviteAttempt = lastInviteAttempt;
49
+ this.inProgressOperation = inProgressOperation;
50
+ }
51
+
52
+ tick(game: Game, now: number) {
53
+ const player = game.world.players.get(this.playerId);
54
+ if (!player) {
55
+ throw new Error(`Invalid player ID ${this.playerId}`);
56
+ }
57
+ if (this.inProgressOperation) {
58
+ if (now < this.inProgressOperation.started + ACTION_TIMEOUT) {
59
+ // Wait on the operation to finish.
60
+ return;
61
+ }
62
+ console.log(`Timing out ${JSON.stringify(this.inProgressOperation)}`);
63
+ delete this.inProgressOperation;
64
+ }
65
+ const conversation = game.world.playerConversation(player);
66
+ const member = conversation?.participants.get(player.id);
67
+
68
+ const recentlyAttemptedInvite =
69
+ this.lastInviteAttempt && now < this.lastInviteAttempt + CONVERSATION_COOLDOWN;
70
+ const doingActivity = player.activity && player.activity.until > now;
71
+ if (doingActivity && (conversation || player.pathfinding)) {
72
+ player.activity!.until = now;
73
+ }
74
+ // If we're not in a conversation, do something.
75
+ // If we aren't doing an activity or moving, do something.
76
+ // If we have been wandering but haven't thought about something to do for
77
+ // a while, do something.
78
+ if (!conversation && !doingActivity && (!player.pathfinding || !recentlyAttemptedInvite)) {
79
+ this.startOperation(game, now, 'agentDoSomething', {
80
+ worldId: game.worldId,
81
+ player: player.serialize(),
82
+ otherFreePlayers: [...game.world.players.values()]
83
+ .filter((p) => p.id !== player.id)
84
+ .filter(
85
+ (p) => ![...game.world.conversations.values()].find((c) => c.participants.has(p.id)),
86
+ )
87
+ .map((p) => p.serialize()),
88
+ agent: this.serialize(),
89
+ map: game.worldMap.serialize(),
90
+ });
91
+ return;
92
+ }
93
+ // Check to see if we have a conversation we need to remember.
94
+ if (this.toRemember) {
95
+ // Fire off the action to remember the conversation.
96
+ console.log(`Agent ${this.id} remembering conversation ${this.toRemember}`);
97
+ this.startOperation(game, now, 'agentRememberConversation', {
98
+ worldId: game.worldId,
99
+ playerId: this.playerId,
100
+ agentId: this.id,
101
+ conversationId: this.toRemember,
102
+ });
103
+ delete this.toRemember;
104
+ return;
105
+ }
106
+ if (conversation && member) {
107
+ const [otherPlayerId, otherMember] = [...conversation.participants.entries()].find(
108
+ ([id]) => id !== player.id,
109
+ )!;
110
+ const otherPlayer = game.world.players.get(otherPlayerId)!;
111
+ if (member.status.kind === 'invited') {
112
+ // Accept a conversation with another agent with some probability and with
113
+ // a human unconditionally.
114
+ if (otherPlayer.human || Math.random() < INVITE_ACCEPT_PROBABILITY) {
115
+ console.log(`Agent ${player.id} accepting invite from ${otherPlayer.id}`);
116
+ conversation.acceptInvite(game, player);
117
+ // Stop moving so we can start walking towards the other player.
118
+ if (player.pathfinding) {
119
+ delete player.pathfinding;
120
+ }
121
+ } else {
122
+ console.log(`Agent ${player.id} rejecting invite from ${otherPlayer.id}`);
123
+ conversation.rejectInvite(game, now, player);
124
+ }
125
+ return;
126
+ }
127
+ if (member.status.kind === 'walkingOver') {
128
+ // Leave a conversation if we've been waiting for too long.
129
+ if (member.invited + INVITE_TIMEOUT < now) {
130
+ console.log(`Giving up on invite to ${otherPlayer.id}`);
131
+ conversation.leave(game, now, player);
132
+ return;
133
+ }
134
+
135
+ // Don't keep moving around if we're near enough.
136
+ const playerDistance = distance(player.position, otherPlayer.position);
137
+ if (playerDistance < CONVERSATION_DISTANCE) {
138
+ return;
139
+ }
140
+
141
+ // Keep moving towards the other player.
142
+ // If we're close enough to the player, just walk to them directly.
143
+ if (!player.pathfinding) {
144
+ let destination;
145
+ if (playerDistance < MIDPOINT_THRESHOLD) {
146
+ destination = {
147
+ x: Math.floor(otherPlayer.position.x),
148
+ y: Math.floor(otherPlayer.position.y),
149
+ };
150
+ } else {
151
+ destination = {
152
+ x: Math.floor((player.position.x + otherPlayer.position.x) / 2),
153
+ y: Math.floor((player.position.y + otherPlayer.position.y) / 2),
154
+ };
155
+ }
156
+ console.log(`Agent ${player.id} walking towards ${otherPlayer.id}...`, destination);
157
+ movePlayer(game, now, player, destination);
158
+ }
159
+ return;
160
+ }
161
+ if (member.status.kind === 'participating') {
162
+ const started = member.status.started;
163
+ if (conversation.isTyping && conversation.isTyping.playerId !== player.id) {
164
+ // Wait for the other player to finish typing.
165
+ return;
166
+ }
167
+ if (!conversation.lastMessage) {
168
+ const isInitiator = conversation.creator === player.id;
169
+ const awkwardDeadline = started + AWKWARD_CONVERSATION_TIMEOUT;
170
+ // Send the first message if we're the initiator or if we've been waiting for too long.
171
+ if (isInitiator || awkwardDeadline < now) {
172
+ // Grab the lock on the conversation and send a "start" message.
173
+ console.log(`${player.id} initiating conversation with ${otherPlayer.id}.`);
174
+ const messageUuid = crypto.randomUUID();
175
+ conversation.setIsTyping(now, player, messageUuid);
176
+ this.startOperation(game, now, 'agentGenerateMessage', {
177
+ worldId: game.worldId,
178
+ playerId: player.id,
179
+ agentId: this.id,
180
+ conversationId: conversation.id,
181
+ otherPlayerId: otherPlayer.id,
182
+ messageUuid,
183
+ type: 'start',
184
+ });
185
+ return;
186
+ } else {
187
+ // Wait on the other player to say something up to the awkward deadline.
188
+ return;
189
+ }
190
+ }
191
+ // See if the conversation has been going on too long and decide to leave.
192
+ const tooLongDeadline = started + MAX_CONVERSATION_DURATION;
193
+ if (tooLongDeadline < now || conversation.numMessages > MAX_CONVERSATION_MESSAGES) {
194
+ console.log(`${player.id} leaving conversation with ${otherPlayer.id}.`);
195
+ const messageUuid = crypto.randomUUID();
196
+ conversation.setIsTyping(now, player, messageUuid);
197
+ this.startOperation(game, now, 'agentGenerateMessage', {
198
+ worldId: game.worldId,
199
+ playerId: player.id,
200
+ agentId: this.id,
201
+ conversationId: conversation.id,
202
+ otherPlayerId: otherPlayer.id,
203
+ messageUuid,
204
+ type: 'leave',
205
+ });
206
+ return;
207
+ }
208
+ // Wait for the awkward deadline if we sent the last message.
209
+ if (conversation.lastMessage.author === player.id) {
210
+ const awkwardDeadline = conversation.lastMessage.timestamp + AWKWARD_CONVERSATION_TIMEOUT;
211
+ if (now < awkwardDeadline) {
212
+ return;
213
+ }
214
+ }
215
+ // Wait for a cooldown after the last message to simulate "reading" the message.
216
+ const messageCooldown = conversation.lastMessage.timestamp + MESSAGE_COOLDOWN;
217
+ if (now < messageCooldown) {
218
+ return;
219
+ }
220
+ // Grab the lock and send a message!
221
+ console.log(`${player.id} continuing conversation with ${otherPlayer.id}.`);
222
+ const messageUuid = crypto.randomUUID();
223
+ conversation.setIsTyping(now, player, messageUuid);
224
+ this.startOperation(game, now, 'agentGenerateMessage', {
225
+ worldId: game.worldId,
226
+ playerId: player.id,
227
+ agentId: this.id,
228
+ conversationId: conversation.id,
229
+ otherPlayerId: otherPlayer.id,
230
+ messageUuid,
231
+ type: 'continue',
232
+ });
233
+ return;
234
+ }
235
+ }
236
+ }
237
+
238
+ startOperation<Name extends keyof AgentOperations>(
239
+ game: Game,
240
+ now: number,
241
+ name: Name,
242
+ args: Omit<FunctionArgs<AgentOperations[Name]>, 'operationId'>,
243
+ ) {
244
+ if (this.inProgressOperation) {
245
+ throw new Error(
246
+ `Agent ${this.id} already has an operation: ${JSON.stringify(this.inProgressOperation)}`,
247
+ );
248
+ }
249
+ const operationId = game.allocId('operations');
250
+ console.log(`Agent ${this.id} starting operation ${name} (${operationId})`);
251
+ game.scheduleOperation(name, { operationId, ...args } as any);
252
+ this.inProgressOperation = {
253
+ name,
254
+ operationId,
255
+ started: now,
256
+ };
257
+ }
258
+
259
+ serialize(): SerializedAgent {
260
+ return {
261
+ id: this.id,
262
+ playerId: this.playerId,
263
+ toRemember: this.toRemember,
264
+ lastConversation: this.lastConversation,
265
+ lastInviteAttempt: this.lastInviteAttempt,
266
+ inProgressOperation: this.inProgressOperation,
267
+ };
268
+ }
269
+ }
270
+
271
+ export const serializedAgent = {
272
+ id: agentId,
273
+ playerId: playerId,
274
+ toRemember: v.optional(conversationId),
275
+ lastConversation: v.optional(v.number()),
276
+ lastInviteAttempt: v.optional(v.number()),
277
+ inProgressOperation: v.optional(
278
+ v.object({
279
+ name: v.string(),
280
+ operationId: v.string(),
281
+ started: v.number(),
282
+ }),
283
+ ),
284
+ };
285
+ export type SerializedAgent = ObjectType<typeof serializedAgent>;
286
+
287
+ type AgentOperations = typeof internal.aiTown.agentOperations;
288
+
289
+ export async function runAgentOperation(ctx: MutationCtx, operation: string, args: any) {
290
+ let reference;
291
+ switch (operation) {
292
+ case 'agentRememberConversation':
293
+ reference = internal.aiTown.agentOperations.agentRememberConversation;
294
+ break;
295
+ case 'agentGenerateMessage':
296
+ reference = internal.aiTown.agentOperations.agentGenerateMessage;
297
+ break;
298
+ case 'agentDoSomething':
299
+ reference = internal.aiTown.agentOperations.agentDoSomething;
300
+ break;
301
+ default:
302
+ throw new Error(`Unknown operation: ${operation}`);
303
+ }
304
+ await ctx.scheduler.runAfter(0, reference, args);
305
+ }
306
+
307
+ export const agentSendMessage = internalMutation({
308
+ args: {
309
+ worldId: v.id('worlds'),
310
+ conversationId,
311
+ agentId,
312
+ playerId,
313
+ text: v.string(),
314
+ messageUuid: v.string(),
315
+ leaveConversation: v.boolean(),
316
+ operationId: v.string(),
317
+ },
318
+ handler: async (ctx, args) => {
319
+ await ctx.db.insert('messages', {
320
+ conversationId: args.conversationId,
321
+ author: args.playerId,
322
+ text: args.text,
323
+ messageUuid: args.messageUuid,
324
+ worldId: args.worldId,
325
+ });
326
+
327
+ // Mirror agent message into city chat so players can see NPC conversations
328
+ const playerDesc = await ctx.db
329
+ .query('playerDescriptions')
330
+ .withIndex('worldId', (q) => q.eq('worldId', args.worldId).eq('playerId', args.playerId))
331
+ .first();
332
+ const agentName = playerDesc?.name ?? `NPC-${args.playerId}`;
333
+ await ctx.db.insert('chat_messages', {
334
+ wallet_address: `npc-${args.playerId}`,
335
+ display_name: agentName,
336
+ message: args.text.slice(0, 280),
337
+ is_ai: true,
338
+ });
339
+
340
+ await insertInput(ctx, args.worldId, 'agentFinishSendingMessage', {
341
+ conversationId: args.conversationId,
342
+ agentId: args.agentId,
343
+ timestamp: Date.now(),
344
+ leaveConversation: args.leaveConversation,
345
+ operationId: args.operationId,
346
+ });
347
+ },
348
+ });
349
+
350
+ export const findConversationCandidate = internalQuery({
351
+ args: {
352
+ now: v.number(),
353
+ worldId: v.id('worlds'),
354
+ player: v.object(serializedPlayer),
355
+ otherFreePlayers: v.array(v.object(serializedPlayer)),
356
+ },
357
+ handler: async (ctx, { now, worldId, player, otherFreePlayers }) => {
358
+ const { position } = player;
359
+ const candidates = [];
360
+
361
+ for (const otherPlayer of otherFreePlayers) {
362
+ // Find the latest conversation we're both members of.
363
+ const lastMember = await ctx.db
364
+ .query('participatedTogether')
365
+ .withIndex('edge', (q) =>
366
+ q.eq('worldId', worldId).eq('player1', player.id).eq('player2', otherPlayer.id),
367
+ )
368
+ .order('desc')
369
+ .first();
370
+ if (lastMember) {
371
+ if (now < lastMember.ended + PLAYER_CONVERSATION_COOLDOWN) {
372
+ continue;
373
+ }
374
+ }
375
+ candidates.push({ id: otherPlayer.id, position });
376
+ }
377
+
378
+ // Sort by distance and take the nearest candidate.
379
+ candidates.sort((a, b) => distance(a.position, position) - distance(b.position, position));
380
+ return candidates[0]?.id;
381
+ },
382
+ });
@@ -0,0 +1,27 @@
1
+ import { ObjectType, v } from 'convex/values';
2
+ import { GameId, agentId, parseGameId } from './ids';
3
+
4
+ export class AgentDescription {
5
+ agentId: GameId<'agents'>;
6
+ identity: string;
7
+ plan: string;
8
+
9
+ constructor(serialized: SerializedAgentDescription) {
10
+ const { agentId, identity, plan } = serialized;
11
+ this.agentId = parseGameId('agents', agentId);
12
+ this.identity = identity;
13
+ this.plan = plan;
14
+ }
15
+
16
+ serialize(): SerializedAgentDescription {
17
+ const { agentId, identity, plan } = this;
18
+ return { agentId, identity, plan };
19
+ }
20
+ }
21
+
22
+ export const serializedAgentDescription = {
23
+ agentId,
24
+ identity: v.string(),
25
+ plan: v.string(),
26
+ };
27
+ export type SerializedAgentDescription = ObjectType<typeof serializedAgentDescription>;
@@ -0,0 +1,155 @@
1
+ import { v } from 'convex/values';
2
+ import { agentId, conversationId, parseGameId } from './ids';
3
+ import { Player, activity } from './player';
4
+ import { Conversation, conversationInputs } from './conversation';
5
+ import { movePlayer } from './movement';
6
+ import { inputHandler } from './inputHandler';
7
+ import { point } from '../util/types';
8
+ import { Descriptions } from '../../data/characters';
9
+ import { AgentDescription } from './agentDescription';
10
+ import { Agent } from './agent';
11
+
12
+ export const agentInputs = {
13
+ finishRememberConversation: inputHandler({
14
+ args: {
15
+ operationId: v.string(),
16
+ agentId,
17
+ },
18
+ handler: (game, now, args) => {
19
+ const agentId = parseGameId('agents', args.agentId);
20
+ const agent = game.world.agents.get(agentId);
21
+ if (!agent) {
22
+ throw new Error(`Couldn't find agent: ${agentId}`);
23
+ }
24
+ if (
25
+ !agent.inProgressOperation ||
26
+ agent.inProgressOperation.operationId !== args.operationId
27
+ ) {
28
+ console.debug(`Agent ${agentId} isn't remembering ${args.operationId}`);
29
+ } else {
30
+ delete agent.inProgressOperation;
31
+ delete agent.toRemember;
32
+ }
33
+ return null;
34
+ },
35
+ }),
36
+ finishDoSomething: inputHandler({
37
+ args: {
38
+ operationId: v.string(),
39
+ agentId: v.id('agents'),
40
+ destination: v.optional(point),
41
+ invitee: v.optional(v.id('players')),
42
+ activity: v.optional(activity),
43
+ },
44
+ handler: (game, now, args) => {
45
+ const agentId = parseGameId('agents', args.agentId);
46
+ const agent = game.world.agents.get(agentId);
47
+ if (!agent) {
48
+ throw new Error(`Couldn't find agent: ${agentId}`);
49
+ }
50
+ if (
51
+ !agent.inProgressOperation ||
52
+ agent.inProgressOperation.operationId !== args.operationId
53
+ ) {
54
+ console.debug(`Agent ${agentId} didn't have ${args.operationId} in progress`);
55
+ return null;
56
+ }
57
+ delete agent.inProgressOperation;
58
+ const player = game.world.players.get(agent.playerId)!;
59
+ if (args.invitee) {
60
+ const inviteeId = parseGameId('players', args.invitee);
61
+ const invitee = game.world.players.get(inviteeId);
62
+ if (!invitee) {
63
+ throw new Error(`Couldn't find player: ${inviteeId}`);
64
+ }
65
+ Conversation.start(game, now, player, invitee);
66
+ agent.lastInviteAttempt = now;
67
+ }
68
+ if (args.destination) {
69
+ movePlayer(game, now, player, args.destination);
70
+ }
71
+ if (args.activity) {
72
+ player.activity = args.activity;
73
+ }
74
+ return null;
75
+ },
76
+ }),
77
+ agentFinishSendingMessage: inputHandler({
78
+ args: {
79
+ agentId,
80
+ conversationId,
81
+ timestamp: v.number(),
82
+ operationId: v.string(),
83
+ leaveConversation: v.boolean(),
84
+ },
85
+ handler: (game, now, args) => {
86
+ const agentId = parseGameId('agents', args.agentId);
87
+ const agent = game.world.agents.get(agentId);
88
+ if (!agent) {
89
+ throw new Error(`Couldn't find agent: ${agentId}`);
90
+ }
91
+ const player = game.world.players.get(agent.playerId);
92
+ if (!player) {
93
+ throw new Error(`Couldn't find player: ${agent.playerId}`);
94
+ }
95
+ const conversationId = parseGameId('conversations', args.conversationId);
96
+ const conversation = game.world.conversations.get(conversationId);
97
+ if (!conversation) {
98
+ throw new Error(`Couldn't find conversation: ${conversationId}`);
99
+ }
100
+ if (
101
+ !agent.inProgressOperation ||
102
+ agent.inProgressOperation.operationId !== args.operationId
103
+ ) {
104
+ console.debug(`Agent ${agentId} wasn't sending a message ${args.operationId}`);
105
+ return null;
106
+ }
107
+ delete agent.inProgressOperation;
108
+ conversationInputs.finishSendingMessage.handler(game, now, {
109
+ playerId: agent.playerId,
110
+ conversationId: args.conversationId,
111
+ timestamp: args.timestamp,
112
+ });
113
+ if (args.leaveConversation) {
114
+ conversation.leave(game, now, player);
115
+ }
116
+ return null;
117
+ },
118
+ }),
119
+ createAgent: inputHandler({
120
+ args: {
121
+ descriptionIndex: v.number(),
122
+ },
123
+ handler: (game, now, args) => {
124
+ const description = Descriptions[args.descriptionIndex];
125
+ const playerId = Player.join(
126
+ game,
127
+ now,
128
+ description.name,
129
+ description.character,
130
+ description.identity,
131
+ );
132
+ const agentId = game.allocId('agents');
133
+ game.world.agents.set(
134
+ agentId,
135
+ new Agent({
136
+ id: agentId,
137
+ playerId: playerId,
138
+ inProgressOperation: undefined,
139
+ lastConversation: undefined,
140
+ lastInviteAttempt: undefined,
141
+ toRemember: undefined,
142
+ }),
143
+ );
144
+ game.agentDescriptions.set(
145
+ agentId,
146
+ new AgentDescription({
147
+ agentId: agentId,
148
+ identity: description.identity,
149
+ plan: description.plan,
150
+ }),
151
+ );
152
+ return { agentId };
153
+ },
154
+ }),
155
+ };