From df01ac8e3570fc55b218f85b0688c935f57e68dc Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 26 Apr 2026 22:17:30 -0400 Subject: [PATCH] Move revenue after churn and raise price churn cap to prevent exploit Churned subscribers no longer generate revenue the tick they leave, and the price churn multiplier cap is raised from 10 to 1000 so astronomical prices empty the subscriber pool in a single tick. Co-Authored-By: Claude Opus 4.6 --- .../src/systems/market/consumerTierSystem.ts | 27 +++++++++---------- packages/shared/src/constants/gameBalance.ts | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/game-engine/src/systems/market/consumerTierSystem.ts b/packages/game-engine/src/systems/market/consumerTierSystem.ts index 0d0719a..ac6d8e3 100644 --- a/packages/game-engine/src/systems/market/consumerTierSystem.ts +++ b/packages/game-engine/src/systems/market/consumerTierSystem.ts @@ -108,20 +108,6 @@ export function processConsumerTiers( tier.conversionRateFromBelow = convRate; } - // --- Revenue & token demand --- - let totalUsers = 0; - let subscriptionRevenue = 0; - let totalTokenDemand = 0; - - for (const id of CONSUMER_TIER_ORDER) { - const tier = updated.tiers[id]; - totalUsers += tier.userCount; - subscriptionRevenue += tier.userCount * (tier.config.price / 86400); - totalTokenDemand += tier.userCount * CONSUMER_TOKENS_PER_SUBSCRIBER; - } - - updated.totalUsers = totalUsers; - // --- Serving penalties & serving-based extra churn --- const paidDemand = consumerPaidMetrics.demandTokens; const freeDemand = consumerFreeMetrics.demandTokens; @@ -235,6 +221,19 @@ export function processConsumerTiers( tier.userCount = Math.max(0, tier.userCount - churned); } + // --- Revenue & token demand (after all churn — cancelled users don't pay) --- + let totalUsers = 0; + let subscriptionRevenue = 0; + let totalTokenDemand = 0; + + for (const id of CONSUMER_TIER_ORDER) { + const tier = updated.tiers[id]; + totalUsers += tier.userCount; + subscriptionRevenue += tier.userCount * (tier.config.price / 86400); + totalTokenDemand += tier.userCount * CONSUMER_TOKENS_PER_SUBSCRIBER; + } + + updated.totalUsers = totalUsers; updated.viralCoefficient = modelQuality > 0.5 ? 1 + (modelQuality - 0.5) * 2 : 0; return { diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts index 984c5a4..ed4e114 100644 --- a/packages/shared/src/constants/gameBalance.ts +++ b/packages/shared/src/constants/gameBalance.ts @@ -886,7 +886,7 @@ export const PERCEIVED_VALUE_REPUTATION_RANGE = { min: 0.5, max: 1.0 }; export const PRICE_ELASTICITY_STEEPNESS = 3.0; export const PRICE_SATISFACTION_WEIGHT = 0.3; export const PRICE_CHURN_EXPONENT = 1.5; -export const PRICE_CHURN_MAX_MULTIPLIER = 10.0; +export const PRICE_CHURN_MAX_MULTIPLIER = 1000.0; // --- API Tier Defaults ---