Overworld ais#1370
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR introduces a complete overworld AI NPC placement system with period-capped quest completion tracking. It adds new DB schema ( ChangesOverworld AI NPC System
Sequence Diagram(s)sequenceDiagram
participant Player as Player (Sector)
participant getSectorData as travelRouter.getSectorData
participant OWAiRouter as overworldAiRouter
participant getNewTrackers as quest.getNewTrackers
participant DB as Database
Player->>getSectorData: getSectorData(sector)
getSectorData->>DB: query overworldAiPlacement (active, sector)
DB-->>getSectorData: placements + aiTemplate
getSectorData->>getSectorData: placementToSectorUser(placement, template)
getSectorData-->>Player: { users, overworldAis, ... }
Player->>Player: click NPC talk/attack mesh
Player->>OWAiRouter: interactWithOverworldAi(placementId, positionVersion, ...)
OWAiRouter->>DB: validate placement + actor (parallel)
OWAiRouter->>OWAiRouter: check active + positionVersion + actor tile
alt HOSTILE interaction
OWAiRouter->>OWAiRouter: initiateBattle(OVERWORLD)
OWAiRouter-->>Player: battle started
else FRIENDLY + bound objective
OWAiRouter->>getNewTrackers: findActionableBoundObjective
OWAiRouter->>OWAiRouter: commitQuestObjectiveRewards
OWAiRouter-->>Player: dialog branches / reward claimed
else FRIENDLY + quest giver
OWAiRouter->>DB: CAS increment dailyOverworldQuestRolls
OWAiRouter->>OWAiRouter: filter eligible quest pool
OWAiRouter->>OWAiRouter: pickWeightedQuest (deterministic roll)
OWAiRouter->>OWAiRouter: assignQuestToUser
OWAiRouter-->>Player: quest assigned or no quest
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (21): Last reviewed commit: "chore: regenerate migrations after rebas..." | Re-trigger Greptile |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/tests/libs/quest.periodcap.test.ts (1)
20-21: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winStrengthen the null-period test so it actually validates null handling.
This case uses
periodCompletes: 0, so it stays uncapped even without exercising the null reset behavior. Use a value abovemaxCompletes(e.g.,periodCompletes: 5) to ensureperiodStartAt: nullis what makes it pass.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/tests/libs/quest.periodcap.test.ts` around lines 20 - 21, The test for null periodStartAt in periodCapReached does not properly validate the null reset behavior. Currently it uses periodCompletes: 0 which keeps the period uncapped regardless of the null check, so the test would pass even if null handling was broken. Change the periodCompletes value to something greater than maxCompletes (such as 5) so that the test actually exercises the null handling logic and confirms that a null periodStartAt prevents capping even when periodCompletes exceeds the maxCompletes threshold.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@app/tests/libs/quest.periodcap.test.ts`:
- Around line 20-21: The test for null periodStartAt in periodCapReached does
not properly validate the null reset behavior. Currently it uses
periodCompletes: 0 which keeps the period uncapped regardless of the null check,
so the test would pass even if null handling was broken. Change the
periodCompletes value to something greater than maxCompletes (such as 5) so that
the test actually exercises the null handling logic and confirms that a null
periodStartAt prevents capping even when periodCompletes exceeds the
maxCompletes threshold.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d3476956-fcde-4da5-a458-553c6b4f6d32
📒 Files selected for processing (2)
app/src/libs/quest.tsapp/tests/libs/quest.periodcap.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/libs/quest.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/drizzle/migrations/0022_long_grim_reaper.sql`:
- Line 10: The Quest migration currently adds attemptDelay with a default of
none, which drops existing retry cadence data from retryDelay. Update the
migration to preserve current values by renaming or altering the existing column
in place, or by adding the new column, backfilling it from retryDelay, and then
dropping the old column in the same migration. Use the Quest table change and
attemptDelay/retryDelay column names as the reference points.
In `@app/src/hooks/quest.ts`:
- Around line 241-246: The `attemptDelay` field in `useQuest` is being added
unconditionally, but it should only be included for quest types that support
capped attempts. Update the `formData.push` logic in `useQuest` to gate the
`attemptDelay` entry behind `QuestTypesWithMaxAttempts.includes(questType)`,
matching the surrounding attempt-related fields so unsupported quest types
cannot configure it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 01ba2814-abcc-443d-939b-a7c43dbe1c82
📒 Files selected for processing (9)
app/drizzle/migrations/0022_long_grim_reaper.sqlapp/drizzle/migrations/meta/0022_snapshot.jsonapp/drizzle/migrations/meta/_journal.jsonapp/drizzle/schema.tsapp/src/hooks/quest.tsapp/src/libs/quest.tsapp/src/server/api/routers/overworldAi.tsapp/src/validators/objectives.tsapp/tests/libs/quest.attemptcap.test.ts
✅ Files skipped from review due to trivial changes (1)
- app/tests/libs/quest.attemptcap.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- app/src/validators/objectives.ts
- app/drizzle/migrations/meta/_journal.json
- app/src/server/api/routers/overworldAi.ts
- app/src/libs/quest.ts
Replace the PR's hand-numbered 0017-0022 migrations (which collided with main's 0017_uneven_sister_grimm) with a single regenerated 0018_violet_turbo migration capturing the overworld AI schema additions.
3c7508a to
9f349d0
Compare
|
Rebased this branch onto the latest Conflicts resolved:
The merged quest and overworld-AI unit test suites pass locally. The branch is now conflict-free against |
Pull Request
Closes #1347 (Overworld AIs) and #1348 (quest retry delays).
Summary
Adds Overworld AIs — staff-placed AI NPCs that stand on the world map and that players interact with by walking onto their tile (a prompt opens) or clicking them. A placement is either FRIENDLY (a quest-giver / dialogue / delivery NPC) or HOSTILE (an enemy you fight), can be pinned to a fixed tile or roam daily, and FRIENDLY NPCs carry a weighted quest pool. This makes missions location-driven: a player can accept a mission from an NPC, travel to and defeat a placed enemy, and deliver an item back — including branching dialogue scenes shown right at the NPC.
It also adds quest retry delays (#1348): a quest can now repeat on a
daily/weekly/monthlycadence withmaxCompletesre-interpreted as a per-period cap (tracked viaperiodCompletes/periodStartAt), instead of only a lifetime cap. This underpins overworld mission cooldowns. A companion per-mission attempt cooldown (attemptDelay) caps how often a player can roll for a mission (see below).What was implemented
Overworld AI placements (#1347)
OverworldAiPlacement+OverworldAiPlacementQuest. A placement binds an AI template to a sector tile with:interactionType(FRIENDLY/HOSTILE),sectorType(specific/random/from_list),locationType(specific/random), anisActiveflag, apositionVersion, and (FRIENDLY) a weighted quest pool (per-quest grant chance summing ≤ 100%)./manual/ai/[aiid]/placementsto create/edit/delete placements, configure the quest pool, and copy placement ids. Reserved sectors (Wake Island, War-Torn Battleground) are excluded.travel.getSectorData→placementToSectorUser→Sector.tsx/libs/threejs/sector.ts): FRIENDLY shows a quest/talk marker, HOSTILE shows the attack marker.feat: render better): overworld NPCs now draw their actual character art standing on the tile — preferring the fullavatar(theiravatarLightis often an un-generated placeholder) via a sharedpickSpriteAvatarhelper, and rendered as a free-standing character (no circular portrait crop / map-pin) while keeping the talk/attack bubble.feat: modal when interacting): walking onto an NPC's tile auto-opens a confirm modal — FRIENDLY → "Request a mission?", HOSTILE → "Attack?" — so players don't have to click the small sprite (QoL, especially on mobile). Clicking the NPC still works; the prompt shows once per arrival and re-arms after the player leaves the tile.GET /api/daily-overworld-ai(added tovercel.json): re-randomizes non-fixed placements once per UTC day and bumpspositionVersion; clients holding a stale version are told to refresh. Guarded byCRON_SECRET+ a daily timer lock.Friendly NPC quest-giving
OVERWORLD_QUEST_ROLLS_PER_DAY(=MISSIONS_PER_DAY= 20) consumed via CAS so failed rolls still cost a slot; one active NPC mission at a time via an atomicactiveNpcQuestIdclaim (with stale-claim self-heal). NewactiveNpcQuestId+dailyOverworldQuestRollscolumns onUserData; the daily-counters cron resets the roll count.Overworld NPC objectives (location-driven missions)
defeat_opponentsbound to a placement (overworldPlacementId): walk onto the NPC's tile, click it, and a PvEOVERWORLDbattle starts against the placement's AI; winning completes the objective via the existing post-battle tracker path. The opponent is derived from the placement at quest save (single source of truth — the author never picks the AI twice). Overworld fights use the PvE loadout (OVERWORLDadded toQuestBattleTypes).deliver_item/dialogbound to a FRIENDLY placement: clicking resolves them (deliver requires the player to own the items; dialog presents branch choices).description(rich text viaparseHtml) plus the scene (sceneBackground+sceneCharacters, fetched withgameAsset.getSceneAssets) above the choice buttons — matching the Logbook scene. Branch selection advances the quest (selectedNextObjectiveId).overworldPlacementIddropdown of existing placements, scoped by task —defeat_opponentsnarrows to the selected Opponent AI's placements;deliver_item/dialogshow FRIENDLY placements only — andopponentAIsis auto-derived and hidden once a placement is bound.startOverworldBattlehelper used by both the plain HOSTILE fight and the quest-linked fight.Quest retry delays (#1348)
retryDelay(none/daily/weekly/monthly). When set,maxCompletesbecomes a per-period cap: the quest can be completed up to N times each calendar period, then unlocks at the next period boundary (nonekeeps lifetime-cap semantics;maxCompletes ≥ 1is required when a real delay is set).periodCompletes+periodStartAton the user-quest record. Period boundaries computed inutils/time.ts(periodStart); the cap is enforced byperiodCapReached/periodCompletionSetinlibs/quest.tsand folded intoisAvailableUserQuests, so period-capped quests automatically drop out of availability (including the overworld NPC pool). Mission cooldowns / NPC mission logic build on this.Per-mission attempt cooldown (
attemptDelay)retryDelay/maxCompletescap completions,attemptDelay(none/daily/weekly/monthly) caps attempts — how often a player can roll for a mission from an overworld NPC per calendar period, where a 99% "nothing" miss still spends the attempt. The two compose;attemptDelaydefaults tonone(existing quests unaffected).Quest.attemptDelaycolumn +UserQuestAttempttable (userId,questId,lastAttemptAt, unique(userId, questId)). A pureattemptCapReachedhelper (sibling ofperiodCapReached) reuses the sameperiodStartboundaries, so it self-resets with no cron.onDuplicateKeyUpdateupsert records the attempt for every eligible cooldown quest, so a grant and a miss both spend it.mission/errand), unlike the completion fields. Enables ultra-rare roaming missions, e.g.attemptDelay = daily+ poolchance = 1→ one 1% roll per day.Why
Overworld AIs turn the world map into an interaction surface: NPCs that hand out missions, enemies you hunt at a known location, and delivery/dialogue targets — enabling multi-step, place-based missions instead of menu-only quests. Quest retry delays give content designers repeatable content with sane cadences (daily/weekly/monthly) and per-period caps, which the overworld mission economy relies on; the attempt cooldown lets designers make a mission genuinely rare to even roll, not just to complete.
Screenshots
AI Overworld Button

Overworld Placement UI

Friendly Quest Placement UI

Friendly Overworld UI selections

Selecting Opponent AI who a placement was created and selecting the id from dropdown

Overworld Friendly NPC

NPC Dialog on acceptance

Breaking changes
Additive schema + migrations; no breaking API changes.
0017–0022): new tablesOverworldAiPlacement,OverworldAiPlacementQuest,UserQuestAttempt;UserDatagainsactiveNpcQuestId,dailyOverworldQuestRolls;QuestgainsretryDelayandattemptDelay; the user-quest record gainsperiodCompletes,periodStartAt; objective content gainsoverworldPlacementId(JSON, no column)./api/daily-overworld-aiplus avercel.jsoncron entry; requiresCRON_SECRETto be set in the environment.Testing
tests/libs/overworldAi.{test,derive,filter,scope,loadout,objective,roll,serializer}.test.ts,tests/validators/overworldAi.test.ts,tests/libs/quest.{attemptcap,boundfail,eligibility,periodcap,periodcount}.test.ts,tests/utils/time.periodStart.test.ts,tests/validators/questCooldown.test.ts.bunx tsc --noEmit→ clean.bunx biome lint→ clean on changed files (one pre-existingnoNonNullAssertionwarning inresolveOverworldPositionand one pre-existinguseOptionalChainwarning in unrelatedSector.tsxcode are not introduced here).tests/libs/combat/*) requires a populated.envto load and is environmental, not exercised by this change.Notes / follow-ups
mission. The completion cooldown (retryDelay/maxCompletes) is still only exposed forQuestTypesWithMaxAttempts(event/story/battlepyramid/starter/raid), so amission-type quest can't set a per-quest completion cap in the UI yet — missions remain bounded by the globalMISSIONS_PER_DAYcap. The period-cap engine itself works for any quest type.defeat_opponentsobjective completes when its AI is defeated in any battle context, not only at the bound placement — bind to an AI that isn't reused elsewhere if you need a location-specific kill.UserQuestAttemptgrowth: one row per (player, attempt-capped quest), upserted and never pruned — same shape asquestHistory; no cleanup cron needed at current scale.random/from_list) placements move daily; keep a placementspecificif a mission needs a predictable location.License
By making this pull request, I confirm that I have the right to waive copyright and related rights to my contribution, and agree that all copyright and related rights in my contributions are waived, and I acknowledge that the Studie-Tech ApS organization has the copyright to use and modify my contribution for perpetuity.
Summary by CodeRabbit
Summary by CodeRabbit
OVERWORLDbattle type support across gameplay and UI.