diff --git a/.claude/knowledge/codebase-graph.md b/.claude/knowledge/codebase-graph.md index 3fb36e3..73e2964 100644 --- a/.claude/knowledge/codebase-graph.md +++ b/.claude/knowledge/codebase-graph.md @@ -1,35 +1,46 @@ # Codebase Dependency Graph Generated by graphify-dotnet from the `src/` directory. Regenerates automatically before -each build of `Perpetuum.Server` (requires .NET 10 SDK for the graphify tool — `dotnet tool restore` from the +each build of `Perpetuum.Server` (requires .NET 10 SDK and `dotnet tool restore` from the solution root). ## Artifacts -- **`docs/graph/graph.json`** — machine-readable graph; gitignored, - present after any local `Perpetuum.Server` build -- **`docs/graph/GRAPH_REPORT.md`** — Markdown architecture report; gitignored, same condition +- **`docs/graph/GRAPH_REPORT.md`** — Markdown summary: god nodes, community clusters, stats. + **Read this first.** Token-efficient starting point for any architectural question. +- **`docs/graph/graph.json`** — Full machine-readable graph (19,926 nodes, 33,689 edges). + Use via `tools/query-graph.ps1` — do not read the raw file. - **GitHub Wiki** — latest report published by CI: `https://github.com/OpenPerpetuum/PerpetuumServer2/wiki/Codebase-Graph` ## Graph Structure Nodes represent C# classes, methods, and namespaces (type: `Entity` or `File`). -Edges represent inheritance, composition, and namespace imports. -Communities (Louvain clustering, clusters) group related symbols. +Edges have two relationship types: +- **`contains`** (33,623 edges) — within-file hierarchy: file → namespace → class → method +- **`imports`** (66 edges) — cross-file namespace dependencies (the meaningful ones for impact analysis) + +Communities (Louvain clustering) group related symbols into 2,616 clusters. ## How to Use -**Impact analysis** — when a type is modified, query `graph.json` for edges pointing to that -node's `id` to find all dependents before assessing blast radius. +| Goal | Action | +|---|---| +| Architectural overview or god-node check | Read `docs/graph/GRAPH_REPORT.md` | +| Blast radius before modifying a class | `.\tools\query-graph.ps1 -Direction in` | +| What a class depends on | `.\tools\query-graph.ps1 -Direction out` | +| Full dependency picture | `.\tools\query-graph.ps1 ` (default: both) | +| Find other `.cs` files in the same Louvain cluster | `.\tools\query-graph.ps1 -Direction community` | + +## God-Node Awareness -**God-node awareness** — the top 10 most-connected symbols are listed in `GRAPH_REPORT.md` -under "God Nodes". These are the highest-risk symbols to change: `RelocateItems`, -`LootItemRepository`, `PackItems`, `ChangeAmmo`, `UnstackAmount`, `EquipModule`, -`ListContainer`, `IWeatherService`, `SetItemName`, `ILootItemRepository` — see `GRAPH_REPORT.md` for the full list. +The top 10 most-connected symbols are listed in `docs/graph/GRAPH_REPORT.md` under "God Nodes". +These are the highest-risk symbols to change — check the report before modifying any of them. +The list regenerates on each build; do not rely on hardcoded names from prior sessions. -**Subsystem navigation** — look up a class node in `graph.json` to find its `community` ID, -then find other nodes with the same `community` to discover related types in the same cluster. +Note: `tools/query-graph.ps1` matches `.cs` file nodes only — namespace-level entries in the God Nodes list (e.g. `Perpetuum.RequestHandlers`) cannot be queried by name and must be explored via `GRAPH_REPORT.md` directly. -**Dependency verification** — check for edge paths between two namespaces to confirm whether -an unintended cross-subsystem dependency would be introduced by a change. +> **Note on import sparsity:** Only 66 `imports` edges exist across the entire codebase (AST-only +> analysis). Only ~7 classes have meaningful inbound edges — for all others, `-Direction in` returns +> only the parent file's `contains` edge, not actual callers. Absence of inbound edges is the norm, +> not a signal that a class is safe to change. diff --git a/CLAUDE.md b/CLAUDE.md index 406c520..0b5d82c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,10 +158,11 @@ For any non-trivial task: 1. Identify affected subsystems 2. Identify relevant documentation 3. Locate similar implementations -4. Understand existing patterns -5. Evaluate runtime implications -6. Produce a short implementation plan -7. Then implement +4. Check `docs/graph/GRAPH_REPORT.md` for God Nodes (high-risk symbols); run `.\tools\query-graph.ps1 -Direction in` to enumerate direct dependents — a null result is normal (most classes have no detected importers) and does not mean the change is safe (if `graph.json` is absent, skip and continue to step 5) +5. Understand existing patterns +6. Evaluate runtime implications +7. Produce a short implementation plan +8. Then implement --- @@ -328,6 +329,7 @@ Avoid parallel abstractions unless justified. |---|---| | Main AI instructions | `CLAUDE.md` | | Architecture deep-dive | `.claude/knowledge/architecture.md` | +| Codebase graph & impact analysis | `.claude/knowledge/codebase-graph.md` | | Specialist agents | `.claude/agents/.md` | diff --git a/docs/Patches/p36.2/Features/EquipmentSets/effects_row.sql b/docs/Patches/p36.2/Features/EquipmentSets/effects_row.sql new file mode 100644 index 0000000..d32157b --- /dev/null +++ b/docs/Patches/p36.2/Features/EquipmentSets/effects_row.sql @@ -0,0 +1,26 @@ +-- Insert effects table row for effect_equipment_set_bonus (ID 139) +-- (IMPROVEMENT-025) +-- Idempotent: safe to run multiple times. +-- The effects.id column is IDENTITY(1,1), so IDENTITY_INSERT is required +-- when inserting a specific ID value. + +SET IDENTITY_INSERT effects ON; + +IF NOT EXISTS (SELECT 1 FROM effects WHERE id = 139) +BEGIN + INSERT INTO effects + (id, name, description, effectcategory, duration, isaura, auraradius, ispositive, display, saveable) + VALUES + (139, + N'Set Bonus Active', + N'An equipment set bonus is active on this robot.', + 0, + 0, + 0, + 0, + 1, + 1, + 0); +END; + +SET IDENTITY_INSERT effects OFF; diff --git a/docs/Patches/p36.2/Features/EquipmentSets/migration.sql b/docs/Patches/p36.2/Features/EquipmentSets/migration.sql new file mode 100644 index 0000000..b5d700d --- /dev/null +++ b/docs/Patches/p36.2/Features/EquipmentSets/migration.sql @@ -0,0 +1,30 @@ +-- Equipment Set Synergy Bonuses Migration (IMPROVEMENT-025) +-- Run once against the game database before deploying the updated server binary. + +CREATE TABLE equipment_sets ( + set_id INT NOT NULL IDENTITY(1,1), + name NVARCHAR(64) NOT NULL, + CONSTRAINT PK_equipment_sets PRIMARY KEY (set_id), + CONSTRAINT UQ_equipment_sets_name UNIQUE (name) +); + +CREATE TABLE equipment_set_members ( + set_id INT NOT NULL, + definition INT NOT NULL, + CONSTRAINT PK_equipment_set_members PRIMARY KEY (set_id, definition), + CONSTRAINT FK_equipment_set_members_set FOREIGN KEY (set_id) + REFERENCES equipment_sets (set_id), + CONSTRAINT FK_equipment_set_members_def FOREIGN KEY (definition) + REFERENCES entitydefaults (definition) +); + +CREATE TABLE equipment_set_bonus_thresholds ( + set_id INT NOT NULL, + required_pieces INT NOT NULL, + aggregate_field INT NOT NULL, + bonus_value FLOAT NOT NULL, + CONSTRAINT PK_equipment_set_bonus_thresholds + PRIMARY KEY (set_id, required_pieces, aggregate_field), + CONSTRAINT FK_equipment_set_bonus_thresholds_set FOREIGN KEY (set_id) + REFERENCES equipment_sets (set_id) +); diff --git a/docs/Patches/p36.2/Features/EquipmentSets/set_striker_pilot.sql b/docs/Patches/p36.2/Features/EquipmentSets/set_striker_pilot.sql new file mode 100644 index 0000000..b5827f6 --- /dev/null +++ b/docs/Patches/p36.2/Features/EquipmentSets/set_striker_pilot.sql @@ -0,0 +1,50 @@ +-- Pilot set: set_striker (IMPROVEMENT-025 validation content) +-- Idempotent: safe to run multiple times. +-- +-- AggregateField IDs used: +-- 17 = armor_max_modifier (formula: Modifier — 1.05 = +5% max armor) +-- 310 = resist_kinetic_modifier (formula: Modifier — 1.08 = +8% kinetic resist) +-- +-- Member modules: all four tiers of medium armor plates. +-- Definition names are resolved dynamically via entitydefaults — no hardcoded IDs. + +-- 1. Create the set (idempotent) +IF NOT EXISTS (SELECT 1 FROM equipment_sets WHERE name = N'set_striker') + INSERT INTO equipment_sets (name) VALUES (N'set_striker'); + +DECLARE @set_id INT; +SELECT @set_id = set_id FROM equipment_sets WHERE name = N'set_striker'; + +-- 2. Assign member modules — all four medium armor plate tiers +-- Skips any member already present to remain idempotent. +INSERT INTO equipment_set_members (set_id, definition) +SELECT @set_id, ed.definition +FROM entitydefaults ed +WHERE ed.definitionname IN ( + N'def_standard_medium_armor_plate', + N'def_named1_medium_armor_plate', + N'def_named2_medium_armor_plate', + N'def_named3_medium_armor_plate' +) +AND NOT EXISTS ( + SELECT 1 + FROM equipment_set_members esm + WHERE esm.set_id = @set_id + AND esm.definition = ed.definition +); + +-- 3. 2-piece threshold: +5% max armor (aggregate_field = 17, bonus_value = 1.05) +IF NOT EXISTS ( + SELECT 1 FROM equipment_set_bonus_thresholds + WHERE set_id = @set_id AND required_pieces = 2 AND aggregate_field = 17 +) + INSERT INTO equipment_set_bonus_thresholds (set_id, required_pieces, aggregate_field, bonus_value) + VALUES (@set_id, 2, 17, 1.05); + +-- 4. 4-piece threshold: +8% kinetic resist (aggregate_field = 310, bonus_value = 1.08) +IF NOT EXISTS ( + SELECT 1 FROM equipment_set_bonus_thresholds + WHERE set_id = @set_id AND required_pieces = 4 AND aggregate_field = 310 +) + INSERT INTO equipment_set_bonus_thresholds (set_id, required_pieces, aggregate_field, bonus_value) + VALUES (@set_id, 4, 310, 1.08); diff --git a/docs/backlog/completed.md b/docs/backlog/completed.md index ef12ee5..ef4d31b 100644 --- a/docs/backlog/completed.md +++ b/docs/backlog/completed.md @@ -248,6 +248,29 @@ refresh already calls `LookupCache.RefreshAllAsync` but does not call `Entities. --- +## IMPROVEMENT-024 - Server Restart: Daily Objective Announcement and Admin Tool Statistics + +Status: DONE +Priority: HIGH +Area: Seasons / Objectives / Admin Tool + +### Description +Two related improvements to daily objective visibility: + +1. **Server restart announcement** — on startup (or first pool computation after season activation), if an active season with daily objectives is configured, announce today's active objectives via the Seasons Info channel. Guard: fires only when `_dailyPool.Date == DateOnly.MinValue` (uninitialized pool), not on periodic 5-minute cache refreshes. + +2. **Admin Tool Season Statistics tab** — added "Today's Daily Objectives" section showing today's active pool and per-objective completion counts. Pool computed in C# using identical seeded Fisher-Yates algorithm as the server (`seed = seasonId * 397 ^ day.DayNumber`). Server's `GetObjectives` query aligned with `ORDER BY display_order` to ensure both sides shuffle from the same input order. + +### Implementation +- `src/Perpetuum/Services/Seasons/SeasonService.cs` — `isFirstLoad` guard in `RefreshCache()` +- `src/Perpetuum/Services/Seasons/SeasonRepository.cs` — added `ORDER BY display_order` to `GetObjectives` +- `src/Perpetuum.AdminTool/Seasons/TodaysDailyObjectiveRow.cs` — new record +- `src/Perpetuum.AdminTool/Seasons/SeasonRepository.cs` — `LoadTodaysDailyObjectivesAsync` +- `src/Perpetuum.AdminTool/ViewModels/SeasonStatisticsViewModel.cs` — `TodaysDailyObjectives` collection +- `src/Perpetuum.AdminTool/Views/SeasonDetailView.xaml` — new Statistics tab section + +--- + ## ISSUE-012 - New Robot dialog: incorrect entity filtering in clone pickers Status: DONE @@ -409,3 +432,407 @@ Without `IsRobot` defaulting to true, operators must manually check it every tim - `CategoryFlagsNode.ContainsOrEquals` handles both exact match and descendant matching, so sub-types within each category are included automatically. - `StatsPanelViewModel.LoadFromClone` already supports the "Original" column display — no changes needed to that class. - The main entity clone picker (existing) is not affected; it continues to use the robots-only `BuildRobotItems` filter (see ISSUE-012). + +--- + +## ISSUE-013 - Robot creation does not populate options field with part definitions + +Status: DONE +Priority: HIGH +Area: Game Content / Robots + +### Problem +When a new robot is added, the `options` field for the robot entity is not populated with its part definitions in `GenXY` format. The options field must contain entries such as: + +``` +#head=n3036 +#chassis=n3037 +#leg=n3038 +#inventory=n332 +``` + +If new robot parts are created as part of the robot creation process, the definitions generated for those parts must be referenced in these options entries. + +### Impact +Robots without correctly populated options are non-functional in-game — the server cannot resolve their component parts, preventing spawning, equipping, or use of the robot. + +### Proposed Fix +- Identify where robot entity creation writes the `options` field (content SQL pipeline or admin tool robot creation flow). +- Ensure that after part definitions are created (head, chassis, leg, inventory), their resolved definition IDs are written back to the robot's `options` field using the `#head=nXXXX` / `#chassis=nXXXX` / `#leg=nXXXX` / `#inventory=nXXXX` format. +- If part definitions are generated dynamically, the options population step must run after the part definitions exist and reference their actual IDs. + +### Notes +Part definition IDs must be resolved dynamically — do not hardcode. +Follows the `GenXY` naming convention where `n` prefix denotes a definition reference by numeric ID. + +--- + +## ISSUE-014 - Robot part clone does not copy or expose options field for editing + +Status: DONE +Priority: HIGH +Area: Game Content / Robots / Admin Tool + +### Problem +When cloning a robot part, the `options` field is not carried over from the source part and is not presented in the editor. The clone workflow leaves the options field empty and provides no way to review or modify it before committing. + +### Impact +Cloned robot parts silently lose their options data, requiring manual correction after the fact. This is error-prone and inconsistent with the rest of the clone workflow. + +### Proposed Fix +- Copy the source part's `options` field into the clone candidate at the point of clone creation. +- Expose the options field in the clone editor using the same old/new pattern already used on the Basic tab: display the original value as read-only on the left, and provide an editable new value field on the right. +- Reuse the existing old/new field component — do not introduce a new pattern. + +### Notes +Follow the existing Basic tab old/new UI pattern exactly for consistency. +The old (source) value must be read-only; only the new value field is editable. + +--- + +## ISSUE-015 - Seasons Objectives tab: selected target not rendered in table cell + +Status: DONE +Priority: HIGH +Area: Seasons / Admin Tool + +### Problem +On the Admin Tool Seasons Objectives tab, when an objective has a target selected, the chosen value is not displayed in the table cell. The value is stored and visible when the user clicks into the cell (via the picker), but the table column renders as blank. + +### Impact +Operators cannot confirm at a glance which target is assigned to each objective. They must click every cell individually to audit or verify configurations, making bulk review error-prone and slow. + +### Proposed Fix +- Locate the cell template / data binding for the target column in the Objectives tab DataGrid. +- Identify why the display path does not render the selected value (likely a missing `DisplayMemberPath`, wrong binding path, or the display value not being propagated back to the row model after picker selection). +- Ensure the table cell shows the human-readable target label (same value visible in the picker) once a target is selected, without requiring the user to click the cell. + +### Notes +The picker itself works correctly — the issue is purely in how the selected value is reflected back to the table row display. +Check whether the binding uses a converter or a nested property that is not notifying change on selection commit. + +--- + +## ISSUE-016 - Saving Daily Objectives Per Day in AdminTool causes varchar to datetime cast error + +Status: DONE +Priority: CRITICAL +Area: Seasons / Admin Tool + +### Problem +In the AdminTool Seasons view, saving the Daily Objectives Per Day field produces a SQL cast error: implicit or explicit conversion from varchar to datetime fails. The save operation aborts and the value is not persisted. + +### Impact +Operators cannot configure Daily Objectives Per Day at all — the field is effectively broken. Any season that requires this setting cannot be properly administered. + +### Root Cause +The `start_time` and `end_time` string literals in `SeasonChanges.BuildInsert` / `BuildUpdate` and `SeasonWizardViewModel.BuildSeasonScript` used the format `'yyyy-MM-dd HH:mm:ss'` (space separator). SQL Server's implicit varchar-to-datetime conversion for this format is locale/DATEFORMAT-sensitive. The ISO 8601 format `'yyyy-MM-ddTHH:mm:ss'` (T separator) is always accepted by SQL Server regardless of collation or DATEFORMAT. The `daily_objectives_per_day` field itself (`SqlLiteral.OfNullableInt`) is correct — it generates a numeric literal or NULL. The error surfaced when users first exercised the Save General path after the new field gave them a reason to use it. + +### Fix +Changed `yyyy-MM-dd HH:mm:ss` → `yyyy-MM-ddTHH:mm:ss` in: +- `SeasonChanges.cs` `BuildInsert` and `BuildUpdate` (both start_time and end_time) +- `SeasonWizardViewModel.cs` `BuildSeasonScript` + +### Notes +Field was recently introduced (commits `837d188`, `0e59ae9`, `6d5432c`, `b442883`). +`daily_objectives_per_day` column type is `smallint [null]` — confirmed correct in schema docs. + +--- + +## ISSUE-017 - Seasons Objectives tab: Activity type selector does not show all active activity types + +Status: DONE +Priority: CRITICAL +Area: Seasons / Admin Tool + +### Problem +On the Admin Tool Seasons Objectives tab, the Activity type selector (dropdown/picker) did not display all active activity types. The Phase 1 (non-combat) and Phase 2 (combat) types added to `SeasonActivityType` were never added to the UI option lists. + +### Root Cause +`SeasonDetailViewModel.ActivityTypeOptions` and `SeasonWizardViewModel.ObjectiveActivityTypeOptions` were both hardcoded lists of 9 types. `SeasonActivityType` has 21 values — 12 were absent from both lists: `Prototyping`, `ReverseEngineering`, `Production`, `ArtifactFound`, `EpEarned`, `DamageDone`, `DamageReceived`, `ArmorRestored`, `EnergyDrainDealt`, `EnergyDrainReceived`, `EnergyTransferDealt`, `EnergyTransferReceived`. + +### Fix +Added all 12 missing types to `ActivityTypeOptions` in `SeasonDetailViewModel.cs` and `ObjectiveActivityTypeOptions` in `SeasonWizardViewModel.cs`. Labels match `SeasonActivityRateRow.ActivityTypeLabel`. + +--- + +## ISSUE-018 - SeasonRepository.GetActiveSeason throws InvalidCastException on daily_objectives_per_day + +Status: DONE +Priority: CRITICAL +Area: Seasons / Server + +### Problem +The server crashed on every `SeasonService.Update` tick with `System.InvalidCastException: Unable to cast object of type 'System.Int16' to type 'System.Nullable\`1[System.Int32]'` when an active season existed. + +### Root Cause +`daily_objectives_per_day` is `smallint [null]` in the DB — SQL Server returns a boxed `System.Int16`. `DataRecordExtensions.GetValue` does a direct unbox cast `(T)record.GetValue(index)`. The CLR cannot unbox an `Int16` as `Nullable` — the unbox target must match the stored type exactly. The crash occurred in all three season-loading methods: `GetActiveSeason`, `GetSeasonById`, and `GetPendingRecurringSeason`. + +The AdminTool's `SeasonRepository` already handled this correctly with explicit `reader.GetInt16(11)` → `(int)` widening. + +### Fix +Changed all three `record.GetValue("daily_objectives_per_day")` calls to `(int?)record.GetValue("daily_objectives_per_day")`. This reads the value with the correct CLR type (`Int16`) and widens to `int?` at the call site. `Season.DailyObjectivesPerDay` stays `int?` — no downstream changes required. + +### Notes +`recurrence_gap_days` is `int [null]` — `GetValue` is correct there and is not affected. +`GetValue` has no numeric widening; other smallint/tinyint columns read as `int?` will hit the same issue if introduced. + +--- + +## ISSUE-019 - CI build fails for AdminToolInstaller: NETSDK1047 missing RID target in assets file + +Status: DONE +Priority: HIGH +Area: Build / CI + +### Problem +The CI pipeline step `dotnet build src/Perpetuum.AdminToolInstaller/Perpetuum.AdminToolInstaller.wixproj --no-restore --configuration Release -p:Platform=x64` fails with: + +``` +NETSDK1047: Assets file '...Perpetuum.AdminTool\obj\project.assets.json' doesn't have a target for 'net8.0-windows/win-x64'. +Ensure that restore has run and that you have included 'net8.0-windows' in the TargetFrameworks for your project. +You may also need to include 'win-x64' in your project's RuntimeIdentifiers. +``` + +### Impact +The AdminTool installer cannot be built in CI, blocking release packaging of the AdminTool. + +### Root Cause +The build step uses `--no-restore`, so NuGet restore never runs for the `Perpetuum.AdminTool` dependency. The assets file in `obj/` is either absent or was produced by a prior restore without the `win-x64` RID, so the SDK cannot resolve the `net8.0-windows/win-x64` target. + +### Proposed Fix +One or more of: +1. Add a `dotnet restore` step for `Perpetuum.AdminToolInstaller.wixproj` (or the full solution) before the `--no-restore` build, with `-p:RuntimeIdentifier=win-x64`. +2. Ensure `Perpetuum.AdminTool.csproj` declares `win-x64` so restore always produces the required RID target. +3. Alternatively, drop `--no-restore` from the AdminToolInstaller build step and rely on the SDK to restore inline. + +### Notes +The error path is `D:\a\...` (GitHub Actions runner). The fix must be applied to `.github/workflows/dotnet.yml` and/or the `.csproj`. + +--- + +## IMPROVEMENT-005 - Seasons: Additional Activity Types + +Status: DONE +Priority: MEDIUM +Area: Seasons / Activities + +### Description +Expanded the Seasons activity tracking system with 12 new activity types implemented in two phases. All types integrate with the existing `RecordActivity` pipeline with no DB schema changes. + +### Phase 1 — Non-combat types (enum values 9–13) +- `Prototyping` (9) — hook in ProductionProcessor.cs at job completion, branch on job type +- `ReverseEngineering` (10) — same hook, different job type branch +- `Production` (11) — same hook, combined items + robots +- `ArtifactFound` (12) — hook in ArtifactScanner.cs after EP boost call; amount = 1 +- `EpEarned` (13) — hook all `AddExtensionPointsBoostAndLog` call sites + passive EP accumulation path + +### Phase 2 — Combat types (enum values 14–20) +- `DamageDone` (14) / `DamageReceived` (15) — hook in TakeDamage/ApplyDamageResult; amount = HP dealt +- `ArmorRestored` (16) — hook repair module application; character = repairer; amount = HP restored +- `EnergyDrainDealt` (17) / `EnergyDrainReceived` (18) — neutralizer + drainer modules; amount = energy removed +- `EnergyTransferDealt` (19) / `EnergyTransferReceived` (20) — transfer module; amount = energy transferred + +### Anti-farming +Handled via `unit_scale` in rates (set high for high-frequency types). Training character filter applies automatically. No new cap infrastructure needed. + +### Spec +`docs/superpowers/specs/2026-05-16-improvement-005-additional-activity-types-design.md` + +### Notes +Distance Travelled was deferred — see [[IMPROVEMENT-015]]. + +--- + +## IMPROVEMENT-006 - Daily Objectives + +Status: DONE +Priority: MEDIUM +Area: Seasons / Objectives + +### Description +Introduced daily objectives: a set of objectives that reset and re-issue automatically every day. The system reuses and extends existing objective infrastructure, adding only the recurrence scheduling layer on top. + +### Implementation +Extended `season_objectives` with `is_daily` (bit) and `package_id` (int, nullable). Added `day_window` (date, sentinel `1900-01-01` for regular, `UtcNow.Date` for daily) to `season_objective_progress` and rebuilt its PK to `(character_id, season_id, objective_id, day_window)`. No reset scheduler needed — fresh row per day via existing MERGE. Optional reward package delivered on daily completion via `InsertRedeemableItems`. Admin Tool gains Is Daily checkbox column, Reward Package combobox column, and All/One-time/Daily filter. Branch: `p36.1`. + +### Notes +Depends on [[ISSUE-001]] — daily reset boundary must use UTC to be consistent across deployments. +See [[IMPROVEMENT-005]] for new activity types that could back daily objective targets. +See [[IMPROVEMENT-001]] for recurring season design — daily objectives are a finer-grained recurrence within a season. +Reset time is hardcoded UTC midnight (configurable reset time deferred). + +--- + +## IMPROVEMENT-009 - Targeted Objectives + +Status: DONE +Priority: LOW +Area: Seasons / Objectives +Spec: `docs/superpowers/specs/2026-05-19-improvement-009-targeted-objectives-design.md` + +### Description +Extended the objective system to support targeted objectives, where a specific subject must be matched for progress to count. The target is activity-type-dependent — for example, a mining objective can target a specific ore type, a kill objective can target an NPC role or rank, a production objective can target an item category, and so on. + +### Impact +Targeted objectives allow season designers to create more varied and specific challenges, directing player behaviour toward particular content rather than rewarding any activity of a given type. + +### Notes +Depends on [[IMPROVEMENT-005]] for the activity types that targeted objectives filter against. +NPC rank/role filtering (see [[IMPROVEMENT-007]], [[IMPROVEMENT-008]]) was not implemented in this pass — NPC kill targets require those systems to be built first. + +--- + +## IMPROVEMENT-012 - Seasons Tiers tab: on-the-fly save generating a single change script + +Status: DONE +Priority: HIGH +Area: Seasons / Admin Tool +Spec: `docs/superpowers/specs/2026-05-16-improvement-012-tiers-tab-queue-save-design.md` + +### Description +The Tiers tab in the Seasons Admin Tool was refactored to adopt the same on-the-fly save mechanic used by Activity Rates and Objectives tabs — producing a single consolidated change script per save. All three tabs now behave consistently. + +### Implementation +Audited Activity Rates and Objectives save pattern (diff computation, script generation, transaction wrapper) and extended it to cover tier definitions (name, point threshold, reward). The generated script follows the same format and conventions as Activity Rates and Objectives saves. + +### Notes +See [[IMPROVEMENT-010]] — the Scoring Balancing tab depends on tiers being editable inline; consistent save mechanics here unblock a clean implementation of that tab. +Preserved existing tier DB schema — this improvement changed the save UI mechanic only, not the underlying data model. + +--- + +## IMPROVEMENT-017 - New Item script filename includes definition name + +Status: DONE +Priority: LOW +Area: Admin Tool / New Item Dialog +Spec: `docs/superpowers/specs/2026-05-18-improvement-017-script-filename-prefixes-design.md` + +### Description +When saving a new item in SqlScript mode, the output `.sql` file is now named `__