@zigrivers/scaffold 3.4.1 → 3.5.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.
- package/README.md +91 -0
- package/content/knowledge/game/game-accessibility.md +328 -0
- package/content/knowledge/game/game-ai-patterns.md +542 -0
- package/content/knowledge/game/game-asset-pipeline.md +359 -0
- package/content/knowledge/game/game-audio-design.md +342 -0
- package/content/knowledge/game/game-binary-vcs-strategy.md +396 -0
- package/content/knowledge/game/game-design-document.md +260 -0
- package/content/knowledge/game/game-domain-patterns.md +297 -0
- package/content/knowledge/game/game-economy-design.md +355 -0
- package/content/knowledge/game/game-engine-selection.md +242 -0
- package/content/knowledge/game/game-input-systems.md +357 -0
- package/content/knowledge/game/game-level-content-design.md +455 -0
- package/content/knowledge/game/game-liveops-analytics.md +280 -0
- package/content/knowledge/game/game-localization.md +323 -0
- package/content/knowledge/game/game-milestone-definitions.md +337 -0
- package/content/knowledge/game/game-modding-ugc.md +390 -0
- package/content/knowledge/game/game-narrative-design.md +404 -0
- package/content/knowledge/game/game-networking.md +391 -0
- package/content/knowledge/game/game-performance-budgeting.md +378 -0
- package/content/knowledge/game/game-platform-certification.md +417 -0
- package/content/knowledge/game/game-project-structure.md +360 -0
- package/content/knowledge/game/game-save-systems.md +452 -0
- package/content/knowledge/game/game-testing-strategy.md +470 -0
- package/content/knowledge/game/game-ui-patterns.md +475 -0
- package/content/knowledge/game/game-vr-ar-design.md +313 -0
- package/content/knowledge/review/review-art-bible.md +305 -0
- package/content/knowledge/review/review-game-design.md +303 -0
- package/content/knowledge/review/review-game-economy.md +272 -0
- package/content/knowledge/review/review-netcode.md +280 -0
- package/content/knowledge/review/review-platform-cert.md +341 -0
- package/content/methodology/custom-defaults.yml +25 -0
- package/content/methodology/deep.yml +25 -0
- package/content/methodology/game-overlay.yml +145 -0
- package/content/methodology/mvp.yml +25 -0
- package/content/pipeline/architecture/ai-behavior-design.md +87 -0
- package/content/pipeline/architecture/netcode-spec.md +86 -0
- package/content/pipeline/architecture/review-netcode.md +78 -0
- package/content/pipeline/foundation/performance-budgets.md +91 -0
- package/content/pipeline/modeling/narrative-bible.md +84 -0
- package/content/pipeline/pre/game-design-document.md +89 -0
- package/content/pipeline/pre/review-gdd.md +74 -0
- package/content/pipeline/quality/analytics-telemetry.md +98 -0
- package/content/pipeline/quality/live-ops-plan.md +99 -0
- package/content/pipeline/quality/platform-cert-prep.md +129 -0
- package/content/pipeline/quality/playtest-plan.md +83 -0
- package/content/pipeline/specification/art-bible.md +87 -0
- package/content/pipeline/specification/audio-design.md +96 -0
- package/content/pipeline/specification/content-structure-design.md +141 -0
- package/content/pipeline/specification/economy-design.md +104 -0
- package/content/pipeline/specification/game-accessibility.md +82 -0
- package/content/pipeline/specification/game-ui-spec.md +97 -0
- package/content/pipeline/specification/input-controls-spec.md +81 -0
- package/content/pipeline/specification/localization-plan.md +113 -0
- package/content/pipeline/specification/modding-ugc-spec.md +116 -0
- package/content/pipeline/specification/online-services-spec.md +104 -0
- package/content/pipeline/specification/review-economy.md +87 -0
- package/content/pipeline/specification/review-game-ui.md +73 -0
- package/content/pipeline/specification/save-system-spec.md +116 -0
- package/dist/cli/commands/adopt.d.ts.map +1 -1
- package/dist/cli/commands/adopt.js +25 -0
- package/dist/cli/commands/adopt.js.map +1 -1
- package/dist/cli/commands/adopt.test.js +28 -1
- package/dist/cli/commands/adopt.test.js.map +1 -1
- package/dist/cli/commands/build.test.js +3 -0
- package/dist/cli/commands/build.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +12 -1
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/knowledge.test.js +8 -0
- package/dist/cli/commands/knowledge.test.js.map +1 -1
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +19 -5
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/next.test.js +56 -0
- package/dist/cli/commands/next.test.js.map +1 -1
- package/dist/cli/commands/rework.d.ts.map +1 -1
- package/dist/cli/commands/rework.js +11 -2
- package/dist/cli/commands/rework.js.map +1 -1
- package/dist/cli/commands/rework.test.js +5 -0
- package/dist/cli/commands/rework.test.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +54 -4
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +384 -0
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.test.js +3 -0
- package/dist/cli/commands/skip.test.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +16 -3
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/status.test.js +55 -0
- package/dist/cli/commands/status.test.js.map +1 -1
- package/dist/cli/output/auto.d.ts +3 -0
- package/dist/cli/output/auto.d.ts.map +1 -1
- package/dist/cli/output/auto.js +9 -0
- package/dist/cli/output/auto.js.map +1 -1
- package/dist/cli/output/context.d.ts +6 -0
- package/dist/cli/output/context.d.ts.map +1 -1
- package/dist/cli/output/context.js.map +1 -1
- package/dist/cli/output/context.test.js +87 -0
- package/dist/cli/output/context.test.js.map +1 -1
- package/dist/cli/output/error-display.test.js +3 -0
- package/dist/cli/output/error-display.test.js.map +1 -1
- package/dist/cli/output/interactive.d.ts +3 -0
- package/dist/cli/output/interactive.d.ts.map +1 -1
- package/dist/cli/output/interactive.js +76 -0
- package/dist/cli/output/interactive.js.map +1 -1
- package/dist/cli/output/json.d.ts +3 -0
- package/dist/cli/output/json.d.ts.map +1 -1
- package/dist/cli/output/json.js +9 -0
- package/dist/cli/output/json.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +641 -15
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +26 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +192 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts +24 -0
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -0
- package/dist/core/assembly/overlay-loader.js +190 -0
- package/dist/core/assembly/overlay-loader.js.map +1 -0
- package/dist/core/assembly/overlay-loader.test.d.ts +2 -0
- package/dist/core/assembly/overlay-loader.test.d.ts.map +1 -0
- package/dist/core/assembly/overlay-loader.test.js +106 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -0
- package/dist/core/assembly/overlay-resolver.d.ts +15 -0
- package/dist/core/assembly/overlay-resolver.d.ts.map +1 -0
- package/dist/core/assembly/overlay-resolver.js +58 -0
- package/dist/core/assembly/overlay-resolver.js.map +1 -0
- package/dist/core/assembly/overlay-resolver.test.d.ts +2 -0
- package/dist/core/assembly/overlay-resolver.test.d.ts.map +1 -0
- package/dist/core/assembly/overlay-resolver.test.js +246 -0
- package/dist/core/assembly/overlay-resolver.test.js.map +1 -0
- package/dist/core/assembly/overlay-state-resolver.d.ts +26 -0
- package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -0
- package/dist/core/assembly/overlay-state-resolver.js +63 -0
- package/dist/core/assembly/overlay-state-resolver.js.map +1 -0
- package/dist/core/assembly/overlay-state-resolver.test.d.ts +2 -0
- package/dist/core/assembly/overlay-state-resolver.test.d.ts.map +1 -0
- package/dist/core/assembly/overlay-state-resolver.test.js +256 -0
- package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -0
- package/dist/core/assembly/preset-loader.d.ts +1 -0
- package/dist/core/assembly/preset-loader.d.ts.map +1 -1
- package/dist/core/assembly/preset-loader.js +2 -0
- package/dist/core/assembly/preset-loader.js.map +1 -1
- package/dist/core/dependency/eligibility.test.js +3 -0
- package/dist/core/dependency/eligibility.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.d.ts +10 -0
- package/dist/e2e/game-pipeline.test.d.ts.map +1 -0
- package/dist/e2e/game-pipeline.test.js +298 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -0
- package/dist/e2e/init.test.js +3 -0
- package/dist/e2e/init.test.js.map +1 -1
- package/dist/project/adopt.d.ts +3 -1
- package/dist/project/adopt.d.ts.map +1 -1
- package/dist/project/adopt.js +29 -1
- package/dist/project/adopt.js.map +1 -1
- package/dist/project/adopt.test.js +51 -1
- package/dist/project/adopt.test.js.map +1 -1
- package/dist/types/config.d.ts +50 -4
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.test.d.ts +2 -0
- package/dist/types/config.test.d.ts.map +1 -0
- package/dist/types/config.test.js +97 -0
- package/dist/types/config.test.js.map +1 -0
- package/dist/utils/eligible.d.ts +3 -2
- package/dist/utils/eligible.d.ts.map +1 -1
- package/dist/utils/eligible.js +18 -4
- package/dist/utils/eligible.js.map +1 -1
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +31 -0
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/errors.test.js +4 -1
- package/dist/utils/errors.test.js.map +1 -1
- package/dist/wizard/questions.d.ts +4 -0
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +59 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +178 -4
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +1 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +4 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/dist/wizard/wizard.test.js +102 -4
- package/dist/wizard/wizard.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: game-domain-patterns
|
|
3
|
+
description: ECS vs DDD as mutually exclusive per-layer patterns, game state machines, and domain modeling for games
|
|
4
|
+
topics: [game-dev, ecs, ddd, state-machines, domain-modeling]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Game software architecture differs fundamentally from business software architecture because games have two distinct layers with incompatible optimization goals: the simulation layer (real-time, data-oriented, performance-critical) and the meta-game layer (behavior-rich, event-driven, correctness-critical). Choosing the right domain modeling approach for each layer is essential. ECS and DDD are mutually exclusive paradigms — applying the wrong one to a layer creates friction that compounds throughout development.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
### Two Layers, Two Paradigms
|
|
12
|
+
|
|
13
|
+
Games are split into two architectural layers that demand different modeling approaches:
|
|
14
|
+
|
|
15
|
+
**Simulation Layer** (the "game" part):
|
|
16
|
+
- Processes thousands of entities per frame at 16ms or 33ms budgets
|
|
17
|
+
- Data access patterns dominate: iterate over all entities with Health, iterate over all entities with Position+Velocity
|
|
18
|
+
- Behavior is uniform: every entity with the same components processes identically
|
|
19
|
+
- Optimization is existential: a 1ms regression in a core system is a shipped bug
|
|
20
|
+
- **Use ECS (Entity-Component-System)** — data-oriented, cache-friendly, composable
|
|
21
|
+
|
|
22
|
+
**Meta-Game Layer** (the "around the game" part):
|
|
23
|
+
- Manages player profiles, inventories, progression, matchmaking, economy, social features
|
|
24
|
+
- Rich business rules: "a player can equip an item only if their level meets the requirement and the item is not already equipped by another character in the same party"
|
|
25
|
+
- Correctness matters more than raw throughput
|
|
26
|
+
- Domain language is complex and stakeholder-facing
|
|
27
|
+
- **Use DDD (Domain-Driven Design)** — behavior-oriented, encapsulated, rule-rich
|
|
28
|
+
|
|
29
|
+
**Do NOT mix these within a layer.** ECS in the meta-game layer creates anemic data bags with scattered business logic. DDD in the simulation layer creates cache-hostile object graphs that kill frame rates. Each paradigm is correct for its layer and wrong for the other.
|
|
30
|
+
|
|
31
|
+
### Game State Machines
|
|
32
|
+
|
|
33
|
+
State machines are the universal pattern in game development. They appear at every scale:
|
|
34
|
+
|
|
35
|
+
- **Character states**: Idle, Walking, Running, Jumping, Falling, Attacking, Stunned, Dead
|
|
36
|
+
- **Game states**: MainMenu, Loading, Playing, Paused, GameOver, Victory
|
|
37
|
+
- **AI states**: Patrol, Alert, Chase, Attack, Flee, Search
|
|
38
|
+
- **Animation states**: Blend trees driven by state machine transitions
|
|
39
|
+
- **UI states**: Screen stacks, modal dialogs, transition animations
|
|
40
|
+
|
|
41
|
+
State machines enforce that an entity can only be in one state at a time and that transitions between states are explicit and guarded.
|
|
42
|
+
|
|
43
|
+
### Game-Specific Ubiquitous Language
|
|
44
|
+
|
|
45
|
+
Games have domain-specific vocabulary that must be consistent across code, design docs, and team communication:
|
|
46
|
+
|
|
47
|
+
- **Entity**: A thing that exists in the game world (character, projectile, pickup, trigger zone)
|
|
48
|
+
- **Component**: A data bucket attached to an entity (Health, Transform, Renderable, Collider)
|
|
49
|
+
- **System**: A function that processes all entities with a specific component signature
|
|
50
|
+
- **Tick/Frame**: One iteration of the game loop
|
|
51
|
+
- **Spawn**: Creating a new entity at runtime
|
|
52
|
+
- **Despawn/Destroy**: Removing an entity from the world
|
|
53
|
+
- **Pool**: A pre-allocated set of reusable entities to avoid runtime allocation
|
|
54
|
+
- **Buff/Debuff**: A temporary modifier to an entity's stats
|
|
55
|
+
- **Cooldown**: A timer preventing repeated use of an ability
|
|
56
|
+
- **Aggro/Threat**: A value determining which target an AI prioritizes
|
|
57
|
+
|
|
58
|
+
## Deep Guidance
|
|
59
|
+
|
|
60
|
+
### ECS for the Simulation Layer
|
|
61
|
+
|
|
62
|
+
Entity-Component-System is a data-oriented architecture where:
|
|
63
|
+
- **Entities** are lightweight identifiers (typically just an integer ID)
|
|
64
|
+
- **Components** are plain data structs with no behavior (Position, Velocity, Health, DamageOnContact)
|
|
65
|
+
- **Systems** are functions that query for entities matching a component signature and process them
|
|
66
|
+
|
|
67
|
+
The key insight is that behavior lives in systems, not in entities or components. An entity does not "know" how to move — the MovementSystem queries all entities with Position+Velocity and updates their positions.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// ECS Example: Components are pure data, Systems are pure logic
|
|
71
|
+
|
|
72
|
+
// --- Components (data only, no methods) ---
|
|
73
|
+
|
|
74
|
+
interface Position {
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface Velocity {
|
|
80
|
+
dx: number;
|
|
81
|
+
dy: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Health {
|
|
85
|
+
current: number;
|
|
86
|
+
max: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface DamageOnContact {
|
|
90
|
+
amount: number;
|
|
91
|
+
destroySelfAfterHit: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface Collider {
|
|
95
|
+
radius: number;
|
|
96
|
+
layer: "player" | "enemy" | "projectile" | "pickup";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Systems (logic only, no state) ---
|
|
100
|
+
|
|
101
|
+
function movementSystem(
|
|
102
|
+
world: World,
|
|
103
|
+
deltaTime: number
|
|
104
|
+
): void {
|
|
105
|
+
// Query all entities that have BOTH Position and Velocity
|
|
106
|
+
for (const [entity, pos, vel] of world.query<[Position, Velocity]>()) {
|
|
107
|
+
pos.x += vel.dx * deltaTime;
|
|
108
|
+
pos.y += vel.dy * deltaTime;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collisionSystem(world: World): void {
|
|
113
|
+
// Query all entities with Position and Collider
|
|
114
|
+
const collidables = world.query<[Position, Collider]>();
|
|
115
|
+
|
|
116
|
+
for (const [entityA, posA, colA] of collidables) {
|
|
117
|
+
for (const [entityB, posB, colB] of collidables) {
|
|
118
|
+
if (entityA === entityB) continue;
|
|
119
|
+
// Layer-based collision filtering
|
|
120
|
+
if (!shouldCollide(colA.layer, colB.layer)) continue;
|
|
121
|
+
|
|
122
|
+
const dist = distance(posA, posB);
|
|
123
|
+
if (dist < colA.radius + colB.radius) {
|
|
124
|
+
world.emit("collision", { entityA, entityB });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function damageOnContactSystem(world: World): void {
|
|
131
|
+
// React to collision events
|
|
132
|
+
for (const { entityA, entityB } of world.events("collision")) {
|
|
133
|
+
const damageA = world.get<DamageOnContact>(entityA);
|
|
134
|
+
const healthB = world.get<Health>(entityB);
|
|
135
|
+
|
|
136
|
+
if (damageA && healthB) {
|
|
137
|
+
healthB.current -= damageA.amount;
|
|
138
|
+
if (damageA.destroySelfAfterHit) {
|
|
139
|
+
world.despawn(entityA);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Entity creation (composition, not inheritance) ---
|
|
146
|
+
|
|
147
|
+
function spawnPlayer(world: World, x: number, y: number): Entity {
|
|
148
|
+
return world.spawn(
|
|
149
|
+
{ x, y } as Position,
|
|
150
|
+
{ dx: 0, dy: 0 } as Velocity,
|
|
151
|
+
{ current: 100, max: 100 } as Health,
|
|
152
|
+
{ radius: 16, layer: "player" } as Collider
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function spawnProjectile(
|
|
157
|
+
world: World,
|
|
158
|
+
x: number, y: number,
|
|
159
|
+
dx: number, dy: number
|
|
160
|
+
): Entity {
|
|
161
|
+
return world.spawn(
|
|
162
|
+
{ x, y } as Position,
|
|
163
|
+
{ dx, dy } as Velocity,
|
|
164
|
+
{ amount: 25, destroySelfAfterHit: true } as DamageOnContact,
|
|
165
|
+
{ radius: 4, layer: "projectile" } as Collider
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Why ECS works for simulation:**
|
|
171
|
+
- **Cache efficiency**: Components of the same type are stored contiguously in memory; iterating over all Position components is a linear memory scan, not pointer-chasing through object graphs
|
|
172
|
+
- **Composition over inheritance**: An entity's behavior emerges from its component combination; no deep inheritance hierarchies or diamond problems
|
|
173
|
+
- **Parallelism**: Systems that touch non-overlapping component sets can run in parallel
|
|
174
|
+
- **Flexibility**: Adding new behavior means adding a new component and system, not modifying existing classes
|
|
175
|
+
|
|
176
|
+
**ECS pitfalls:**
|
|
177
|
+
- Debugging is harder — an entity is just an ID; you need tooling to inspect its component set
|
|
178
|
+
- Relational queries (find all enemies within range of a specific player) require spatial indexing, not just component queries
|
|
179
|
+
- One-off behaviors feel awkward — if only one entity has a unique mechanic, creating a component and system for one entity feels like overkill (but do it anyway for consistency)
|
|
180
|
+
|
|
181
|
+
### DDD for the Meta-Game Layer
|
|
182
|
+
|
|
183
|
+
The meta-game layer manages persistent state that exists outside the real-time simulation: player accounts, inventories, progression trees, matchmaking, economies, social graphs. These domains are rich in business rules and benefit from DDD's emphasis on encapsulation and domain language.
|
|
184
|
+
|
|
185
|
+
**Inventory example using DDD:**
|
|
186
|
+
- An Inventory is an Aggregate Root that enforces capacity limits, stacking rules, and equip requirements
|
|
187
|
+
- An Item is a Value Object — two items with the same properties are interchangeable
|
|
188
|
+
- Equipping an item is a domain operation on the Inventory aggregate, not a flag flip on the item
|
|
189
|
+
- Domain events (ItemEquipped, ItemDropped, InventoryFull) communicate changes to other systems
|
|
190
|
+
|
|
191
|
+
**Why DDD works for meta-game:**
|
|
192
|
+
- Business rules are encapsulated in domain objects, not scattered across controllers and services
|
|
193
|
+
- Ubiquitous language keeps code readable by designers ("player.inventory.equip(item)" reads like the design doc)
|
|
194
|
+
- Aggregates enforce transactional boundaries — an inventory operation either fully succeeds or fully rolls back
|
|
195
|
+
- Domain events enable loose coupling between meta-game subsystems
|
|
196
|
+
|
|
197
|
+
**Why DDD fails for simulation:**
|
|
198
|
+
- Object graphs with references and encapsulation create cache-hostile memory layouts
|
|
199
|
+
- Method dispatch (virtual calls through interfaces) prevents compiler optimization and branch prediction
|
|
200
|
+
- Encapsulation means systems cannot batch-process similar data across entities
|
|
201
|
+
- The overhead of aggregate boundaries and domain events is unacceptable at 60fps for thousands of entities
|
|
202
|
+
|
|
203
|
+
### State Machine Patterns
|
|
204
|
+
|
|
205
|
+
**Finite State Machine (FSM):**
|
|
206
|
+
|
|
207
|
+
The simplest state machine. Each state has a set of allowed transitions. An entity is in exactly one state at a time.
|
|
208
|
+
|
|
209
|
+
Use for: Character controllers, game flow, simple AI, UI screens.
|
|
210
|
+
|
|
211
|
+
Limitations: State explosion when states need to combine (walking+aiming, crouching+reloading). Transitions become a combinatorial matrix.
|
|
212
|
+
|
|
213
|
+
**Hierarchical State Machine (HSM):**
|
|
214
|
+
|
|
215
|
+
States can contain sub-states. A "Combat" super-state might contain "MeleeAttack," "RangedAttack," and "Block" sub-states. The super-state handles common transitions (e.g., any combat sub-state can transition to "Stunned"), reducing transition duplication.
|
|
216
|
+
|
|
217
|
+
Use for: Complex character controllers, sophisticated AI, multi-phase boss fights.
|
|
218
|
+
|
|
219
|
+
**Pushdown Automaton (PDA):**
|
|
220
|
+
|
|
221
|
+
A stack of states. "Pushing" a state pauses the current one; "popping" resumes it. The pause menu is a classic example: push Paused onto the Playing state, pop it to resume.
|
|
222
|
+
|
|
223
|
+
Use for: Menu stacks, interrupt-based gameplay (cutscene interrupts exploration, then resumes), nested game states.
|
|
224
|
+
|
|
225
|
+
**State machine implementation rules:**
|
|
226
|
+
- States should be classes or objects, not stringly-typed enums (allows behavior attachment)
|
|
227
|
+
- Every state must define: `onEnter()`, `onUpdate(dt)`, `onExit()`
|
|
228
|
+
- Transitions are guarded: a transition from Idle to Attack requires `hasAmmo && !isCooldown`
|
|
229
|
+
- State machines should be data-driven when possible (load states and transitions from config)
|
|
230
|
+
- Log state transitions during development — most gameplay bugs are state transition bugs
|
|
231
|
+
|
|
232
|
+
### Resource and Inventory Patterns
|
|
233
|
+
|
|
234
|
+
**Resource types in games:**
|
|
235
|
+
- **Currencies**: Discrete countable values (gold, gems, energy). Store as integers to avoid floating-point drift.
|
|
236
|
+
- **Items**: Discrete objects with properties (weapons, armor, consumables). May be stackable or unique.
|
|
237
|
+
- **Meters**: Continuous values that fill/drain (health, mana, stamina, fuel). Usually floats with clamping.
|
|
238
|
+
- **Timers**: Resources that regenerate over real or game time (energy, daily rewards, cooldowns).
|
|
239
|
+
|
|
240
|
+
**Inventory patterns:**
|
|
241
|
+
- **Slot-based**: Fixed number of slots, each holds one item (or stack). UI-friendly, capacity is explicit.
|
|
242
|
+
- **Weight-based**: Items have weight, inventory has a weight limit. More flexible, harder to visualize.
|
|
243
|
+
- **Hybrid**: Slot-based with weight limits (e.g., Diablo: grid-based slots, each item occupies different grid sizes).
|
|
244
|
+
|
|
245
|
+
**Economy sink/faucet balance:**
|
|
246
|
+
- Faucets (sources of currency): quest rewards, enemy drops, crafting, trading, daily login
|
|
247
|
+
- Sinks (drains of currency): item purchases, upgrades, repair costs, taxes, consumables
|
|
248
|
+
- If faucets exceed sinks, inflation occurs and currency becomes worthless
|
|
249
|
+
- Track currency generation and consumption rates per player-hour and tune aggressively
|
|
250
|
+
|
|
251
|
+
### Player Progression Models
|
|
252
|
+
|
|
253
|
+
**Experience/Level systems:**
|
|
254
|
+
- XP curve formula matters: linear (constant effort per level), polynomial (increasing effort), exponential (dramatically increasing effort)
|
|
255
|
+
- Common formula: `xp_for_level(n) = base * n^exponent` where exponent of 1.5-2.0 is typical
|
|
256
|
+
- Level caps should match content: if the game has 20 hours of content, the level cap should be reachable in roughly 20 hours
|
|
257
|
+
|
|
258
|
+
**Skill trees / Talent systems:**
|
|
259
|
+
- Flat trees (many small choices) vs deep trees (few branching paths with big impact)
|
|
260
|
+
- Respec cost: free respec encourages experimentation, costly respec encourages commitment
|
|
261
|
+
- Trap choices (options that are never worth taking) are a design failure — every node should be viable in some build
|
|
262
|
+
|
|
263
|
+
**Unlock progression:**
|
|
264
|
+
- Gate unlocks behind milestones, not just time played
|
|
265
|
+
- Sequence unlocks to gradually increase complexity (do not give the player every mechanic in the tutorial)
|
|
266
|
+
- Prestige mechanics extend endgame by trading progress for permanent bonuses
|
|
267
|
+
|
|
268
|
+
### Combat System Modeling
|
|
269
|
+
|
|
270
|
+
**Damage calculation patterns:**
|
|
271
|
+
- **Subtractive armor**: `damage = attack - defense` (simple, intuitive, but defense can fully negate)
|
|
272
|
+
- **Multiplicative armor**: `damage = attack * (1 - defense_percent)` (defense never fully negates)
|
|
273
|
+
- **Hybrid**: `damage = (attack - flat_reduction) * (1 - percent_reduction)` (most RPGs use this)
|
|
274
|
+
|
|
275
|
+
**Attack resolution flow:**
|
|
276
|
+
1. Attacker generates base damage (weapon + stats)
|
|
277
|
+
2. Apply attacker modifiers (buffs, critical hits, elemental bonuses)
|
|
278
|
+
3. Apply defender modifiers (armor, resistances, shields)
|
|
279
|
+
4. Apply environmental modifiers (cover, elevation, weather)
|
|
280
|
+
5. Resolve final damage (clamp to non-negative, apply to health)
|
|
281
|
+
6. Trigger feedback (hit animation, damage numbers, sound effect, screen shake)
|
|
282
|
+
|
|
283
|
+
### Spawning and Object Pooling
|
|
284
|
+
|
|
285
|
+
**Object pooling** pre-allocates a fixed number of entities and recycles them instead of creating/destroying at runtime. This avoids garbage collection spikes and allocation overhead.
|
|
286
|
+
|
|
287
|
+
**Pool sizing rules:**
|
|
288
|
+
- Size the pool to the maximum concurrent count needed plus a 20% buffer
|
|
289
|
+
- If the pool is exhausted, either grow it (with a warning log) or recycle the oldest active instance
|
|
290
|
+
- Pre-warm pools during loading screens, not during gameplay
|
|
291
|
+
- Profile actual usage to right-size pools — over-pooling wastes memory, under-pooling causes runtime allocation
|
|
292
|
+
|
|
293
|
+
**Spawn patterns:**
|
|
294
|
+
- **Wave spawning**: Groups of enemies spawn at intervals; common in action games and tower defense
|
|
295
|
+
- **Proximity spawning**: Entities spawn when the player enters a trigger zone; keeps entity counts manageable in open worlds
|
|
296
|
+
- **Procedural spawning**: Rules-based placement (spawn enemy X at least Y meters from player, at least Z meters from other enemies); used in roguelikes and open-world games
|
|
297
|
+
- **Director-based spawning**: An AI "director" monitors player performance and adjusts spawn rates, types, and placement to maintain tension (Left 4 Dead's AI Director is the canonical example)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: game-economy-design
|
|
3
|
+
description: Virtual currency design, faucet/sink balancing, loot table probability, monetization models, and ethical monetization patterns
|
|
4
|
+
topics: [game-dev, economy, monetization, loot-tables, balance, f2p]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Game economy design governs how players earn, spend, and value in-game resources and how the studio monetizes those systems. A well-designed economy creates meaningful choices — players decide what to spend resources on, creating engagement. A poorly designed economy either trivializes resources (inflation makes everything cheap) or frustrates players (everything feels unattainable). Monetization layers add a second dimension: real money must integrate with virtual economies without destroying the earned-value perception that makes the economy work.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
### Virtual Currency Design
|
|
12
|
+
|
|
13
|
+
Most games with economies use at least two currency tiers:
|
|
14
|
+
|
|
15
|
+
- **Soft currency**: Earned freely through gameplay (gold, coins, credits). Abundant faucets, many sinks. Players should never feel completely starved of soft currency.
|
|
16
|
+
- **Hard currency** (premium): Purchased with real money or earned in small quantities through milestones. Fewer faucets, high-value sinks (cosmetics, time-skips, premium items). This is the monetization lever.
|
|
17
|
+
|
|
18
|
+
Separating currencies prevents direct dollar-to-gameplay equivalence, which regulators and players scrutinize. Never allow hard currency to purchase competitive advantages in PvP contexts — this crosses into pay-to-win territory.
|
|
19
|
+
|
|
20
|
+
### Faucet/Sink Balancing
|
|
21
|
+
|
|
22
|
+
Every economy has faucets (sources of currency entering the system) and sinks (drains removing currency). When faucets exceed sinks, inflation occurs and currency becomes worthless. When sinks exceed faucets, deflation makes the game feel punishing. The goal is a steady state where players always have meaningful spending decisions.
|
|
23
|
+
|
|
24
|
+
Track currency generation and consumption per player-hour. A healthy economy has a target "time to purchase" for key items: if the best sword costs 10,000 gold and players earn 500 gold/hour, the time-to-purchase is 20 hours. Adjust faucets and sinks to hit target purchase timelines.
|
|
25
|
+
|
|
26
|
+
### Loot Table Probability
|
|
27
|
+
|
|
28
|
+
Loot tables define drop rates for items from enemies, chests, or gacha pulls. Transparency matters: China requires probability disclosure by law, and player trust erodes quickly when drop rates feel manipulated. Use weighted random selection with published rates for any monetized loot mechanic.
|
|
29
|
+
|
|
30
|
+
### Monetization Models
|
|
31
|
+
|
|
32
|
+
- **Premium (buy-to-play)**: One-time purchase, all content included. DLC/expansions sold separately. No predatory pressure. Revenue is front-loaded.
|
|
33
|
+
- **Free-to-play (F2P)**: Free entry, monetized through cosmetics, convenience, or content gates. Revenue is ongoing but depends on conversion rates (typically 2-5% of players spend money).
|
|
34
|
+
- **Hybrid**: Premium purchase with optional cosmetic microtransactions. Increasingly common (e.g., Helldivers 2, Diablo IV).
|
|
35
|
+
|
|
36
|
+
### Legal Landscape
|
|
37
|
+
|
|
38
|
+
- **China**: Probability disclosure is mandatory for any randomized paid mechanic. Published rates must match actual implementation.
|
|
39
|
+
- **Belgium/Netherlands**: Post-2022 FIFA loot box ruling nuanced the landscape. Belgium's Gaming Commission targeted specific implementations (FIFA Ultimate Team) rather than issuing a blanket loot box ban. The Netherlands situation shifted after court rulings; enforcement depends on whether the mechanic meets gambling criteria (prize, chance, stake). Neither country has a simple "loot boxes are banned" law — the legal test is whether the specific implementation constitutes gambling under existing frameworks.
|
|
40
|
+
- **COPPA (US)**: Games directed at children under 13 face strict data collection and monetization rules. Purchases require verifiable parental consent. Aggressive monetization targeting children invites FTC scrutiny regardless of COPPA technical compliance.
|
|
41
|
+
|
|
42
|
+
## Deep Guidance
|
|
43
|
+
|
|
44
|
+
### Faucet/Sink Math
|
|
45
|
+
|
|
46
|
+
A game economy is a system of flows. Model it as a spreadsheet before implementing it in code.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Economy simulation framework
|
|
50
|
+
// Model faucets and sinks as rates per player-hour
|
|
51
|
+
|
|
52
|
+
interface EconomyConfig {
|
|
53
|
+
currencies: CurrencyConfig[];
|
|
54
|
+
faucets: Faucet[];
|
|
55
|
+
sinks: Sink[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface CurrencyConfig {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
startingBalance: number;
|
|
62
|
+
maxBalance: number; // Soft cap — excess goes to overflow or is lost
|
|
63
|
+
decimalPlaces: 0 | 2; // 0 for integer currencies, 2 for float
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Faucet {
|
|
67
|
+
id: string;
|
|
68
|
+
currencyId: string;
|
|
69
|
+
amountPerEvent: number;
|
|
70
|
+
eventsPerHour: number; // Average occurrences per player-hour
|
|
71
|
+
variance: number; // 0-1, randomness factor
|
|
72
|
+
description: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Sink {
|
|
76
|
+
id: string;
|
|
77
|
+
currencyId: string;
|
|
78
|
+
cost: number;
|
|
79
|
+
purchasesPerHour: number; // Average purchases per player-hour
|
|
80
|
+
required: boolean; // Is this sink mandatory (repair) or optional (cosmetic)?
|
|
81
|
+
description: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Simulation ---
|
|
85
|
+
|
|
86
|
+
function simulateEconomy(
|
|
87
|
+
config: EconomyConfig,
|
|
88
|
+
hoursToSimulate: number,
|
|
89
|
+
playerCount: number
|
|
90
|
+
): SimulationResult {
|
|
91
|
+
const results: SimulationResult = {
|
|
92
|
+
hourlySnapshots: [],
|
|
93
|
+
inflationRate: 0,
|
|
94
|
+
medianBalance: 0,
|
|
95
|
+
timeToPurchase: new Map(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
for (const currency of config.currencies) {
|
|
99
|
+
let totalGenerated = 0;
|
|
100
|
+
let totalSpent = 0;
|
|
101
|
+
|
|
102
|
+
for (const faucet of config.faucets.filter(f => f.currencyId === currency.id)) {
|
|
103
|
+
const hourlyGeneration = faucet.amountPerEvent * faucet.eventsPerHour;
|
|
104
|
+
totalGenerated += hourlyGeneration * hoursToSimulate;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const sink of config.sinks.filter(s => s.currencyId === currency.id)) {
|
|
108
|
+
const hourlySpend = sink.cost * sink.purchasesPerHour;
|
|
109
|
+
totalSpent += hourlySpend * hoursToSimulate;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const netFlow = totalGenerated - totalSpent;
|
|
113
|
+
const inflationRate = netFlow / Math.max(totalGenerated, 1);
|
|
114
|
+
|
|
115
|
+
// HEALTHY: inflation rate between -0.1 and 0.1 (10% either direction)
|
|
116
|
+
// WARNING: inflation rate between 0.1 and 0.3 or -0.1 and -0.3
|
|
117
|
+
// CRITICAL: inflation rate beyond +-0.3
|
|
118
|
+
results.inflationRate = inflationRate;
|
|
119
|
+
results.medianBalance = currency.startingBalance + netFlow / playerCount;
|
|
120
|
+
|
|
121
|
+
// Calculate time-to-purchase for each sink
|
|
122
|
+
for (const sink of config.sinks.filter(s => s.currencyId === currency.id)) {
|
|
123
|
+
const hourlyNet = (totalGenerated - totalSpent) / hoursToSimulate;
|
|
124
|
+
const hourlyEarnings = totalGenerated / hoursToSimulate;
|
|
125
|
+
const ttp = sink.cost / hourlyEarnings;
|
|
126
|
+
results.timeToPurchase.set(sink.id, ttp);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface SimulationResult {
|
|
134
|
+
hourlySnapshots: number[];
|
|
135
|
+
inflationRate: number;
|
|
136
|
+
medianBalance: number;
|
|
137
|
+
timeToPurchase: Map<string, number>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Example: RPG economy ---
|
|
141
|
+
|
|
142
|
+
const rpgEconomy: EconomyConfig = {
|
|
143
|
+
currencies: [
|
|
144
|
+
{ id: "gold", name: "Gold", startingBalance: 100, maxBalance: 999999, decimalPlaces: 0 },
|
|
145
|
+
],
|
|
146
|
+
faucets: [
|
|
147
|
+
{ id: "quest-rewards", currencyId: "gold", amountPerEvent: 200, eventsPerHour: 1.5, variance: 0.2, description: "Quest completion rewards" },
|
|
148
|
+
{ id: "enemy-drops", currencyId: "gold", amountPerEvent: 15, eventsPerHour: 30, variance: 0.5, description: "Gold dropped by defeated enemies" },
|
|
149
|
+
{ id: "item-sales", currencyId: "gold", amountPerEvent: 50, eventsPerHour: 3, variance: 0.3, description: "Selling loot to vendors" },
|
|
150
|
+
{ id: "daily-login", currencyId: "gold", amountPerEvent: 100, eventsPerHour: 0.5, variance: 0, description: "Daily login bonus (prorated per hour assuming 2hr sessions)" },
|
|
151
|
+
],
|
|
152
|
+
sinks: [
|
|
153
|
+
{ id: "equipment", currencyId: "gold", cost: 500, purchasesPerHour: 0.2, required: false, description: "Buying weapons and armor" },
|
|
154
|
+
{ id: "consumables", currencyId: "gold", cost: 30, purchasesPerHour: 5, required: true, description: "Health potions, ammo, etc." },
|
|
155
|
+
{ id: "repairs", currencyId: "gold", cost: 75, purchasesPerHour: 0.5, required: true, description: "Equipment repair costs" },
|
|
156
|
+
{ id: "upgrades", currencyId: "gold", cost: 1000, purchasesPerHour: 0.05, required: false, description: "Upgrade slots on equipment" },
|
|
157
|
+
{ id: "fast-travel", currencyId: "gold", cost: 25, purchasesPerHour: 1, required: false, description: "Fast travel between locations" },
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Hourly faucet total: (200*1.5) + (15*30) + (50*3) + (100*0.5) = 300 + 450 + 150 + 50 = 950 gold/hr
|
|
162
|
+
// Hourly sink total: (500*0.2) + (30*5) + (75*0.5) + (1000*0.05) + (25*1) = 100 + 150 + 37.5 + 50 + 25 = 362.5 gold/hr
|
|
163
|
+
// Net flow: +587.5 gold/hr — INFLATIONARY, needs stronger sinks or weaker faucets
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Loot Table Design
|
|
167
|
+
|
|
168
|
+
Loot tables use weighted random selection. Each item has a weight, and the probability of dropping is its weight divided by the total weight of all items in the table.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Loot table with weighted random selection and pity system
|
|
172
|
+
|
|
173
|
+
interface LootTableEntry {
|
|
174
|
+
itemId: string;
|
|
175
|
+
weight: number; // Relative weight (NOT percentage)
|
|
176
|
+
rarity: "common" | "uncommon" | "rare" | "epic" | "legendary";
|
|
177
|
+
maxPerDrop: number; // Maximum times this item can appear in one drop
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface LootTable {
|
|
181
|
+
id: string;
|
|
182
|
+
entries: LootTableEntry[];
|
|
183
|
+
guaranteedDrops: number; // Minimum items per roll
|
|
184
|
+
bonusDropChance: number; // Probability of additional items (0-1)
|
|
185
|
+
maxDrops: number; // Maximum total items per roll
|
|
186
|
+
pityCounter?: PityConfig;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface PityConfig {
|
|
190
|
+
// After N rolls without a rare+ item, guarantee one
|
|
191
|
+
rareThreshold: number; // Rolls without rare → guarantee rare
|
|
192
|
+
epicThreshold: number; // Rolls without epic → guarantee epic
|
|
193
|
+
legendaryThreshold: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function rollLootTable(table: LootTable, playerPity: PityState): LootDrop[] {
|
|
197
|
+
const drops: LootDrop[] = [];
|
|
198
|
+
const totalWeight = table.entries.reduce((sum, e) => sum + e.weight, 0);
|
|
199
|
+
|
|
200
|
+
// Check pity system first
|
|
201
|
+
if (table.pityCounter && playerPity.rollsSinceLastLegendary >= table.pityCounter.legendaryThreshold) {
|
|
202
|
+
const legendaries = table.entries.filter(e => e.rarity === "legendary");
|
|
203
|
+
if (legendaries.length > 0) {
|
|
204
|
+
drops.push(selectWeightedFrom(legendaries));
|
|
205
|
+
playerPity.rollsSinceLastLegendary = 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Guaranteed drops
|
|
210
|
+
for (let i = drops.length; i < table.guaranteedDrops; i++) {
|
|
211
|
+
drops.push(selectWeighted(table.entries, totalWeight));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Bonus drops
|
|
215
|
+
while (drops.length < table.maxDrops && Math.random() < table.bonusDropChance) {
|
|
216
|
+
drops.push(selectWeighted(table.entries, totalWeight));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Update pity counters
|
|
220
|
+
const hasRare = drops.some(d => ["rare", "epic", "legendary"].includes(d.rarity));
|
|
221
|
+
const hasLegendary = drops.some(d => d.rarity === "legendary");
|
|
222
|
+
playerPity.rollsSinceLastRare = hasRare ? 0 : playerPity.rollsSinceLastRare + 1;
|
|
223
|
+
playerPity.rollsSinceLastLegendary = hasLegendary ? 0 : playerPity.rollsSinceLastLegendary + 1;
|
|
224
|
+
|
|
225
|
+
return drops;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function selectWeighted(entries: LootTableEntry[], totalWeight: number): LootDrop {
|
|
229
|
+
let roll = Math.random() * totalWeight;
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
roll -= entry.weight;
|
|
232
|
+
if (roll <= 0) {
|
|
233
|
+
return { itemId: entry.itemId, rarity: entry.rarity };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Fallback (should never reach due to floating point)
|
|
237
|
+
const last = entries[entries.length - 1];
|
|
238
|
+
return { itemId: last.itemId, rarity: last.rarity };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function selectWeightedFrom(subset: LootTableEntry[]): LootDrop {
|
|
242
|
+
const total = subset.reduce((s, e) => s + e.weight, 0);
|
|
243
|
+
return selectWeighted(subset, total);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface PityState {
|
|
247
|
+
rollsSinceLastRare: number;
|
|
248
|
+
rollsSinceLastLegendary: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
interface LootDrop {
|
|
252
|
+
itemId: string;
|
|
253
|
+
rarity: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Published probability disclosure (required in China, good practice everywhere)
|
|
257
|
+
// Given a table with weights: common=700, uncommon=200, rare=70, epic=25, legendary=5
|
|
258
|
+
// Total weight: 1000
|
|
259
|
+
// Probabilities: common=70%, uncommon=20%, rare=7%, epic=2.5%, legendary=0.5%
|
|
260
|
+
// With pity at 200 rolls: effective legendary rate is higher than 0.5%
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Battle Pass Structure
|
|
264
|
+
|
|
265
|
+
Battle passes are a retention mechanic: players purchase a pass and unlock rewards by playing over a defined season. The design must balance accessibility (casual players can complete it) with aspiration (dedicated players feel rewarded).
|
|
266
|
+
|
|
267
|
+
**Key design parameters:**
|
|
268
|
+
- **Season length**: 8-12 weeks is standard. Shorter seasons feel rushed; longer seasons lose urgency.
|
|
269
|
+
- **Total XP required**: Calculate backwards from target playtime. If the goal is 1 hour/day for 80% of the season, total XP = (daily XP rate) * (season days * 0.8).
|
|
270
|
+
- **Free vs premium tiers**: Free track gets meaningful rewards (not just scraps). Premium track gets exclusive cosmetics. Never gate gameplay-affecting items behind premium.
|
|
271
|
+
- **Catch-up mechanics**: Players who start late or miss days need a path to completion. Weekly challenges that award large XP chunks, bonus XP weekends, or purchasable tier skips (with limits).
|
|
272
|
+
|
|
273
|
+
### Monetization Models in Depth
|
|
274
|
+
|
|
275
|
+
**Premium (buy-to-play):**
|
|
276
|
+
|
|
277
|
+
Revenue is front-loaded at launch. Post-launch revenue comes from DLC and expansions. No daily engagement pressure on the economy. The economy can be generous because there is no monetization tension. This is the safest model for player trust but the hardest to sustain financially for live-service games.
|
|
278
|
+
|
|
279
|
+
**Free-to-play (F2P):**
|
|
280
|
+
|
|
281
|
+
Revenue depends on a small percentage of paying players. The economy must create desire without creating frustration. Common F2P revenue sources:
|
|
282
|
+
- Cosmetics (skins, emotes, effects) — safest; no gameplay impact
|
|
283
|
+
- Convenience (XP boosts, fast-travel, inventory expansion) — moderate; time-vs-money tradeoff
|
|
284
|
+
- Energy systems (limited plays per day, replenished with premium currency) — aggressive; feels exploitative
|
|
285
|
+
- Gacha/loot boxes (randomized rewards) — most profitable, most controversial, most regulated
|
|
286
|
+
|
|
287
|
+
**Conversion funnel:**
|
|
288
|
+
- 100% of players install for free
|
|
289
|
+
- ~30% reach meaningful engagement (play more than 1 hour)
|
|
290
|
+
- ~5% make any purchase ever
|
|
291
|
+
- ~1% become recurring spenders
|
|
292
|
+
- ~0.1% are "whales" (high spenders)
|
|
293
|
+
|
|
294
|
+
Designing for whales is ethically fraught. The industry is moving toward broader, lower-cost monetization that converts more of the 5% rather than extracting more from the 0.1%.
|
|
295
|
+
|
|
296
|
+
### Predatory Pattern Avoidance
|
|
297
|
+
|
|
298
|
+
These patterns damage player trust and invite regulatory action:
|
|
299
|
+
|
|
300
|
+
- **Artificial scarcity timers**: "Buy now or it's gone forever!" creates FOMO-driven purchasing. If used, ensure items genuinely return in future rotations.
|
|
301
|
+
- **Obfuscated pricing**: Converting real money to gems to coins to items makes it hard for players to understand what they are spending. Keep the money-to-value chain as short as possible.
|
|
302
|
+
- **Pay-to-win**: Any monetization that grants competitive advantage in PvP destroys game integrity and player trust. Even perceived pay-to-win (stat boosts, faster progression in competitive contexts) is toxic.
|
|
303
|
+
- **Undisclosed odds manipulation**: Adjusting loot table probabilities based on player spending patterns (giving better drops to new spenders to "hook" them) is deceptive and potentially illegal.
|
|
304
|
+
- **Dark patterns in purchase UI**: Making the "buy" button prominent and the "earn through gameplay" option hidden, using confusing currency bundles (1100 gems when items cost 1000, forcing leftover currency), or auto-selecting the most expensive option.
|
|
305
|
+
|
|
306
|
+
### Ethical Monetization Checklist
|
|
307
|
+
|
|
308
|
+
```yaml
|
|
309
|
+
ethical_monetization_checklist:
|
|
310
|
+
transparency:
|
|
311
|
+
- All purchasable items have clear real-money cost visible
|
|
312
|
+
- Loot box / gacha probabilities are published and accurate
|
|
313
|
+
- Currency conversion rates are simple and visible
|
|
314
|
+
- No hidden fees, auto-renewals without clear disclosure
|
|
315
|
+
|
|
316
|
+
fairness:
|
|
317
|
+
- No competitive advantage from spending money (PvP contexts)
|
|
318
|
+
- Free players can access all gameplay content (F2P model)
|
|
319
|
+
- Battle pass is completable with reasonable playtime
|
|
320
|
+
- No artificial throttling to pressure purchases
|
|
321
|
+
|
|
322
|
+
player_respect:
|
|
323
|
+
- Purchase confirmations prevent accidental spending
|
|
324
|
+
- Refund policy is clear and accessible
|
|
325
|
+
- No targeting of spending prompts based on loss/frustration
|
|
326
|
+
- Children and minors have spending protections
|
|
327
|
+
- No manipulative urgency ("limited time!" when it returns regularly)
|
|
328
|
+
|
|
329
|
+
legal_compliance:
|
|
330
|
+
- China: probability disclosure for all randomized paid mechanics
|
|
331
|
+
- Belgium/Netherlands: legal review of loot box implementation
|
|
332
|
+
- COPPA: parental consent for under-13 purchases
|
|
333
|
+
- Platform TOS: compliance with each platform's monetization rules
|
|
334
|
+
- GDPR: spending data handled per data protection regulations
|
|
335
|
+
|
|
336
|
+
economy_health:
|
|
337
|
+
- Inflation/deflation tracked with automated monitoring
|
|
338
|
+
- Economy simulation run before every balance change
|
|
339
|
+
- New faucets/sinks analyzed for impact before deployment
|
|
340
|
+
- Player wealth distribution monitored (Gini coefficient)
|
|
341
|
+
- Exploit detection for currency duplication or manipulation
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Economy Monitoring in Production
|
|
345
|
+
|
|
346
|
+
Once live, an economy requires ongoing monitoring. Key metrics:
|
|
347
|
+
|
|
348
|
+
- **Currency velocity**: How fast currency moves through the system (earned and spent per player-hour)
|
|
349
|
+
- **Median and mean balance**: Median is more informative than mean (whales skew the mean)
|
|
350
|
+
- **Gini coefficient**: Measures wealth inequality among players (0 = perfect equality, 1 = one player has everything). A healthy game economy typically targets 0.3-0.5.
|
|
351
|
+
- **Time-to-purchase drift**: If the time to buy a key item increases over time, the economy is deflating. If it decreases, it is inflating.
|
|
352
|
+
- **Sink utilization**: What percentage of players use each sink? Underused sinks need to be made more attractive or replaced.
|
|
353
|
+
- **Exploit detection**: Monitor for outlier currency gains (players earning 10x the average may have found a dupe exploit or farming bot)
|
|
354
|
+
|
|
355
|
+
Run economy simulations before every balance patch. A change that looks small ("increase quest rewards by 20%") can compound dramatically when millions of players execute it daily.
|