Gameplay Systems¶
Status: 🚧 Partial — Faction, RegionalGovernanceStack (6 tables +
RegionalPolicyVote),RegionalTreasuryEntry,AdminScopeGrant/AdminActionLog, Player ranking columns,BountyClaim,MultiAccountCluster/Flag, andProcessedWebhookEventare committed and migrated.SectorFactionInfluenceschema + write path are on the feature tip; ADR-0021 read-side (taxonomy helper,patrol_spawn_weight, UTC idle decay +last_action_at) is implemented on LEG-INI-056d42bf2c/ PR #603 (open — tip_HAS_land=NO vs origin/feat46bce720; Fibril Soft-ORDER#1634/ LEG-1558 land path — not tip-shipped) — see § SectorFactionInfluence. Still open after that merge:Faction.base_patrol_intensity/zone_modcolumns (identity defaults1.0in the WO tip). RBAC Phase-A1 + regional governance wiring still as noted 2026-08-06.
NPC factions and the regional governance / diplomacy stack. The turn system is implemented as plain integer counters on Player (turns, turn_reset_at) and Galaxy.default_turns_per_day rather than a dedicated entity, so it doesn't get its own model section.
Schema status¶
Per ADR-0066 D-V1, schema-level implementation status is consolidated here. Field descriptions describe the target schema.
Design-only entities: none remaining — the last holdouts (AdminScopeGrant/AdminActionLog, RegionalTreasuryEntry, the regional governance stack) all shipped; see the corrected status banner above. (re-verified 2026-08-06 against services/gameserver/src/models/region.py:287-565 + admin_action_log.py + admin_scope_grant.py + their alembic migrations and service/route wiring)
The Faction, BountyClaim, MultiAccountCluster/MultiAccountFlag, ProcessedWebhookEvent, AdminScopeGrant/AdminActionLog, RegionalTreasuryEntry, and the regional governance stack (RegionalMembership, InterRegionalTravel, RegionalTreaty, RegionalElection, RegionalVote, RegionalPolicy, RegionalPolicyVote) tables are all committed. SectorFactionInfluence write path is on the feature tip; ADR-0021 read-side (taxonomy / patrol weight / idle decay) is implemented on LEG-INI-05 6d42bf2c / PR #603 (open, not yet on feat/new-feature-development) — see § SectorFactionInfluence. Intensity/zone_mod columns remain identity-default residuals.
Faction¶
Source: services/gameserver/src/models/faction.py
Purpose: NPC political/economic entity. Controls territory, biases prices, and gates faction-locked sectors via player reputation.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| name | String(100) | unique, not null, indexed | |
| faction_type | Enum factiontype (custom FactionTypeDB) |
not null | Archetype slot for the lore faction. Ten values: FEDERATION (Terran Federation), MERCHANTS (Mercantile Guild), INDEPENDENTS (Frontier Coalition), MINING (Astral Mining Consortium — promoted per ADR-0033), EXPLORERS (Nova Scientific Institute), OUTLAWS (Fringe Alliance), SYNDICATE (Shadow Syndicate — reserved), PIRATES (hostile-only), CABAL (reserved — endgame antagonist singleton), CONCORD (Galactic Concord — operator-managed, no player rep gain mechanics; canon-to-seed per ADR-0093 item 38, extending the MINING precedent). Lore name lives on Faction.name; see FEATURES/gameplay/faction-lore.md for full lore-to-archetype mapping and FEATURES/gameplay/police-forces.md for CONCORD's Nexus Sentinel Corps context. |
| description | Text | nullable | |
| territory_sectors | ARRAY(UUID) | default [] | sector UUIDs under faction control |
| home_sector_id | UUID | nullable | primary HQ sector (no FK constraint) |
| base_pricing_modifier | Float | default 1.0 | 0.8 = 20% discount, 1.2 = 20% markup |
| trade_specialties | ARRAY(String) | default [] | commodities focus |
| aggression_level | Integer | default 5 | 1-10, drives NPC behavior |
| diplomacy_stance | String(50) | default neutral |
hostile/neutral/friendly |
| color_primary, color_secondary | String(7) | nullable | hex |
| logo_url | String(255) | nullable |
Relationships:
- reputation_records → Reputation (1:many cascade).
Faction.get_pricing_modifier(player_reputation) and can_access_territory(player_reputation) encode price tiers and access gating.
Audit-row preservation across region deletion¶
Per ADR-0050 SK24, audit-trail tables in this domain (bounty_claim, aria_observation_log, pirate_kill_log, future cargo_wreck_log) gain a region_id_snapshot UUID column populated at row creation. The existing sector FK becomes ON DELETE SET NULL. On region regeneration (force=true) or termination cleanup, the sector rows cascade-delete but the audit rows persist with sector_id = NULL and region_id_snapshot retaining the original region pointer. Audit queries handle the NULL gracefully ("sector unknown — region was deleted"). (folded from ADR-0050 SK24, re-verified 2026-08-07)
BountyClaim — auto-bounty targets player, not ship¶
Per ADR-0054 X-V2, BountyClaim carries target_player_id (FK Player.id) as the canonical bounty target — not target_ship_id. The legacy target_ship_id column is retained as nullable for non-stolen-report bounty types (future) but is no longer used by the stolen-report flow.
| Column | Type | Notes |
| `id` | UUID PK | |
| `target_player_id` | UUID FK Player.id, not null | Per [ADR-0054](../ADR/0054-group-b-region-lifecycle-composition.md) — bounty stays on the thief regardless of ship state. |
| `target_ship_id` | UUID FK Ship.id, nullable | Legacy / reserved for future bounty types that target specific hulls. Null for stolen-report-driven auto-bounties. |
| `placer_player_id` | UUID FK Player.id | Who funded the bounty. |
| `amount` | Integer | Bounty pool amount (held in escrow on `placer_player_id`'s wallet at file-time). |
| `placed_at` | DateTime | |
| `collected_at` | DateTime nullable | Set when the bounty pays out (target killed). |
| `collector_player_id` | UUID FK Player.id, nullable | The hunter who killed the target and collected. |
| `region_id_snapshot` | UUID | Per SK24 audit-row preservation. |
| `sector_id` | UUID FK Sector.id, nullable, ON DELETE SET NULL | Per SK24. |
Migration: existing target_ship_id rows in production backfill to target_player_id from the ship's pilot_id at migration time. Rows where the pilot is unknown are dropped (rare; data-quality cleanup).
Indices (per ADR-0055 S-V3):
UNIQUE (placer_player_id, target_player_id) WHERE collected_at IS NULL— one active bounty per (placer, target) pair. Stops a single placer from stacking multiple bounties on the same target; multiple distinct placers still stack.(target_player_id, collected_at)— used by the destruction handler's bounty-collection step (SELECT ... FOR UPDATE WHERE target_player_id = :pid AND collected_at IS NULL).(collector_player_id, collected_at)— used by collector-side audit lookups.
Same-team collusion check (per ADR-0055 S-F1): bounty collection is rejected with ERR_COLLECTOR_SAME_TEAM_AS_PLACER if collector_player_id shares a team with placer_player_id at collection-time. The check is enforced at the application layer via a live team_members join — DB-level constraints can't reference dynamic team membership. Rejected rows leave the escrow held; placer can retract or wait for a non-team-mate kill.
AdminScopeGrant¶
Per ADR-0058 A-F2 (supersedes ADR-0027). Replaces the flat User.is_admin boolean as the authorization gate. One row per (admin user, scope) grant.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| user_id | UUID FK users.id | not null, CASCADE | The admin user holding the scope. |
| scope | String(64) | not null | One of the canonical 19 scopes (e.g., admin.players.suspend, admin.subscriptions.modify, admin.webhooks.replay). Full list in ADR-0058. |
| granted_by | UUID FK users.id | not null | The admin who issued the grant. The bootstrap superadmin self-grants via a one-time migration. |
| granted_at | DateTime | not null | |
| revoked_at | DateTime | nullable | Set when the grant is revoked; the row persists for audit. Active grants have revoked_at IS NULL. |
| revoked_by | UUID FK users.id | nullable | Who revoked it. |
Indexes:
- (user_id) WHERE revoked_at IS NULL — runtime authorization check ("does this admin have this scope?").
- UNIQUE (user_id, scope) WHERE revoked_at IS NULL — at most one active grant per (admin, scope) pair.
The legacy User.is_admin boolean becomes a derived view: is_admin = EXISTS (SELECT 1 FROM admin_scope_grants WHERE user_id = User.id AND revoked_at IS NULL). Code that previously checked User.is_admin continues to work; new code reads scopes directly.
AdminActionLog¶
Per ADR-0058 A-F2. Append-only audit trail for every admin action. Drives the daily review queue (high-impact actions surfaced for retrospective acknowledgement).
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| admin_user_id | UUID FK users.id | not null | Who acted. |
| scope_used | String(64) | not null | The scope that authorized the action. |
| action | String(128) | not null | Action name (e.g., subscription.create, region.terminate, webhook.replay). |
| target_type | String(64) | nullable | Entity type acted upon (Player, Region, Subscription, etc.). |
| target_id | UUID | nullable | Entity ID. |
| payload_snapshot | JSONB | not null | Full request payload at action time, sanitized of secrets. |
| result | Enum (ok, failed, partial) |
not null | |
| failure_reason | String | nullable | If result != ok. |
| reviewed_by | UUID FK users.id | nullable | Set when another admin acknowledges this row in the review queue. |
| reviewed_at | DateTime | nullable | |
| at | DateTime | not null | When the action fired. |
Indexes:
- (at DESC) — chronological audit feed.
- (admin_user_id, at DESC) — per-admin action history.
- (action, at DESC) — review-queue filter by action class.
- (reviewed_by) WHERE reviewed_at IS NULL — pending review queue.
Retention: 5 years (compliance default). Append-only; no in-place edits.
Review-queue scope: actions in admin.subscriptions.*, admin.webhooks.replay, admin.regions.terminate, and admin.scopes.* surface for retrospective review. Any holder of admin.audit.view can acknowledge.
ProcessedWebhookEvent¶
Per ADR-0058 A-D3. Idempotency table for PayPal (and future webhook providers). Inserted in the same transaction as the webhook-driven mutation, so a successful insert means the mutation also committed.
| name | type | constraints | notes |
|---|---|---|---|
| event_id | String(64) | PK | The provider's event ID (PayPal event_id). |
| provider | String(32) | not null | paypal at Launch; future providers use the same shape. |
| event_type | String(64) | not null | The event class (BILLING.SUBSCRIPTION.ACTIVATED, etc.). |
| received_at | DateTime | not null | When the gameserver received the webhook. |
| processed_at | DateTime | not null | When the corresponding mutation committed (same transaction). |
| signature_valid | Boolean | not null | Always true for committed rows (invalid signatures reject before insert). Kept for forensic queries. |
PayPal's at-least-once delivery semantics produce duplicate events; the UNIQUE PK constraint ensures the second delivery returns HTTP 200 without re-applying the mutation.
MultiAccountCluster¶
Per ADR-0056. One row per detected cluster of accounts likely operated by the same human. Built and maintained by MultiAccountDetectionService per ../OPERATIONS/multi-account-detection.md.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| signal_summary | JSONB | not null | {hard: ["payment_method", ...], soft: ["ip_24h", "device_fingerprint", "trade_correlation"], evidence: {...}} — the heuristics that fired and the supporting evidence. |
| severity | Enum (hard, soft) |
not null | The severity of the most-severe signal in this cluster. Drives the discount math. |
| all_paid_subscribers | Boolean | not null | Cached: true if every member account has an active Galactic Citizen or Region Owner subscription at the most recent sweep. ✅ Live: participation_weight returns 1.0 for members of clusters where this is true (see Discount math). Refreshed every sweep. |
| admin_decision | Enum (pending, confirmed, overridden, escalated) |
default pending |
Admin review outcome. overridden clears all member flags permanently for this cluster ID; re-detection requires a new signal. |
| admin_decision_reason | String | nullable | Free-text rationale for the admin decision. |
| admin_decision_at | DateTime | nullable | When the admin acted. |
| admin_decision_by | UUID FK users.id | nullable | Admin who decided. |
| created_at | DateTime | not null | First detection. |
| updated_at | DateTime | not null | Most recent re-evaluation. |
MultiAccountFlag¶
Per ADR-0056. One row per (player, cluster) membership. Read by every gated participation surface (governance vote, station volume, beacon visibility, faction-rep gain) to compute the participation_weight.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | not null, CASCADE | The flagged account. |
| cluster_id | UUID FK multi_account_clusters.id | not null, CASCADE | The cluster this player belongs to. |
| signal | String | not null | The specific signal that ties this player to the cluster (e.g., payment_method, ip_24h, device_fingerprint). |
| severity | Enum (hard, soft) |
not null | Snapshot of the signal severity at flag-time. |
| created_at | DateTime | not null |
Indexes:
- (player_id) — single-row lookup by gated surfaces.
- (cluster_id) — admin review queue and cluster-membership rollups.
- UNIQUE (player_id, cluster_id, signal) — one row per (player, cluster, signal) triple; re-detections update created_at rather than insert.
Discount math (per ADR-0056 E-V5; shipped-vs-unbuilt SSOT: ../OPERATIONS/multi-account-detection.md):
- ✅ Live on tip (
46bce720+; LEG-256 / PR #668) —multi_account_service.participation_weight(db, player_id): no flag →1.0;cluster.all_paid_subscribers→1.0; HARD →0.0; SOFT →0.5. Nosurfaceargument. - 📐 Design-target / unbuilt — a
surfaceparameter (the ADR E-V5 shape / formerMultiAccountDetectionService.participation_weight(player_id, surface)sketch). Soft-tier0.5×andall_paid_subscribersexemption are no longer gaps.
SectorFactionInfluence¶
Source: services/gameserver/src/models/sector_faction_influence.py (write-path columns on feature tip). Read-side on LEG-INI-05 tip 6d42bf2c / PR #603 (open as of 2026-08-21 — tip_HAS_land=NO vs 46bce720; Soft-ORDER #1634 tip-pending): faction_service.py (sector_territory_tier, compute_patrol_spawn_weight / apply_patrol_spawn_weight, apply_sector_influence_daily_decay); scheduler faction_influence_sweeps.py + core_loop.py / Loop B in npc_tick_loops.py. Decay formula from LEG-65.
Purpose: Per-(sector, faction) row holding the faction's stored influence percentage, derived patrol_spawn_weight, and (on the LEG-INI-05 tip) last_action_at activity clock. Write path: faction_service.adjust_sector_influence UPSERTs and clamps influence_percentage to [0, 100]; on the LEG-INI-05 tip it also stamps last_action_at and refreshes patrol_spawn_weight. ADR-0021 read-side is ✅ Implemented on LEG-INI-05 tip / pending merge — no longer Design-only, but not yet on feat/new-feature-development. 📐 Design-only residual: real Faction.base_patrol_intensity / zone_mod columns — WO tip uses identity defaults 1.0 × 1.0.
Schema (feature tip + LEG-INI-05 tip):
| name | type | constraints | notes |
|---|---|---|---|
id |
UUID | PK | |
sector_id |
UUID FK sectors.id |
not null, indexed, CASCADE | Sector PK (not compound region_id + sector_number) |
faction_id |
UUID FK factions.id |
not null, indexed, CASCADE | Faction PK (not a faction_code string) |
influence_percentage |
Float | not null, default 0.0 | 0–100 percentage points (not a 0–1 fraction). Taxonomy thresholds key off this unit. |
patrol_spawn_weight |
Float | not null, default 0.0 | On LEG-INI-05 tip: written by derivation (clamp((influence/100)×intensity×zone_mod, 0, 2)); Loop B reads max weight per sector. Feature tip still leaves default until merge. |
last_action_at |
DateTime (tz) | nullable | LEG-INI-05 tip / migration a8c3e1f4b902. Set only by write-path adjust_sector_influence; decay never writes it. Not on feature tip until PR #603 merges. |
created_at, updated_at |
DateTime | server defaults | Fallback activity clock for pre-last_action_at rows |
Not on the model (still design / not columns): taxonomy_tier (computed at read time on LEG-INI-05 tip, not stored), region_id/sector_number/faction_code compound keys, influence_pct as a 0–1 fraction, Faction.base_patrol_intensity / zone_mod.
Indexes / constraints (shipped):
- UNIQUE (sector_id, faction_id) — exactly one row per (sector, faction) tuple.
Influence accumulation (write path — ✅ Shipped; LEG-INI-05 tip also stamps activity + weight)¶
adjust_sector_influence applies a caller-supplied delta to influence_percentage and clamps to [0, 100]. On the LEG-INI-05 tip it also stamps last_action_at and refreshes patrol_spawn_weight. Which gameplay actions call it (and with what deltas) remains tied to ADR-0032 / factions-and-teams.md#dynamic-influence; the stored unit is percentage points, not the fractional influence_pct previously documented here.
Territory taxonomy derivation¶
✅ Implemented on LEG-INI-05 tip (
sector_territory_tier) / pending PR #603 merge. Notaxonomy_tiercolumn. Thresholds (0–100 percentage points) match LEG-34:
if influence_percentage >= 95: tier = core
elif influence_percentage >= 75: tier = controlled
elif influence_percentage >= 40:
if any rival faction has influence_percentage >= 25:
tier = contested
else: tier = controlled
else: tier = uncontrolled
Patrol spawn weight derivation¶
✅ Implemented on LEG-INI-05 tip (stored column + Loop B read) / pending PR #603 merge. Formula:
patrol_spawn_weight = clamp(
(influence_percentage / 100.0) × faction.base_patrol_intensity × zone_mod,
0.0, 2.0
)
📐 Design-only residual: Faction.base_patrol_intensity and zone_mod columns do not exist yet — LEG-INI-05 uses identity defaults 1.0 × 1.0 (flagged in code). Do not invent rates beyond that residual.
Daily decay sweep¶
✅ Implemented on LEG-INI-05 tip (
scheduler/faction_influence_sweeps.py→_run_sector_faction_influence_decay_sync, wired from the NPC scheduler core loop) / pending PR #603 merge. Numbers remain the LEG-65 provisional targets (Max may override async under the ratification-authority amendment).
Cadence: once per UTC calendar day (advisory lock + Galaxy.state day anchor).
Idle definition: a row is idle when its activity clock is older than 3 UTC days. Activity clock prefers last_action_at (set only by write-path adjust_sector_influence); falls back to updated_at for pre-column rows. Decay never writes last_action_at.
Decay rate (provisional): for each idle row,
influence_percentage = max(0.0, influence_percentage − 0.5)
−0.5 influence_percentage points per idle UTC day (0–100 scale). After each change, refresh patrol_spawn_weight. Rows already at 0.0 are no-ops.
Worked example: a Contested row at 50.0 that stays idle decays to 49.5 after one midnight, 47.0 after six idle days, and reaches Uncontrolled (<40) after 21 idle days from 50.0 — slow enough that active sectors stay sticky, fast enough that abandoned holdings erode within a real-time month.
Player Ranking (in-place on Player)¶
Ranking is not a separate table. Player.military_rank (String, default Recruit) and Player.rank_points (Integer) hold progression. Personal alignment is Player.personal_reputation (-1000..+1000) with cached reputation_tier and name_color strings. See ./player.md for the full Player schema. Rank/reputation columns were added in migration fe22441146b1.
Regional Governance Stack¶
These models live in services/gameserver/src/models/region.py alongside Region itself (covered in ./galaxy.md). They implement the in-game political loop: membership → vote weight → elections, treaties, policies.
RegionalMembership¶
Purpose: A player's standing inside a specific region.
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | not null | |
| region_id | UUID FK regions.id | not null | |
| membership_type | String(50) | default visitor |
visitor/resident/citizen |
| reputation_score | Integer | default 0, -1000..1000 | check constraint |
| local_rank | String(50) | nullable | closed vocabulary — administrator / moderator / None — PATCH-validated server-side; not a free-form field (ADR-0093 item 20) |
| voting_power | DECIMAL(5,4) | default 1.0, 0.0–5.0 | check constraint |
| joined_at, last_visit | TIMESTAMP | server defaults | |
| total_visits | Integer | default 0 |
Unique: (player_id, region_id). Relationships: player, region.
InterRegionalTravel¶
Purpose: Tracks a single cross-region travel job and any asset transfer.
Columns: id, player_id FK, source_region_id FK, destination_region_id FK (must differ — check), travel_method (platform_gate/player_gate/warp_jumper), travel_cost (≥0), assets_transferred JSONB, initiated_at, completed_at, status (in_transit/completed/failed/cancelled).
RegionalTreaty¶
Purpose: Bilateral region-to-region agreements.
Columns: region_a_id FK, region_b_id FK (must differ), treaty_type (trade_agreement/defense_pact/non_aggression/cultural_exchange), terms JSONB, signed_at, expires_at, status (default active). Unique on (region_a_id, region_b_id, treaty_type).
RegionalElection¶
Purpose: Election for a regional position (governor, council_member, ambassador).
Columns: region_id FK, position, candidates JSONB (array of {player_id, platform}), voting_opens_at, voting_closes_at (must be after open), results JSONB, status (pending/active/completed/cancelled).
Relationship: votes → RegionalVote (1:many cascade).
RegionalVote¶
Columns: election_id FK, voter_id FK players.id, candidate_id FK players.id, weight DECIMAL(5,4) default 1.0 (0.0-5.0), cast_at. Unique on (election_id, voter_id).
RegionalPolicy¶
Purpose: Policy proposal / referendum within a region.
Columns: region_id FK, policy_type (tax_rate/pvp_rules/trade_policy/…), title, description, proposed_changes JSONB, proposed_by FK players.id, proposed_at, voting_closes_at (after proposed_at), votes_for (≥0), votes_against (≥0), status (voting/passed/rejected/implemented).
approval_percentage, is_passing are computed from votes against Region.voting_threshold.
RegionalTreasuryEntry¶
Per ADR-0059 N-I4. Append-only ledger of every balance-affecting event on Region.treasury_balance. Drives the daily reconciliation sweep that verifies the running balance matches the sum of entries.
| Column | Type | Constraint | Notes |
|---|---|---|---|
id |
UUID | PK | |
region_id |
UUID FK regions.id | not null, CASCADE | |
before_balance |
Integer | not null | Treasury balance immediately before the event. |
after_balance |
Integer | not null | Treasury balance immediately after the event. |
delta |
Integer | not null | after_balance - before_balance; positive for inflow, negative for outflow. |
cause_type |
Enum (policy_enactment, tax_collection, expenditure, transfer_in, transfer_out, manual_admin_adjustment) |
not null | |
cause_id |
UUID | nullable | RegionalPolicy.id, Tax.id, or other entity per cause_type. Null for manual_admin_adjustment. |
reason |
String | nullable | Free-text snapshot of the event. For manual_admin_adjustment, captures the admin user's identity per ADR-0058. |
at |
DateTime | not null |
Indexes:
- (region_id, at DESC) — chronological treasury feed for a region.
- (region_id) → SUM(delta) — reconciliation sweep aggregate.
Reconciliation sweep: a daily job (per ADR-0053) verifies SUM(treasury_entries.delta WHERE region_id = R) == Region.treasury_balance for every active region. Mismatches fire a non-blocking ops alert.
Retention: indefinite. No GDPR exposure (region-level balance changes only; no player-identifiable data).
Region governance config columns¶
Per ADR-0059 N-D5, Region gains a configurable quorum field:
| Column | Type | Constraint | Default | Notes |
|---|---|---|---|---|
governance_quorum_pct |
Decimal(3,2) | 0.25 ≤ x ≤ 0.60 |
0.33 |
Fraction of eligible voters who must cast a vote for the result to count. Per-region admin-tunable; the 25–60% band keeps governance reachable while preventing impossible-quorum griefing. The 2-voter hard floor (when 2+ eligible) is non-configurable and lives in the quorum-evaluation logic, not as a column. |
Game events¶
Source: services/gameserver/src/models/game_event.py (not detailed here — the file contains scheduled/active world events and is referenced by Galaxy.events JSONB and Sector.active_events). New code that touches event scheduling should refer to that model directly.
Notes¶
- Faction membership and player faction standing live in
Reputation(per-faction row, see./player.md). - Region-level reputation lives in
RegionalMembership.reputation_score. These two are independent: a player can be hated by Pirates faction (Reputation row) yet be a high-rep citizen of a player-owned region (RegionalMembership row). Player.military_rankandPlayer.rank_pointsare the achievement-progression dimension that is independent of either reputation system.