From ce561d53540c4814e02d7dc6a8a7406644cb3dde Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 13:48:29 +0000 Subject: [PATCH] Add technical context documents for Group B - Operations Created comprehensive technical specifications for three epics: - Epic 3: Planning & Automation (EPIC-03) - A* pathfinding algorithm design - Conveyor belt system architecture - AI route suggestion implementation - Performance targets: <100ms pathfinding, 60 FPS with 50+ conveyors - Epic 4: Production & Offline Systems (EPIC-04) - Tier 1 offline production (O(n) algorithm) - Tier 2 topological sort for conveyor dependencies - Welcome Back modal with ad boost integration - Anti-cheat time validation system - Epic 6: Quality & Progression (EPIC-06) - Tier 2 unlock requirements and validation - Real-time progression tracking UI - Celebration animation system (fireworks, confetti) - Achievement framework foundation All documents include: - System architecture alignment with Clean Architecture - Detailed API specifications and data models - Non-functional requirements (performance, security, observability) - Test strategies with acceptance criteria - Risk analysis and mitigation strategies Dependencies: Epic 1, 2, 5 completed Target Sprint: 5-6 (Week 7-8) --- ...pic-03-planning-automation-tech-context.md | 1012 ++++++++++++++++ ...epic-04-production-offline-tech-context.md | 1034 +++++++++++++++++ ...pic-06-quality-progression-tech-context.md | 1016 ++++++++++++++++ 3 files changed, 3062 insertions(+) create mode 100644 docs/epic-contexts/epic-03-planning-automation-tech-context.md create mode 100644 docs/epic-contexts/epic-04-production-offline-tech-context.md create mode 100644 docs/epic-contexts/epic-06-quality-progression-tech-context.md diff --git a/docs/epic-contexts/epic-03-planning-automation-tech-context.md b/docs/epic-contexts/epic-03-planning-automation-tech-context.md new file mode 100644 index 0000000..6c653e1 --- /dev/null +++ b/docs/epic-contexts/epic-03-planning-automation-tech-context.md @@ -0,0 +1,1012 @@ +# Epic Technical Specification: Planning & Automation (EPIC-03) + +Date: 2025-11-23 +Author: Claude (BMAD Game Development Agent) +Epic ID: EPIC-03 +Epic Name: Tier 2 Automation / Planning Systems +Status: Ready for Implementation +Group: B - Operations + +--- + +## Overview + +Epic 03 delivers the **Planning & Automation** core system that transforms Trade Factory Masters from a simple collection game into a strategic automation factory builder. This epic implements **A* pathfinding**, **conveyor belt systems**, and **automated resource transport** - the key differentiator from competitor games. + +**Epic Goal:** Enable players to design and automate complex production chains using conveyors that intelligently transport resources between buildings. + +**Business Value:** +- **Core Differentiator:** Automation gameplay distinguishes us from idle clickers (Tap Titans, Cookie Clicker) +- **Engagement Driver:** Planning optimal routes creates "one more turn" gameplay loop +- **Retention Impact:** 60%+ players unlock Tier 2 within 2-3 hours (target metric) +- **Monetization Hook:** Premium "Auto-Optimize Routes" IAP ($2.99) for advanced players + +**Technical Complexity:** HIGH (8-13 SP stories) due to A* algorithm implementation, performance optimization for 60 FPS with 50+ active conveyors, and complex state management. + +--- + +## Objectives and Scope + +### In-Scope (Must Have) + +✅ **A* Pathfinding Algorithm** +- Manhattan distance heuristic for grid-based pathfinding +- Obstacle avoidance (existing buildings, other conveyors, terrain) +- Performance: <100ms for 50-tile paths on Snapdragon 660 (target device) +- Return optimal path or null if no valid route exists + +✅ **Conveyor Entity System** +- ConveyorRoute data model (start/end buildings, path, resource type, state) +- JSON serialization for Hive (local) and Firestore (cloud save) +- State management: active, paused, blocked (insufficient resources) +- Travel time calculation: 1 tile/second base speed (upgradeable to 0.5s/tile at Level 5) + +✅ **Conveyor Rendering** +- Flame sprite rendering for conveyor tiles (belt, arrows, direction indicators) +- Resource sprite animation along path (smooth movement, 1 tile/sec) +- Visual feedback: green (active), yellow (paused), red (blocked) +- Performance: 60 FPS with 50+ conveyors + 100+ animated resource sprites + +✅ **Placement & Validation System** +- UI: Tap start building → tap end building → preview path → confirm/cancel +- Path validation: Check all tiles unoccupied before placement +- Cost calculation: 10 gold/tile (encourages efficient route planning) +- Undo/delete conveyor functionality + +✅ **AI Route Suggestion** +- "Suggest Route" button analyzes building placement +- Identifies unconnected production chains (e.g., Lumbermill → Smelter gap) +- Calls A* pathfinder and presents suggested path with cost preview +- Player can accept, modify, or reject suggestion + +### Out-of-Scope (Future Phases) + +❌ **Advanced Features (Post-MVP)** +- Conveyor upgrades (speed, multi-resource support) → EPIC-06 Progression +- Route optimization algorithms (minimize total tile count) → Phase 2 +- Underground conveyors (pass through buildings) → Month 3 feature +- Conveyor junction splits (1 input → 2 outputs) → Post-launch + +❌ **Performance Enhancements (If Needed)** +- Spatial hashing for collision detection (only if FPS <50) +- WebGL sprite batching (Flame supports natively, may not need custom solution) +- Conveyor pooling (object pooling pattern) → optimize if >100 conveyors + +### Success Criteria (Testable) + +| Metric | Target | Measurement | +|--------|--------|-------------| +| A* Performance | <100ms for 50-tile path | Performance test on Snapdragon 660 emulator | +| Rendering Performance | 60 FPS with 50 conveyors + 100 sprites | FPS counter in debug mode | +| Pathfinding Accuracy | 100% success for valid paths | 25 unit tests (various grid configurations) | +| Player Adoption | 60%+ players place ≥1 conveyor | Firebase Analytics event tracking | +| Session Length | +20% avg session time vs Tier 1 | A/B test with/without conveyors | + +--- + +## System Architecture Alignment + +### Clean Architecture Layers + +**Domain Layer (lib/domain/)** +``` +lib/domain/entities/ + ├── conveyor_route.dart # Core entity: path, state, metadata + ├── building.dart # Extended: input/output connection points + └── resource_flow.dart # New: Represents resource in transit + +lib/domain/use_cases/ + ├── pathfinding/ + │ ├── find_conveyor_path.dart # A* algorithm implementation + │ ├── validate_path.dart # Check path validity (obstacles, cost) + │ └── suggest_optimal_routes.dart # AI route suggestion logic + └── conveyors/ + ├── place_conveyor.dart # Create conveyor, deduct gold, validate + ├── delete_conveyor.dart # Remove, refund 50% gold + └── transport_resources.dart # Move resources along path (game loop) + +lib/domain/repositories/ + └── conveyor_repository.dart # Abstract: save/load conveyors +``` + +**Data Layer (lib/data/)** +``` +lib/data/models/ + └── conveyor_route_model.dart # JSON serialization, Hive TypeAdapter + +lib/data/repositories/ + ├── conveyor_repository_impl.dart # Implements domain interface + └── local_cache/ + └── hive_conveyor_cache.dart # Offline-first caching +``` + +**Presentation Layer (lib/presentation/)** +``` +lib/presentation/screens/game/ + └── widgets/ + ├── conveyor_placement_overlay.dart # UI: start/end selection, preview + ├── route_suggestion_button.dart # "AI Suggest" button + └── conveyor_cost_display.dart # "Cost: 150 gold (15 tiles)" + +lib/presentation/providers/ + ├── conveyor_placement_provider.dart # @riverpod state for placement flow + └── pathfinding_provider.dart # Cached A* results (memoization) +``` + +**Game Layer (lib/game/)** +``` +lib/game/systems/ + ├── pathfinding_system.dart # A* algorithm (pure Dart, no Flame deps) + └── conveyor_system.dart # Flame system: update resource positions + +lib/game/components/ + ├── conveyor_component.dart # Flame component: sprite rendering + └── resource_sprite_component.dart # Animated sprite moving along path +``` + +### Technology Stack Alignment + +| Component | Technology | Justification | +|-----------|-----------|---------------| +| Pathfinding | Pure Dart (no packages) | A* is 100 lines, no external deps needed | +| Data Structure | `PriorityQueue` (collection package) | Efficient open set for A* algorithm | +| Sprites | Flame SpriteComponent | Native Flame rendering, sprite batching | +| Animation | Flame MoveEffect | Smooth 1 tile/sec movement along path | +| State | Riverpod @riverpod | Reactive state updates when conveyors placed | +| Storage | Hive TypeAdapter | Fast local save/load of conveyor state | +| Cloud Sync | Firestore subcollection | `users/{uid}/conveyors/{conveyorId}` | + +### Integration Points + +**Epic 01 (Core Loop) Dependencies:** +- Grid System: A* uses `GridManager.isOccupied(x, y)` for obstacle detection +- Building Entity: Extended with `inputConnections` and `outputConnections` sets + +**Epic 02 (Economy) Dependencies:** +- Gold Deduction: Conveyor placement costs `10 gold × pathLength` +- Building Inventory: Resources consumed from output buildings, added to input buildings + +**Provides to Epic 04 (Offline Production):** +- `List` persisted to Hive for offline calculation +- Topological sort uses conveyor graph to determine production order + +--- + +## Detailed Design + +### Services and Modules + +#### 1. Pathfinding System (`lib/game/systems/pathfinding_system.dart`) + +**Responsibility:** Find optimal path from point A to B using A* algorithm. + +**API:** +```dart +class ConveyorPathfinder { + final Size gridSize; + final Set> occupiedTiles; + + ConveyorPathfinder({ + required this.gridSize, + required this.occupiedTiles, + }); + + /// Finds shortest path using A* with Manhattan heuristic. + /// Returns null if no valid path exists. + List>? findPath(Point start, Point end) { + // A* implementation (see Algorithm section below) + } + + /// Validates if path is placeable (no obstacles on route). + bool isPathValid(List> path) { + return path.every((tile) => !occupiedTiles.contains(tile)); + } + + /// Calculates gold cost for path (10 gold/tile). + int calculateCost(List> path) => path.length * 10; +} +``` + +**A* Algorithm (Pseudocode):** +``` +1. Initialize openSet = PriorityQueue (sorted by fScore) +2. Initialize closedSet = Set +3. Initialize gScore = Map { start: 0 } +4. Initialize cameFrom = Map + +5. Add start node to openSet + +6. While openSet not empty: + a. current = openSet.removeFirst() (lowest fScore) + b. If current == end: return reconstructPath(cameFrom, current) + c. Add current to closedSet + + d. For each neighbor in getNeighbors(current): + i. If neighbor in closedSet OR occupiedTiles: skip + ii. tentativeGScore = gScore[current] + 1 + iii. If tentativeGScore < gScore[neighbor]: + - cameFrom[neighbor] = current + - gScore[neighbor] = tentativeGScore + - fScore = tentativeGScore + heuristic(neighbor, end) + - Add Node(neighbor, fScore) to openSet + +7. Return null (no path found) +``` + +**Heuristic Function:** +```dart +double _heuristic(Point a, Point b) { + // Manhattan distance (4-directional movement) + return (a.x - b.x).abs() + (a.y - b.y).abs().toDouble(); +} +``` + +#### 2. Conveyor System (`lib/game/systems/conveyor_system.dart`) + +**Responsibility:** Update resource positions along conveyor paths (60 FPS game loop). + +**API:** +```dart +class ConveyorSystem extends System { + late final List _conveyors; + late final Map _activeResources; + + @override + void update(double dt) { + for (final conveyor in _conveyors.where((c) => c.state == ConveyorState.active)) { + _updateResourceFlow(conveyor, dt); + } + } + + void _updateResourceFlow(ConveyorRoute conveyor, double dt) { + // 1. Check if source building has resources + final sourceBuilding = _getBuildingById(conveyor.startBuildingId); + if (!sourceBuilding.hasOutputResource(conveyor.resourceType)) { + conveyor.state = ConveyorState.blocked; + return; + } + + // 2. Spawn new resource sprite if cooldown expired + if (conveyor.timeSinceLastSpawn >= conveyor.spawnInterval) { + final resource = sourceBuilding.consumeOutputResource(conveyor.resourceType, 1); + _spawnResourceSprite(resource, conveyor.path); + conveyor.timeSinceLastSpawn = 0; + } + + // 3. Update all resource sprites on this conveyor + for (final sprite in _activeResources.values.where((s) => s.conveyorId == conveyor.id)) { + sprite.position += sprite.velocity * dt; // Move along path + + // Check if reached destination + if (sprite.currentPathIndex >= conveyor.path.length) { + _deliverResource(sprite, conveyor.endBuildingId); + _activeResources.remove(sprite.id); + } + } + + conveyor.timeSinceLastSpawn += dt; + } +} +``` + +#### 3. Route Suggestion AI (`lib/domain/use_cases/pathfinding/suggest_optimal_routes.dart`) + +**Responsibility:** Analyze building layout and suggest conveyor placements. + +**Logic:** +```dart +class SuggestOptimalRoutesUseCase { + final ConveyorPathfinder pathfinder; + + List execute(PlayerEconomy economy) { + final suggestions = []; + + // 1. Find all buildings with output resources + final producers = economy.buildings.where((b) => b.production.outputResource != null); + + // 2. Find all buildings that consume those resources + for (final producer in producers) { + final resourceType = producer.production.outputResource!.type; + final consumers = economy.buildings.where( + (b) => b.production.inputResources.contains(resourceType), + ); + + // 3. For each producer-consumer pair, check if already connected + for (final consumer in consumers) { + if (_isConnected(producer, consumer, economy.conveyors)) continue; + + // 4. Try to find path + final path = pathfinder.findPath( + producer.gridPosition, + consumer.gridPosition, + ); + + if (path != null) { + suggestions.add(SuggestedRoute( + from: producer, + to: consumer, + path: path, + cost: path.length * 10, + reason: 'Connect ${producer.type} → ${consumer.type} (${resourceType})', + )); + } + } + } + + // 5. Sort by priority (shortest path first, high-value resources prioritized) + suggestions.sort((a, b) => a.cost.compareTo(b.cost)); + + return suggestions.take(3).toList(); // Show top 3 suggestions + } +} +``` + +--- + +### Data Models and Contracts + +#### ConveyorRoute Entity + +```dart +import 'package:equatable/equatable.dart'; + +enum ConveyorState { active, paused, blocked } + +class ConveyorRoute extends Equatable { + final String id; + final String startBuildingId; + final String endBuildingId; + final List> path; + final ResourceType resourceType; + final ConveyorState state; + final int level; // 1-5, affects speed + final double timeSinceLastSpawn; // Internal state for resource flow + + const ConveyorRoute({ + required this.id, + required this.startBuildingId, + required this.endBuildingId, + required this.path, + required this.resourceType, + this.state = ConveyorState.active, + this.level = 1, + this.timeSinceLastSpawn = 0.0, + }); + + /// Base speed: 1 tile/sec at level 1, 0.5 tile/sec at level 5 + double get tileSpeed => 1.0 / level; + + /// Time between resource spawns (seconds) + double get spawnInterval => 5.0; // Spawn 1 resource every 5 seconds + + /// Gold cost to place this conveyor + int get placementCost => path.length * 10; + + /// Refund when deleting (50% of placement cost) + int get deletionRefund => (placementCost * 0.5).toInt(); + + @override + List get props => [ + id, + startBuildingId, + endBuildingId, + path, + resourceType, + state, + level, + ]; + + ConveyorRoute copyWith({ + ConveyorState? state, + int? level, + double? timeSinceLastSpawn, + }) { + return ConveyorRoute( + id: id, + startBuildingId: startBuildingId, + endBuildingId: endBuildingId, + path: path, + resourceType: resourceType, + state: state ?? this.state, + level: level ?? this.level, + timeSinceLastSpawn: timeSinceLastSpawn ?? this.timeSinceLastSpawn, + ); + } +} +``` + +#### ConveyorRouteModel (Data Layer) + +```dart +import 'package:hive/hive.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'conveyor_route_model.g.dart'; + +@HiveType(typeId: 3) +@JsonSerializable() +class ConveyorRouteModel { + @HiveField(0) + final String id; + + @HiveField(1) + final String startBuildingId; + + @HiveField(2) + final String endBuildingId; + + @HiveField(3) + final List> path; // [{x: 0, y: 0}, {x: 1, y: 0}, ...] + + @HiveField(4) + final String resourceType; + + @HiveField(5) + final String state; // 'active', 'paused', 'blocked' + + @HiveField(6) + final int level; + + ConveyorRouteModel({ + required this.id, + required this.startBuildingId, + required this.endBuildingId, + required this.path, + required this.resourceType, + required this.state, + required this.level, + }); + + factory ConveyorRouteModel.fromJson(Map json) => + _$ConveyorRouteModelFromJson(json); + + Map toJson() => _$ConveyorRouteModelToJson(this); + + // Domain entity conversion + ConveyorRoute toDomain() { + return ConveyorRoute( + id: id, + startBuildingId: startBuildingId, + endBuildingId: endBuildingId, + path: path.map((p) => Point(p['x']!, p['y']!)).toList(), + resourceType: ResourceType.values.firstWhere((t) => t.name == resourceType), + state: ConveyorState.values.firstWhere((s) => s.name == state), + level: level, + ); + } + + static ConveyorRouteModel fromDomain(ConveyorRoute conveyor) { + return ConveyorRouteModel( + id: conveyor.id, + startBuildingId: conveyor.startBuildingId, + endBuildingId: conveyor.endBuildingId, + path: conveyor.path.map((p) => {'x': p.x, 'y': p.y}).toList(), + resourceType: conveyor.resourceType.name, + state: conveyor.state.name, + level: conveyor.level, + ); + } +} +``` + +--- + +### APIs and Interfaces + +#### ConveyorRepository (Domain Interface) + +```dart +abstract class ConveyorRepository { + /// Fetches all conveyors for current player (local + cloud merge) + Future> getConveyors(); + + /// Saves conveyor to local cache and syncs to Firestore + Future saveConveyor(ConveyorRoute conveyor); + + /// Deletes conveyor from local cache and Firestore + Future deleteConveyor(String conveyorId); + + /// Batch update for offline production (efficient bulk update) + Future updateConveyorStates(Map updates); +} +``` + +#### ConveyorRepositoryImpl (Data Layer) + +```dart +class ConveyorRepositoryImpl implements ConveyorRepository { + final Box _hiveBox; + final FirebaseFirestore _firestore; + final FirebaseAuth _auth; + + ConveyorRepositoryImpl(this._hiveBox, this._firestore, this._auth); + + @override + Future> getConveyors() async { + final userId = _auth.currentUser!.uid; + + // 1. Load from local cache first (offline-first) + final localConveyors = _hiveBox.values.map((m) => m.toDomain()).toList(); + + // 2. Sync from Firestore if online + try { + final snapshot = await _firestore + .collection('users') + .doc(userId) + .collection('conveyors') + .get() + .timeout(Duration(seconds: 3)); + + final cloudConveyors = snapshot.docs + .map((doc) => ConveyorRouteModel.fromJson(doc.data()).toDomain()) + .toList(); + + // 3. Merge: cloud wins for conflicts (last-write-wins) + final merged = _mergeConveyors(localConveyors, cloudConveyors); + + // 4. Update local cache + await _hiveBox.clear(); + for (final conveyor in merged) { + await _hiveBox.put(conveyor.id, ConveyorRouteModel.fromDomain(conveyor)); + } + + return merged; + } catch (e) { + // Offline or network error: return local cache + return localConveyors; + } + } + + @override + Future saveConveyor(ConveyorRoute conveyor) async { + final model = ConveyorRouteModel.fromDomain(conveyor); + + // 1. Save to local cache immediately + await _hiveBox.put(conveyor.id, model); + + // 2. Sync to Firestore (fire-and-forget, retry on next app open) + final userId = _auth.currentUser!.uid; + _firestore + .collection('users') + .doc(userId) + .collection('conveyors') + .doc(conveyor.id) + .set(model.toJson()) + .catchError((e) => print('Firestore sync failed: $e')); + } + + // ... deleteConveyor, updateConveyorStates implementations +} +``` + +--- + +### Workflows and Sequencing + +#### Workflow 1: Player Places Conveyor + +``` +┌──────────┐ ┌────────────────┐ ┌──────────────┐ ┌────────────┐ +│ Player │ │ Presentation │ │ Domain │ │ Data │ +│ │ │ Layer │ │ Layer │ │ Layer │ +└────┬─────┘ └────────┬───────┘ └──────┬───────┘ └─────┬──────┘ + │ │ │ │ + │ 1. Tap "Conveyor" │ │ │ + ├───────────────────>│ │ │ + │ │ │ │ + │ 2. Tap Start │ │ │ + │ Building │ │ │ + ├───────────────────>│ │ │ + │ │ │ │ + │ 3. Tap End │ │ │ + │ Building │ │ │ + ├───────────────────>│ │ │ + │ │ 4. FindPath │ │ + │ │ (start, end) │ │ + │ ├───────────────────>│ │ + │ │ │ │ + │ │ 5. Path Preview │ │ + │ │<───────────────────┤ │ + │ │ │ │ + │ 6. Confirm │ │ │ + ├───────────────────>│ │ │ + │ │ 7. PlaceConveyor │ │ + │ │ (path, cost) │ │ + │ ├───────────────────>│ │ + │ │ │ 8. ValidatePath │ + │ │ │ & DeductGold │ + │ │ ├───────────────────┤ + │ │ │ │ + │ │ │ 9. SaveConveyor │ + │ │ ├──────────────────>│ + │ │ │ │ + │ │ │10. Success │ + │ │ │<──────────────────┤ + │ │11. ConveyorPlaced │ │ + │ │ Event │ │ + │ │<───────────────────┤ │ + │ │ │ │ + │12. Show Conveyor │ │ │ + │ on Grid │ │ │ + │<───────────────────┤ │ │ + │ │ │ │ +``` + +#### Workflow 2: Resource Transport (Game Loop) + +``` +Every Frame (60 FPS): +1. ConveyorSystem.update(dt) called by Flame engine +2. For each active conveyor: + a. Check if source building has resources + - If yes: Proceed to step 3 + - If no: Mark conveyor as 'blocked', show red indicator + + b. Check spawn cooldown (timeSinceLastSpawn >= 5 seconds) + - If ready: Spawn new ResourceSprite at start position + - If not ready: Increment timeSinceLastSpawn += dt + + c. Update all ResourceSprite positions on this conveyor + - Move sprite along path (velocity = 1 tile/sec × dt) + - If reached next path point: Increment sprite.currentPathIndex + - If reached end of path: Deliver resource to destination building + +3. Render all conveyor sprites (Flame batches automatically) +4. Render all resource sprites (Flame batches automatically) +``` + +**Performance Optimization:** +- **Sprite Batching:** Flame automatically batches sprites with same texture +- **Culling:** Only update/render conveyors in viewport (CameraComponent.visibleRect) +- **Fixed Timestep:** Use `update(double dt)` for frame-rate independent movement + +--- + +## Non-Functional Requirements + +### Performance + +**Target Device:** Snapdragon 660, 4GB RAM (mid-tier Android phone from 2018) + +| Metric | Requirement | Test Method | +|--------|-------------|-------------| +| Frame Rate | 60 FPS sustained | Debug FPS counter, 5-min gameplay session | +| A* Pathfinding | <100ms for 50-tile path | Performance test: `Stopwatch` in unit test | +| Conveyor Rendering | 50+ conveyors, 100+ sprites | Stress test: Place max conveyors, monitor FPS | +| Memory Usage | <200MB RAM for game layer | Android Studio Profiler | +| Battery Drain | <10% per hour | Manual test: 1-hour gameplay session | + +**Optimization Strategies:** +1. **Object Pooling:** Reuse ResourceSprite components (create pool of 200 sprites) +2. **Spatial Partitioning:** Grid-based culling (only render visible conveyors) +3. **A* Caching:** Memoize frequently used paths (e.g., common building positions) +4. **Sprite Atlases:** Pack all conveyor/resource sprites into single texture (reduce draw calls) + +### Security + +**Data Integrity:** +- **Client-Side Validation:** Check path validity before placement (prevent cheating) +- **Server-Side Validation:** Cloud Functions validate gold cost on Firestore write + ```javascript + // Firebase Cloud Function + exports.validateConveyorPlacement = functions.firestore + .document('users/{userId}/conveyors/{conveyorId}') + .onCreate((snap, context) => { + const conveyor = snap.data(); + const pathLength = conveyor.path.length; + const expectedCost = pathLength * 10; + + // Check if player has enough gold (read from users/{userId}/economy) + // If not: Delete conveyor document, log cheat attempt + }); + ``` + +**Anti-Cheat:** +- **Path Validation:** Server checks all path tiles are valid (no diagonal, no out-of-bounds) +- **Cost Verification:** Server recalculates cost and verifies gold deduction +- **Rate Limiting:** Max 100 conveyors per player (prevent spam/DOS) + +### Reliability/Availability + +**Offline Support:** +- **Local-First:** All conveyor data cached in Hive (works offline) +- **Sync on Connect:** Merge local + cloud state when connection restored +- **Conflict Resolution:** Last-write-wins (cloud timestamp > local timestamp) + +**Error Handling:** +```dart +try { + final path = pathfinder.findPath(start, end); + if (path == null) { + showSnackBar('No valid path found. Try different positions.'); + return; + } +} catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError(e, stackTrace); + showSnackBar('Pathfinding error. Please restart the app.'); +} +``` + +### Observability + +**Firebase Analytics Events:** +```dart +// Track conveyor placement +FirebaseAnalytics.instance.logEvent( + name: 'conveyor_placed', + parameters: { + 'path_length': path.length, + 'cost': pathCost, + 'resource_type': resourceType.name, + 'ai_suggested': usedAISuggestion, // true/false + }, +); + +// Track pathfinding performance +FirebaseAnalytics.instance.logEvent( + name: 'pathfinding_performance', + parameters: { + 'duration_ms': stopwatch.elapsedMilliseconds, + 'path_length': path?.length ?? 0, + 'grid_size': '${gridSize.width}x${gridSize.height}', + }, +); +``` + +**Crashlytics Custom Keys:** +```dart +FirebaseCrashlytics.instance.setCustomKey('active_conveyors', conveyors.length); +FirebaseCrashlytics.instance.setCustomKey('grid_size', '50x50'); +``` + +--- + +## Dependencies and Integrations + +### Upstream Dependencies (Must Complete First) + +| Epic/Story | Dependency Type | Required Artifacts | +|------------|----------------|-------------------| +| **EPIC-01: Core Loop** | Hard Dependency | `GridManager.isOccupied()`, `Building.gridPosition` | +| **STORY-01.5: Grid System** | Hard Dependency | Grid rendering, tile coordinate system | +| **EPIC-02: Economy** | Hard Dependency | `PlayerEconomy.gold`, `Building.production.outputResource` | +| **STORY-02.1: Resource Definitions** | Hard Dependency | `ResourceType` enum, `ResourceDefinition` class | + +### Downstream Dependents (Will Use This Epic) + +| Epic/Story | How It Uses EPIC-03 | +|------------|---------------------| +| **EPIC-04: Offline Production** | Uses `List` for topological sort, resource flow simulation | +| **EPIC-06: Progression** | Tier 2 unlock requires placing ≥1 conveyor (achievement) | +| **EPIC-10: Analytics** | Tracks conveyor placement rate, AI suggestion adoption, avg path length | + +### External Integrations + +| Service | Purpose | API Endpoint | +|---------|---------|-------------| +| Firebase Firestore | Cloud save for conveyors | `users/{uid}/conveyors/{conveyorId}` | +| Firebase Analytics | Event tracking | `FirebaseAnalytics.instance.logEvent()` | +| Hive | Local cache | `Box('conveyors')` | + +--- + +## Acceptance Criteria (Authoritative) + +### Functional Acceptance Criteria + +✅ **AC-1: Pathfinding Accuracy** +- Given: 50×50 grid with obstacles (buildings at random positions) +- When: Player requests path from (0, 0) to (49, 49) +- Then: A* returns optimal path OR null if no valid path exists +- Test: 25 unit tests with various grid configurations + +✅ **AC-2: Conveyor Placement** +- Given: Player has 500 gold, 2 buildings at (5,5) and (10,10) +- When: Player taps "Conveyor" → taps building A → taps building B → confirms +- Then: + - Path preview shown with cost (e.g., "8 tiles × 10 gold = 80 gold") + - On confirm: Conveyor created, 80 gold deducted + - Conveyor renders on grid with green color (active state) +- Test: Widget test + integration test + +✅ **AC-3: Resource Transport** +- Given: Conveyor connecting Lumbermill (produces Wood) to Smelter (consumes Wood) +- When: 60 seconds elapse (game time) +- Then: + - 12 Wood sprites spawn (1 every 5 seconds) + - Each sprite moves along path at 1 tile/sec + - When sprite reaches Smelter: Wood added to Smelter's input storage +- Test: Integration test with mocked time + +✅ **AC-4: AI Route Suggestion** +- Given: 3 buildings (A produces Wood, B unconnected, C consumes Wood) +- When: Player taps "Suggest Routes" button +- Then: + - UI shows "Suggested: A → C (5 tiles, 50 gold)" + - On accept: Conveyor auto-placed, gold deducted +- Test: Unit test for suggestion logic + widget test for UI + +✅ **AC-5: Performance** +- Given: 50 conveyors placed, 100 resource sprites active +- When: Player plays for 5 minutes +- Then: FPS stays ≥60 FPS (measured with debug counter) +- Test: Performance test on Snapdragon 660 emulator + +### Non-Functional Acceptance Criteria + +✅ **AC-6: Offline Support** +- Given: 10 conveyors placed while online +- When: Player goes offline → closes app → reopens offline +- Then: All 10 conveyors render and transport resources (no cloud needed) +- Test: Manual test with airplane mode + +✅ **AC-7: Error Handling** +- Given: Player tries to place conveyor with path that crosses obstacles +- When: Pathfinder returns null +- Then: Show error: "No valid path. Try different positions." +- Test: Unit test for null path handling + +--- + +## Traceability Mapping + +| Requirement | Epic/Story | Acceptance Criteria | +|-------------|-----------|---------------------| +| **FR-003: Tier 2 Automation** | EPIC-03 (this doc) | AC-1 through AC-5 | +| FR-003.1: A* Pathfinding | STORY-03.1 | AC-1 (Pathfinding Accuracy) | +| FR-003.2: Conveyor Placement | STORY-03.3 | AC-2 (Conveyor Placement) | +| FR-003.3: Resource Transport | STORY-03.4 | AC-3 (Resource Transport) | +| FR-003.4: AI Suggestions | STORY-03.5 | AC-4 (AI Route Suggestion) | +| **NFR-001: Performance** | Cross-cutting | AC-5 (60 FPS with 50 conveyors) | +| **NFR-002: Offline-First** | Cross-cutting | AC-6 (Offline Support) | + +--- + +## Risks, Assumptions, Open Questions + +### High Risks + +🔴 **RISK-1: A* Performance on Low-End Devices** +- **Impact:** HIGH - Pathfinding >100ms breaks 60 FPS target +- **Likelihood:** MEDIUM - A* is O(n log n), may struggle on 50×50 grid with obstacles +- **Mitigation:** + 1. Implement A* with binary heap (not PriorityQueue) for faster open set operations + 2. Add path caching: Store frequently used paths in Map<(start, end), path> + 3. Fallback: If pathfinding >100ms, show loading spinner (non-blocking) +- **Contingency:** If still slow, limit grid size to 30×30 for pathfinding (performance mode) + +🔴 **RISK-2: FPS Drop with 50+ Conveyors** +- **Impact:** HIGH - <60 FPS causes poor user experience, negative reviews +- **Likelihood:** MEDIUM - 50 conveyors × 100 sprites = 5000 draw calls/frame +- **Mitigation:** + 1. Use Flame sprite batching (automatic, same texture = 1 draw call) + 2. Implement object pooling for ResourceSprite (reuse components) + 3. Culling: Only render conveyors in viewport +- **Contingency:** Reduce max conveyors to 30, or add "Low Graphics Mode" setting + +🟡 **RISK-3: Offline Production with Conveyors (Complex)** +- **Impact:** MEDIUM - Wrong offline calculation = players lose resources = churn +- **Likelihood:** MEDIUM - Topological sort + resource flow simulation is complex +- **Mitigation:** + 1. Extensive unit tests (40+ tests for STORY-04.2) + 2. Add "Offline Production Log" UI to show calculation details (transparency) + 3. Beta test with 100 players before launch +- **Contingency:** Disable conveyor offline production in v1.0, add in v1.1 after validation + +### Assumptions + +1. **Pathfinding Grid:** Assumes 50×50 grid is sufficient for MVP (can expand later) +2. **4-Directional Movement:** Assumes no diagonal movement (simplifies A* heuristic) +3. **Single Resource per Conveyor:** Assumes conveyors transport 1 resource type (multi-resource in v2.0) +4. **No Conveyor Intersections:** Assumes conveyors cannot cross each other (simplifies collision) + +### Open Questions + +❓ **Q1:** Should conveyors have visual levels (e.g., Level 3 = blue belt)? +- **Decision Needed By:** Sprint 3 planning +- **Owner:** UX Designer +- **Impact:** Medium (affects sprite assets needed) + +❓ **Q2:** Should AI suggestions be free or cost gold? +- **Decision Needed By:** Sprint 3 planning +- **Owner:** Game Designer +- **Impact:** Low (balance decision, can A/B test) + +❓ **Q3:** Should conveyors auto-pause when source runs out of resources? +- **Decision Needed By:** Sprint 4 (before STORY-03.4) +- **Owner:** Game Designer +- **Current Approach:** Show "blocked" state (red color), don't pause (still consumes resources when available) + +--- + +## Test Strategy Summary + +### Unit Tests (Target: 90+ tests) + +**Pathfinding (25 tests):** +- ✅ Find path (0,0) → (5,5) on empty grid = 11 tiles +- ✅ Find path with obstacles (building at 2,2) = detour around +- ✅ No path exists (end surrounded by obstacles) = return null +- ✅ Path to self (start == end) = [start] +- ✅ Path with single obstacle blocking direct route +- ✅ Performance: 50-tile path in <100ms + +**Conveyor Entity (15 tests):** +- ✅ Calculate travel time: 10-tile path at level 1 = 10 seconds +- ✅ Calculate cost: 15-tile path = 150 gold +- ✅ Calculate refund: 150 gold placement = 75 gold refund +- ✅ State transitions: active → blocked (no resources) +- ✅ JSON serialization roundtrip (toJson → fromJson) + +**Route Suggestion (10 tests):** +- ✅ Suggest route: Lumbermill → unconnected Smelter +- ✅ No suggestions if all buildings connected +- ✅ Prioritize short paths over long paths +- ✅ Skip suggestions if no valid path exists + +**Placement Use Case (20 tests):** +- ✅ Place conveyor: deduct gold, create ConveyorRoute entity +- ✅ Insufficient gold: throw exception, no conveyor created +- ✅ Invalid path (overlaps building): throw exception +- ✅ Delete conveyor: refund 50% gold, remove from list + +**Transport Use Case (30 tests):** +- ✅ Spawn resource every 5 seconds (time-based test) +- ✅ Move resource along path (position updates) +- ✅ Deliver resource to destination building (inventory updated) +- ✅ Block conveyor if source has no resources + +### Integration Tests (5 tests) + +- ✅ End-to-end: Place conveyor → resources flow → destination receives +- ✅ Offline sync: Place conveyor offline → go online → syncs to Firestore +- ✅ Multi-conveyor: 3 conveyors active simultaneously, no conflicts +- ✅ AI suggestion accepted: Tap suggest → tap accept → conveyor placed +- ✅ Performance: 50 conveyors, 100 sprites, 60 FPS for 5 minutes + +### Widget Tests (8 tests) + +- ✅ Conveyor placement UI: Tap start → tap end → preview shown +- ✅ Cost display: "Cost: 150 gold (15 tiles)" shown in preview +- ✅ Confirm button: Tap confirm → conveyor renders on grid +- ✅ Cancel button: Tap cancel → no conveyor placed, no gold deducted +- ✅ Insufficient gold: Show error snackbar "Not enough gold" +- ✅ AI suggestion button: Tap → suggestion modal appears +- ✅ Conveyor state indicator: Green (active), red (blocked) +- ✅ Delete conveyor: Long-press → confirm delete → refund shown + +### Performance Tests (3 tests) + +- ✅ A* benchmark: 50-tile path in <100ms on Snapdragon 660 +- ✅ FPS test: 50 conveyors + 100 sprites = 60 FPS sustained +- ✅ Memory test: Conveyor system uses <50MB RAM + +**Total Tests:** 90+ unit + 5 integration + 8 widget + 3 performance = **106 tests** + +--- + +## Implementation Notes + +**Sprint Allocation:** +- **Sprint 3 (Week 5):** STORY-03.1 (A*), STORY-03.2 (Entity) +- **Sprint 4 (Week 6):** STORY-03.3 (Placement UI), STORY-03.4 (Transport), STORY-03.5 (Rendering) +- **Sprint 5 (Week 7):** STORY-03.6 (AI Suggestion), STORY-03.7 (Validation) + +**Developer Notes:** +- Use `collection` package for PriorityQueue (part of Dart SDK) +- Flame's `SpriteComponent` automatically batches sprites with same texture +- Test on real device (Snapdragon 660) before sprint review +- Add debug overlay: Show pathfinding time, active conveyor count, FPS + +**Code Review Checklist:** +- [ ] A* returns null for impossible paths (no infinite loops) +- [ ] All path tiles validated before placement (no cheating) +- [ ] Conveyor state updates trigger UI rebuild (Riverpod reactivity) +- [ ] Performance: No List.map() in update() loop (causes allocations) +- [ ] Tests: 100% coverage for pathfinding, conveyor entity, placement logic + +--- + +**Document Status:** ✅ Ready for Development +**Next Steps:** Sprint 3 planning, assign STORY-03.1 and STORY-03.2 +**Questions:** Contact Game Designer (AI suggestion pricing) or Architect (performance concerns) diff --git a/docs/epic-contexts/epic-04-production-offline-tech-context.md b/docs/epic-contexts/epic-04-production-offline-tech-context.md new file mode 100644 index 0000000..35cb422 --- /dev/null +++ b/docs/epic-contexts/epic-04-production-offline-tech-context.md @@ -0,0 +1,1034 @@ +# Epic Technical Specification: Production & Offline Systems (EPIC-04) + +Date: 2025-11-23 +Author: Claude (BMAD Game Development Agent) +Epic ID: EPIC-04 +Epic Name: Offline Production +Status: Ready for Implementation +Group: B - Operations + +--- + +## Overview + +Epic 04 implements the **Offline Production System** that calculates resources earned while players are away from the game. This is a **critical retention driver** - players who receive meaningful offline rewards are 2.5× more likely to return within 24 hours (industry benchmark: Idle Miner Tycoon, AdVenture Capitalist). + +**Epic Goal:** Accurately simulate production for Tier 1 (simple) and Tier 2 (conveyor-based) buildings during offline periods, present rewards in an engaging "Welcome Back" modal, and offer 2× boost via rewarded video ads. + +**Business Value:** +- **Retention Driver:** Offline rewards incentivize daily return (Day 1, Day 7, Day 30 retention) +- **Monetization Hook:** 60%+ players watch ads for 2× boost (benchmark: $0.015 eCPM × 60% × 10k DAU = $90/day) +- **Perceived Progress:** Players feel they're advancing even when not actively playing +- **Competitive Parity:** All successful idle games have offline production (Cookie Clicker, Egg Inc., Idle Heroes) + +**Technical Complexity:** HIGH (8 SP stories) due to: +- Offline calculation must be <50ms for 20 buildings + 50 conveyors (performance constraint) +- Tier 2 requires topological sort for dependency graphs (Lumbermill → Smelter → Workshop chains) +- Accurate simulation prevents exploits (time manipulation, resource duplication) +- Integration with ads SDK (AdMob) requires careful error handling + +--- + +## Objectives and Scope + +### In-Scope (Must Have) + +✅ **Tier 1 Offline Production (Simple Buildings)** +- O(n) algorithm: Iterate buildings, calculate (minutesElapsed × productionRate) +- Apply storage capacity limits (100 Wood max → stop production when full) +- Apply offline cap: 12 hours for Tier 1 (prevents infinite accumulation) +- Return `OfflineProductionResult` with resources earned per building type + +✅ **Tier 2 Offline Production (Conveyor Chains)** +- Build dependency graph from conveyor connections +- Topological sort (Kahn's algorithm): Process buildings in correct order +- Simulate resource flow: Deduct inputs, add outputs, respect bottlenecks +- Handle complex chains: Lumbermill (Wood) → Smelter (Iron) → Workshop (Tools) +- Apply offline cap: 24 hours for Tier 2 (rewards advanced players) + +✅ **Welcome Back Modal** +- Full-screen modal on app resume: "Welcome Back! You were away for 3h 24m" +- Animated counter: Resources earned (Wood +450, Iron +120, Gold +1,200) +- Show breakdown: "10 Lumbermills produced 450 Wood (capped at 12h)" +- "Watch Ad for 2× Rewards" button (60-second delay before showing) +- "Collect" button: Add resources to player inventory, dismiss modal + +✅ **2× Ad Boost (Rewarded Video)** +- Integrate Google AdMob SDK (rewarded video ad format) +- On "Watch Ad" tap: Show 30-second video ad +- On ad complete: Double all offline rewards, log analytics event +- On ad failed/dismissed: Award 1× rewards (no penalty for trying) +- Cooldown: One ad per offline session (prevents spam) + +✅ **Time Validation (Anti-Cheat)** +- Server timestamp validation: Compare device time vs Firebase server time +- Reject if device time manipulated (>5 minutes drift) +- Log suspicious activity to Crashlytics (manual review for ban) +- Fallback: Use last known server timestamp if offline + +### Out-of-Scope (Future Phases) + +❌ **Advanced Features (Post-MVP)** +- Offline speed boosts (Premium Pass: 1.5× offline production) → Month 2 +- Push notifications: "Your factory produced 10k gold!" → Month 3 +- Offline production history log (show last 7 days) → v1.1 +- Idle mode (background production while app open) → Out of scope for MVP + +❌ **Extended Offline Caps** +- 48-hour cap for VIP players → Post-launch feature +- Infinite offline (Premium IAP) → Not planned (breaks balance) + +### Success Criteria (Testable) + +| Metric | Target | Measurement | +|--------|--------|-------------| +| Calculation Performance | <50ms for 20 buildings + 50 conveyors | Performance test on Snapdragon 660 | +| Accuracy | 100% correct for Tier 1, 95% for Tier 2 | 70+ unit tests (edge cases) | +| Ad Adoption | 60%+ players watch ad for 2× boost | Firebase Analytics event tracking | +| Day 1 Retention | +15% vs no offline production | A/B test cohort analysis | +| Day 7 Retention | +10% vs no offline production | A/B test cohort analysis | + +--- + +## System Architecture Alignment + +### Clean Architecture Layers + +**Domain Layer (lib/domain/)** +``` +lib/domain/entities/ + ├── offline_production_result.dart # Result: resources, gold, time elapsed, capped + └── production_snapshot.dart # Snapshot: buildings, conveyors, last seen time + +lib/domain/use_cases/ + ├── offline_production/ + │ ├── calculate_offline_production_tier1.dart # O(n) simple algorithm + │ ├── calculate_offline_production_tier2.dart # Topological sort + simulation + │ └── validate_time_integrity.dart # Anti-cheat: detect time manipulation + └── ads/ + ├── show_rewarded_ad.dart # AdMob integration wrapper + └── apply_ad_boost.dart # Double rewards if ad watched + +lib/domain/repositories/ + ├── offline_production_repository.dart # Save/load last seen timestamp + └── ads_repository.dart # Abstract: show ads, track impressions +``` + +**Data Layer (lib/data/)** +``` +lib/data/models/ + ├── offline_production_result_model.dart # JSON serialization + └── production_snapshot_model.dart # Firestore document structure + +lib/data/repositories/ + ├── offline_production_repository_impl.dart # Firestore + Hive caching + └── ads_repository_impl.dart # AdMob SDK wrapper +``` + +**Presentation Layer (lib/presentation/)** +``` +lib/presentation/screens/ + └── welcome_back/ + ├── welcome_back_modal.dart # Full-screen modal + ├── widgets/ + │ ├── resource_counter_widget.dart # Animated counter (0 → 450 Wood) + │ ├── ad_boost_button.dart # "Watch Ad for 2×" CTA + │ └── production_breakdown_card.dart # "10 Lumbermills → 450 Wood" + +lib/presentation/providers/ + ├── offline_production_provider.dart # @riverpod: Calculate on app resume + └── ads_provider.dart # @riverpod: AdMob ad state (loaded, shown, completed) +``` + +**Game Layer (lib/game/)** +``` +lib/game/systems/ + └── offline_production_system.dart # Integrates with Flame lifecycle (onResume) +``` + +### Technology Stack Alignment + +| Component | Technology | Justification | +|-----------|-----------|---------------| +| Offline Calculation | Pure Dart (no packages) | Simple algorithms, no external deps | +| Topological Sort | Pure Dart (Queue, Map) | Kahn's algorithm is ~50 lines | +| Time Validation | Firebase Firestore `FieldValue.serverTimestamp()` | Prevents device time manipulation | +| Ad SDK | Google AdMob Flutter Plugin | Industry standard, 90% fill rate | +| Ad Mediation | AdMob Mediation (optional) | Maximize eCPM with Unity Ads, AppLovin fallbacks | +| Storage | Hive (last seen time) + Firestore (cloud backup) | Offline-first, syncs when online | +| State Management | Riverpod @riverpod | Reactive updates for modal display | + +### Integration Points + +**Epic 01 (Core Loop) Dependencies:** +- Building Entity: Uses `Building.productionRate`, `Building.storageCapacity` +- PlayerEconomy: Adds earned resources to player inventory + +**Epic 02 (Economy) Dependencies:** +- Resource Definitions: Calculates gold value (Wood × baseMarketPrice) +- Market Prices: Offline gold earnings based on resource values + +**Epic 03 (Planning) Dependencies:** +- Conveyor Routes: `List` used for dependency graph +- Topological Sort: Determines production order (source → sink) + +**Provides to Epic 05 (Mobile UX):** +- Welcome Back Modal: Full-screen takeover on app resume +- Integrates with app lifecycle (AppLifecycleState.resumed) + +**Provides to Epic 08 (Monetization):** +- Rewarded Video Ads: Primary ad format (60%+ adoption rate) +- Ad revenue: $0.015 eCPM × 60% watch rate × daily sessions + +--- + +## Detailed Design + +### Services and Modules + +#### 1. Offline Production Algorithm - Tier 1 (Simple) + +**File:** `lib/domain/use_cases/offline_production/calculate_offline_production_tier1.dart` + +**Responsibility:** Calculate production for buildings without conveyor dependencies (Tier 1). + +**Algorithm:** +```dart +class CalculateOfflineProductionTier1UseCase { + OfflineProductionResult execute({ + required List buildings, + required DateTime lastSeen, + required DateTime now, + }) { + final minutesElapsed = now.difference(lastSeen).inMinutes; + final cappedMinutes = min(minutesElapsed, 12 * 60); // 12-hour cap + + final resourcesEarned = {}; + var totalGoldValue = 0; + + for (final building in buildings.where((b) => b.tier == 1)) { + // Calculate raw production + final rawProduction = cappedMinutes * building.productionRate; + + // Apply storage capacity limit + final cappedProduction = min( + rawProduction, + building.production.storageCapacity, + ); + + // Add to results + final resourceType = building.production.outputResource.type; + resourcesEarned[resourceType] = + (resourcesEarned[resourceType] ?? 0) + cappedProduction.toInt(); + + // Calculate gold value + totalGoldValue += cappedProduction.toInt() * + ResourceDefinitions.getPrice(resourceType); + } + + return OfflineProductionResult( + resourcesEarned: resourcesEarned, + totalGoldValue: totalGoldValue, + minutesElapsed: cappedMinutes, + wasCapped: minutesElapsed > 12 * 60, + capReason: minutesElapsed > 12 * 60 ? '12-hour limit' : null, + ); + } +} +``` + +**Complexity:** O(n) where n = number of buildings +**Performance:** <10ms for 20 buildings (simple arithmetic) + +**Edge Cases Handled:** +- Zero buildings: Return empty result (no error) +- Zero time elapsed: Return zero resources +- Negative time (clock change): Treat as zero elapsed +- Storage full: Stop production at capacity (e.g., 100 Wood max) + +#### 2. Offline Production Algorithm - Tier 2 (Complex) + +**File:** `lib/domain/use_cases/offline_production/calculate_offline_production_tier2.dart` + +**Responsibility:** Calculate production for buildings with conveyor dependencies (Tier 2). + +**Algorithm (Topological Sort + Simulation):** +```dart +class CalculateOfflineProductionTier2UseCase { + OfflineProductionResult execute({ + required List buildings, + required List conveyors, + required DateTime lastSeen, + required DateTime now, + }) { + final minutesElapsed = now.difference(lastSeen).inMinutes; + final cappedMinutes = min(minutesElapsed, 24 * 60); // 24-hour cap for Tier 2 + + // Step 1: Build dependency graph + final graph = _buildDependencyGraph(buildings, conveyors); + + // Step 2: Topological sort (Kahn's algorithm) + final sortedBuildings = _topologicalSort(buildings, graph); + + // Step 3: Simulate production in dependency order + final inventory = {}; // buildingId → available resources + final resourcesEarned = {}; + + for (final building in sortedBuildings) { + // Calculate how much this building can produce + final maxProduction = cappedMinutes * building.productionRate; + + // Check if building has required inputs (from conveyors) + final inputRequirements = _getInputRequirements(building, conveyors); + var actualProduction = maxProduction; + + for (final input in inputRequirements.entries) { + final requiredAmount = input.value * cappedMinutes; + final availableAmount = inventory[input.key] ?? 0; + + if (availableAmount < requiredAmount) { + // Bottleneck: Not enough inputs + actualProduction = min( + actualProduction, + (availableAmount / input.value).floor(), + ); + } + } + + // Deduct consumed inputs + for (final input in inputRequirements.entries) { + inventory[input.key] = (inventory[input.key] ?? 0) - + (input.value * actualProduction).toInt(); + } + + // Add produced outputs + final outputResource = building.production.outputResource.type; + inventory[building.id] = (inventory[building.id] ?? 0) + actualProduction.toInt(); + resourcesEarned[outputResource] = + (resourcesEarned[outputResource] ?? 0) + actualProduction.toInt(); + } + + final totalGoldValue = resourcesEarned.entries + .map((e) => e.value * ResourceDefinitions.getPrice(e.key)) + .fold(0, (a, b) => a + b); + + return OfflineProductionResult( + resourcesEarned: resourcesEarned, + totalGoldValue: totalGoldValue, + minutesElapsed: cappedMinutes, + wasCapped: minutesElapsed > 24 * 60, + capReason: minutesElapsed > 24 * 60 ? '24-hour limit' : null, + ); + } + + /// Build adjacency list from conveyors + Map> _buildDependencyGraph( + List buildings, + List conveyors, + ) { + final graph = >{}; + for (final building in buildings) { + graph[building.id] = []; + } + for (final conveyor in conveyors) { + // endBuilding depends on startBuilding + graph[conveyor.endBuildingId]!.add(conveyor.startBuildingId); + } + return graph; + } + + /// Kahn's algorithm for topological sort + List _topologicalSort( + List buildings, + Map> graph, + ) { + final inDegree = {}; + for (final building in buildings) { + inDegree[building.id] = graph[building.id]!.length; + } + + final queue = Queue(); + for (final building in buildings) { + if (inDegree[building.id] == 0) { + queue.add(building); + } + } + + final sorted = []; + while (queue.isNotEmpty) { + final current = queue.removeFirst(); + sorted.add(current); + + // Remove edges from current + for (final neighborId in graph.keys) { + if (graph[neighborId]!.contains(current.id)) { + inDegree[neighborId] = inDegree[neighborId]! - 1; + if (inDegree[neighborId] == 0) { + final neighbor = buildings.firstWhere((b) => b.id == neighborId); + queue.add(neighbor); + } + } + } + } + + if (sorted.length != buildings.length) { + throw CircularDependencyException('Conveyor cycle detected'); + } + + return sorted; + } +} +``` + +**Complexity:** O(V + E) where V = buildings, E = conveyors (standard topological sort) +**Performance:** <50ms for 20 buildings + 50 conveyors + +**Edge Cases Handled:** +- Circular dependencies: Throw exception (should never happen, but defensive) +- Missing inputs (bottleneck): Produce zero output for downstream buildings +- Multiple conveyors to same building: Aggregate inputs + +#### 3. Time Validation (Anti-Cheat) + +**File:** `lib/domain/use_cases/offline_production/validate_time_integrity.dart` + +**Responsibility:** Detect device time manipulation (players changing clock to farm resources). + +**API:** +```dart +class ValidateTimeIntegrityUseCase { + final FirebaseFirestore _firestore; + final FirebaseAuth _auth; + + Future getValidatedServerTime() async { + final userId = _auth.currentUser!.uid; + + try { + // Write dummy document with server timestamp + final docRef = _firestore.collection('_time_validation').doc(userId); + await docRef.set({ + 'timestamp': FieldValue.serverTimestamp(), + }); + + // Read back server timestamp + final snapshot = await docRef.get(); + final serverTime = (snapshot.data()!['timestamp'] as Timestamp).toDate(); + + // Compare with device time + final deviceTime = DateTime.now(); + final drift = deviceTime.difference(serverTime).inMinutes.abs(); + + if (drift > 5) { + // Device time manipulated (>5 min drift) + FirebaseCrashlytics.instance.log( + 'Time manipulation detected: device=$deviceTime, server=$serverTime, drift=${drift}min', + ); + FirebaseAnalytics.instance.logEvent( + name: 'time_cheat_detected', + parameters: {'drift_minutes': drift}, + ); + + // Use server time instead (don't reward cheaters) + return serverTime; + } + + return deviceTime; // Device time is valid + } catch (e) { + // Offline: Use device time (can't validate, but better than blocking) + return DateTime.now(); + } + } +} +``` + +**Anti-Cheat Logic:** +1. On app resume: Call `getValidatedServerTime()` +2. Calculate offline time: `serverTime - lastSeenTime` +3. If drift >5 min: Use server time, log event (manual review for ban) +4. If offline: Trust device time (no internet, can't validate) + +#### 4. AdMob Rewarded Video Integration + +**File:** `lib/data/repositories/ads_repository_impl.dart` + +**Responsibility:** Load and show rewarded video ads, handle callbacks. + +**API:** +```dart +class AdsRepositoryImpl implements AdsRepository { + RewardedAd? _rewardedAd; + bool _isAdLoaded = false; + + @override + Future loadRewardedAd() async { + await RewardedAd.load( + adUnitId: Platform.isAndroid + ? 'ca-app-pub-3940256099942544/5224354917' // Test ID + : 'ca-app-pub-3940256099942544/1712485313', + request: AdRequest(), + rewardedAdLoadCallback: RewardedAdLoadCallback( + onAdLoaded: (ad) { + _rewardedAd = ad; + _isAdLoaded = true; + }, + onAdFailedToLoad: (error) { + print('RewardedAd failed to load: $error'); + _isAdLoaded = false; + }, + ), + ); + } + + @override + Future showRewardedAd() async { + if (!_isAdLoaded || _rewardedAd == null) { + return false; // Ad not ready + } + + final completer = Completer(); + + _rewardedAd!.fullScreenContentCallback = FullScreenContentCallback( + onAdDismissedFullScreenContent: (ad) { + ad.dispose(); + _rewardedAd = null; + _isAdLoaded = false; + completer.complete(false); // User dismissed ad (no reward) + }, + onAdFailedToShowFullScreenContent: (ad, error) { + ad.dispose(); + _rewardedAd = null; + _isAdLoaded = false; + completer.complete(false); // Ad failed to show + }, + ); + + bool rewardEarned = false; + _rewardedAd!.show( + onUserEarnedReward: (ad, reward) { + rewardEarned = true; + FirebaseAnalytics.instance.logEvent( + name: 'ad_rewarded', + parameters: { + 'ad_type': 'offline_production_2x', + 'reward_amount': reward.amount, + }, + ); + }, + ); + + await _rewardedAd!.show(onUserEarnedReward: (ad, reward) {}); + completer.complete(rewardEarned); + + return completer.future; + } + + @override + bool isAdReady() => _isAdLoaded; +} +``` + +--- + +### Data Models and Contracts + +#### OfflineProductionResult Entity + +```dart +class OfflineProductionResult extends Equatable { + final Map resourcesEarned; + final int totalGoldValue; + final int minutesElapsed; + final bool wasCapped; + final String? capReason; + + const OfflineProductionResult({ + required this.resourcesEarned, + required this.totalGoldValue, + required this.minutesElapsed, + required this.wasCapped, + this.capReason, + }); + + /// Format elapsed time: "3h 24m" + String get formattedElapsedTime { + final hours = minutesElapsed ~/ 60; + final minutes = minutesElapsed % 60; + if (hours > 0) { + return '${hours}h ${minutes}m'; + } + return '${minutes}m'; + } + + /// Apply 2× boost from ad + OfflineProductionResult applyBoost(double multiplier) { + return OfflineProductionResult( + resourcesEarned: resourcesEarned.map( + (type, amount) => MapEntry(type, (amount * multiplier).toInt()), + ), + totalGoldValue: (totalGoldValue * multiplier).toInt(), + minutesElapsed: minutesElapsed, + wasCapped: wasCapped, + capReason: capReason, + ); + } + + @override + List get props => [ + resourcesEarned, + totalGoldValue, + minutesElapsed, + wasCapped, + capReason, + ]; +} +``` + +--- + +### APIs and Interfaces + +#### OfflineProductionRepository + +```dart +abstract class OfflineProductionRepository { + /// Get last seen timestamp (local + cloud merge) + Future getLastSeenTime(); + + /// Update last seen timestamp on app pause + Future updateLastSeenTime(DateTime time); + + /// Save offline production result for analytics + Future saveOfflineProductionLog(OfflineProductionResult result); +} +``` + +#### AdsRepository + +```dart +abstract class AdsRepository { + /// Load rewarded video ad (call on app start) + Future loadRewardedAd(); + + /// Show ad and return true if user earned reward + Future showRewardedAd(); + + /// Check if ad is ready to show + bool isAdReady(); +} +``` + +--- + +### Workflows and Sequencing + +#### Workflow 1: App Resume → Welcome Back Modal + +``` +┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ +│ Flutter │ │ Domain Layer │ │ Data Layer │ +│ Lifecycle │ │ (Use Cases) │ │ (Firestore) │ +└──────┬───────┘ └────────┬────────┘ └────────┬─────────┘ + │ │ │ + │ 1. AppLifecycleState │ │ + │ .resumed │ │ + ├─────────────────────>│ │ + │ │ │ + │ │ 2. GetLastSeenTime │ + │ ├──────────────────────>│ + │ │ │ + │ │ 3. lastSeen = 3h ago │ + │ │<──────────────────────┤ + │ │ │ + │ │ 4. ValidateTime │ + │ │ (anti-cheat) │ + │ ├──────────────────────>│ + │ │ │ + │ │ 5. serverTime = now │ + │ │<──────────────────────┤ + │ │ │ + │ │ 6. CalculateOffline │ + │ │ Production(3h) │ + │ │ │ + │ │ [Tier 1 calculation] │ + │ │ [Tier 2 topological │ + │ │ sort + simulation] │ + │ │ │ + │ 7. Result: │ │ + │ Wood +450 │ │ + │ Iron +120 │ │ + │ Gold +1,200 │ │ + │<─────────────────────┤ │ + │ │ │ + │ 8. Show Welcome │ │ + │ Back Modal │ │ + │ (animated) │ │ + │ │ │ + │ [60-second delay] │ │ + │ │ │ + │ 9. Show "Watch Ad │ │ + │ for 2×" button │ │ + │ │ │ +``` + +#### Workflow 2: Player Watches Ad for 2× Boost + +``` +┌──────────┐ ┌────────────────┐ ┌─────────────┐ ┌──────────────┐ +│ Player │ │ Presentation │ │ Domain │ │ AdMob SDK │ +└────┬─────┘ └────────┬───────┘ └──────┬──────┘ └──────┬───────┘ + │ │ │ │ + │ 1. Tap "Watch Ad" │ │ │ + ├───────────────────>│ │ │ + │ │ 2. ShowRewardedAd()│ │ + │ ├───────────────────>│ │ + │ │ │ 3. Load ad │ + │ │ ├──────────────────>│ + │ │ │ │ + │ │ │ 4. Show ad │ + │ │ │<──────────────────┤ + │ │ │ │ + │ [Watch 30-sec ad] │ │ │ + │ │ │ │ + │ │ │ 5. onRewardEarned │ + │ │ │<──────────────────┤ + │ │ │ │ + │ │ 6. ApplyBoost(2.0) │ │ + │ │<───────────────────┤ │ + │ │ │ │ + │ │ [Double resources: │ │ + │ │ Wood 450→900] │ │ + │ │ │ │ + │ 7. Updated rewards │ │ │ + │ shown (2×) │ │ │ + │<───────────────────┤ │ │ + │ │ │ │ +``` + +--- + +## Non-Functional Requirements + +### Performance + +| Metric | Requirement | Test Method | +|--------|-------------|-------------| +| Tier 1 Calculation | <10ms for 20 buildings | Performance test with Stopwatch | +| Tier 2 Calculation | <50ms for 20 buildings + 50 conveyors | Performance test with Stopwatch | +| Modal Load Time | <300ms from app resume to modal shown | User experience test | +| Ad Load Time | <3 seconds for rewarded video | AdMob test ads | +| Memory Usage | <50MB for offline calculation | Android Studio Profiler | + +**Optimization Strategies:** +1. **Lazy Calculation:** Only calculate offline production when modal shown (not on every app resume) +2. **Caching:** Cache topological sort result (reuse if graph unchanged) +3. **Batch Firestore Reads:** Single read for lastSeenTime + buildings + conveyors +4. **Ad Preloading:** Load rewarded video on app start (ready when needed) + +### Security + +**Anti-Cheat Measures:** +```dart +// 1. Server timestamp validation +final serverTime = await _getServerTime(); // Firestore FieldValue.serverTimestamp() +final deviceTime = DateTime.now(); +final drift = deviceTime.difference(serverTime).inMinutes.abs(); + +if (drift > 5) { + // Time manipulation detected + _logCheatAttempt(drift); + return OfflineProductionResult.empty(); // No rewards for cheaters +} + +// 2. Maximum cap enforcement +final cappedMinutes = min(minutesElapsed, 24 * 60); // Hard cap + +// 3. Resource validation +if (result.totalGoldValue > 1000000) { + // Suspicious: >1M gold from offline production + _logSuspiciousActivity(result); + // Flag account for manual review +} +``` + +**Firestore Security Rules:** +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /users/{userId} { + allow write: if request.auth.uid == userId && + request.resource.data.offlineGoldEarned < 100000; // Max 100k per session + } + } +} +``` + +### Reliability/Availability + +**Error Handling:** +```dart +try { + final result = await _calculateOfflineProduction(); + _showWelcomeBackModal(result); +} catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError(e, stackTrace); + + // Fallback: Show simplified modal without breakdown + _showSimplifiedWelcomeBack('Welcome back! Rewards calculated.'); +} +``` + +**Ad Failure Handling:** +```dart +final adShown = await _adsRepository.showRewardedAd(); + +if (!adShown) { + // Ad failed to load or user dismissed + showSnackBar('Ad not available. You still get 1× rewards!'); + // Award 1× rewards (no penalty) +} +``` + +### Observability + +**Firebase Analytics Events:** +```dart +// Track offline production events +FirebaseAnalytics.instance.logEvent( + name: 'offline_production_calculated', + parameters: { + 'minutes_elapsed': result.minutesElapsed, + 'total_gold_value': result.totalGoldValue, + 'was_capped': result.wasCapped, + 'tier': hasConveyors ? 'tier2' : 'tier1', + }, +); + +// Track ad adoption +FirebaseAnalytics.instance.logEvent( + name: 'offline_ad_shown', + parameters: { + 'watched_ad': watchedAd, // true/false + 'boost_value': result.totalGoldValue, // How much they could have earned + }, +); +``` + +--- + +## Dependencies and Integrations + +### Upstream Dependencies + +| Epic/Story | Required Artifacts | +|------------|-------------------| +| **EPIC-01: Core Loop** | `Building.productionRate`, `Building.storageCapacity` | +| **EPIC-02: Economy** | `ResourceDefinitions.getPrice()`, `PlayerEconomy.addResources()` | +| **EPIC-03: Planning** | `List`, dependency graph | +| **EPIC-05: Mobile UX** | App lifecycle hooks (`AppLifecycleState.resumed`) | + +### Downstream Dependents + +| Epic | How It Uses EPIC-04 | +|------|---------------------| +| **EPIC-08: Monetization** | Rewarded video ad implementation, ad revenue tracking | +| **EPIC-10: Analytics** | Offline production metrics, retention cohort analysis | + +--- + +## Acceptance Criteria (Authoritative) + +✅ **AC-1: Tier 1 Offline Calculation** +- Given: Player has 5 Lumbermills (1 Wood/min each), offline for 2 hours +- When: App resumes +- Then: Welcome Back modal shows "Wood +600" (5 × 60 min × 2 hr = 600) +- Test: Unit test with mocked time + +✅ **AC-2: Tier 2 Offline Calculation (Conveyor Chain)** +- Given: Lumbermill → Conveyor → Smelter, offline for 1 hour +- When: App resumes +- Then: Wood produced (60), Iron produced (30, assumes 2 Wood → 1 Iron recipe) +- Test: Integration test with topological sort + +✅ **AC-3: Storage Capacity Limit** +- Given: Lumbermill (max 100 Wood storage), offline for 10 hours +- When: App resumes +- Then: Wood +100 (capped), modal shows "Storage full after 100 min" +- Test: Unit test with capacity constraint + +✅ **AC-4: 12-Hour Cap (Tier 1)** +- Given: Player offline for 48 hours +- When: App resumes +- Then: Rewards calculated for 12 hours only, modal shows "Capped at 12h" +- Test: Unit test with excessive elapsed time + +✅ **AC-5: Welcome Back Modal** +- Given: Player offline for 3h 24m, earned Wood +450, Gold +1,200 +- When: App resumes +- Then: Modal shows "Welcome Back! Away: 3h 24m", animated counters, "Watch Ad for 2×" button (after 60s delay) +- Test: Widget test + manual UI test + +✅ **AC-6: 2× Ad Boost** +- Given: Player earned Wood +450 +- When: Player taps "Watch Ad" → watches 30-sec ad → ad completes +- Then: Wood +900 awarded (2× boost), analytics event logged +- Test: Integration test with AdMob test ads + +✅ **AC-7: Time Manipulation Detection** +- Given: Device time set to +10 hours (cheating) +- When: App resumes +- Then: Use server time instead, log cheat attempt, no bonus rewards +- Test: Manual test with device clock change + +✅ **AC-8: Performance** +- Given: 20 buildings + 50 conveyors +- When: Offline production calculated +- Then: Completes in <50ms on Snapdragon 660 +- Test: Performance benchmark + +--- + +## Traceability Mapping + +| Requirement | Epic/Story | Acceptance Criteria | +|-------------|-----------|---------------------| +| **FR-004: Offline Production** | EPIC-04 (this doc) | AC-1 through AC-8 | +| FR-004.1: Tier 1 Calculation | STORY-04.1 | AC-1, AC-3, AC-4 | +| FR-004.2: Tier 2 Calculation | STORY-04.2 | AC-2 | +| FR-004.3: Welcome Back Modal | STORY-04.3 | AC-5 | +| FR-004.4: 2× Ad Boost | STORY-04.4 | AC-6 | +| FR-004.5: Time Validation | STORY-04.5 | AC-7 | +| **NFR-001: Performance** | Cross-cutting | AC-8 | + +--- + +## Risks, Assumptions, Open Questions + +### High Risks + +🔴 **RISK-1: Tier 2 Calculation Complexity (Topological Sort Bugs)** +- **Impact:** HIGH - Wrong calculation = players lose resources = churn +- **Likelihood:** MEDIUM - Complex algorithm with edge cases (bottlenecks, cycles) +- **Mitigation:** + 1. 40+ unit tests covering all edge cases (empty inputs, bottlenecks, cycles) + 2. Offline production log: Show detailed breakdown to players (transparency) + 3. Beta test with 50 players for 1 week before launch +- **Contingency:** Disable Tier 2 offline in v1.0, launch with Tier 1 only (fallback) + +🔴 **RISK-2: Time Manipulation Exploits** +- **Impact:** HIGH - Exploits break economy, require wipe/bans +- **Likelihood:** LOW - Server validation mitigates, but determined cheaters find ways +- **Mitigation:** + 1. Server timestamp validation (5-min drift tolerance) + 2. Cloud Functions validate resource amounts (flag suspicious values) + 3. Manual review queue for flagged accounts +- **Contingency:** Add client-side obfuscation (encrypt lastSeenTime in Hive) + +🟡 **RISK-3: Low Ad Fill Rate (<50%)** +- **Impact:** MEDIUM - Reduced ad revenue (target: 60% watch rate × 90% fill = 54% monetization) +- **Likelihood:** MEDIUM - AdMob fill rates vary by region (90% US, 50% LATAM) +- **Mitigation:** + 1. Use AdMob Mediation (Unity Ads, AppLovin fallbacks) + 2. Preload ad on app start (ready when modal shown) + 3. Fallback message: "Ad not available, but you still get 1× rewards!" +- **Contingency:** Offer IAP alternative: "$0.99 for 2× boost (no ad)" + +### Assumptions + +1. **Offline Cap is Acceptable:** Assumes 12h (Tier 1) / 24h (Tier 2) cap doesn't frustrate players +2. **Ad Watch Rate:** Assumes 60%+ players watch ads (industry benchmark: 50-70%) +3. **No Circular Dependencies:** Assumes players cannot create conveyor cycles (validated in EPIC-03) +4. **Server Time Available:** Assumes Firebase reachable for time validation (offline = trust device time) + +### Open Questions + +❓ **Q1:** Should we show push notification when offline cap is reached? +- **Example:** "Your factory is full! Come back to collect rewards." +- **Decision Needed By:** Sprint 5 planning +- **Owner:** Product Manager +- **Impact:** Medium (could increase retention, but may annoy users) + +❓ **Q2:** Should VIP players get extended offline cap (48h)? +- **Decision Needed By:** Monetization planning (Month 2) +- **Owner:** Business Analyst +- **Impact:** Low (premium feature, post-launch) + +❓ **Q3:** Should we add "Offline Production History" log? +- **Example:** "Last 7 days: Day 1: +5k gold, Day 2: +8k gold..." +- **Decision Needed By:** Sprint 5 (before STORY-04.3) +- **Owner:** UX Designer +- **Impact:** Low (nice-to-have, can add in v1.1) + +--- + +## Test Strategy Summary + +### Unit Tests (Target: 70+ tests) + +**Tier 1 Calculation (30 tests):** +- ✅ 1 hour offline, 1 building (1 Wood/min) = 60 Wood +- ✅ 12 hours offline (at cap) = 720 Wood +- ✅ 48 hours offline (exceeds cap) = 720 Wood (capped) +- ✅ Storage capacity: 100 max, 10 hours = 100 Wood (capped) +- ✅ Zero buildings = empty result +- ✅ Zero elapsed time = empty result +- ✅ Negative elapsed time (clock change) = empty result + +**Tier 2 Calculation (40 tests):** +- ✅ Topological sort: Linear chain (A → B → C) sorted correctly +- ✅ Topological sort: Parallel branches (A → B, A → C) both processed +- ✅ Topological sort: Circular dependency throws exception +- ✅ Bottleneck: Smelter needs 10 Wood, only 5 available = 0 Iron produced +- ✅ Multiple conveyors to same building: Aggregate inputs correctly +- ✅ Complex chain: Lumbermill → Smelter → Workshop (3-tier simulation) + +### Integration Tests (5 tests) + +- ✅ End-to-end: App resumes → calculation → modal shown → collect rewards +- ✅ Ad flow: Watch ad → onRewardEarned → 2× boost applied +- ✅ Time validation: Device time +10h → use server time instead +- ✅ Offline sync: Calculate offline → sync to Firestore → verify cloud save +- ✅ Performance: 20 buildings + 50 conveyors in <50ms + +### Widget Tests (5 tests) + +- ✅ Welcome Back modal renders correctly (title, resources, buttons) +- ✅ Animated counter: Wood 0 → 450 (smooth animation, 2 seconds) +- ✅ "Watch Ad" button appears after 60-second delay +- ✅ Tap "Collect" → modal dismisses, resources added to inventory +- ✅ Ad button disabled if ad not loaded (gray out, show tooltip) + +### Performance Tests (2 tests) + +- ✅ Tier 1: 20 buildings in <10ms (Snapdragon 660) +- ✅ Tier 2: 20 buildings + 50 conveyors in <50ms (Snapdragon 660) + +**Total Tests:** 70 unit + 5 integration + 5 widget + 2 performance = **82 tests** + +--- + +## Implementation Notes + +**Sprint Allocation:** +- **Sprint 5 (Week 7):** STORY-04.1 (Tier 1), STORY-04.3 (Modal UI) +- **Sprint 6 (Week 8):** STORY-04.2 (Tier 2), STORY-04.4 (Ad integration), STORY-04.5 (Time validation) + +**Developer Notes:** +- Preload rewarded video ad on app start (`main.dart` initialization) +- Use Riverpod `@riverpod` for modal state (reactive updates) +- Test with AdMob test ad IDs before switching to production IDs +- Add Firebase Analytics events for funnel analysis (modal shown → ad watched → boost applied) + +**Code Review Checklist:** +- [ ] Topological sort handles circular dependencies (exception thrown) +- [ ] Storage capacity limits applied correctly (no overflow) +- [ ] Time validation uses server timestamp (anti-cheat) +- [ ] Ad failure handled gracefully (show 1× rewards, no error) +- [ ] Performance: <50ms on Snapdragon 660 emulator +- [ ] Tests: 100% coverage for offline calculation logic + +--- + +**Document Status:** ✅ Ready for Development +**Next Steps:** Sprint 5 planning, assign STORY-04.1 and STORY-04.3 +**Questions:** Contact Game Economist (offline cap balance) or Tech Lead (performance concerns) diff --git a/docs/epic-contexts/epic-06-quality-progression-tech-context.md b/docs/epic-contexts/epic-06-quality-progression-tech-context.md new file mode 100644 index 0000000..0b8b80f --- /dev/null +++ b/docs/epic-contexts/epic-06-quality-progression-tech-context.md @@ -0,0 +1,1016 @@ +# Epic Technical Specification: Quality & Progression Systems (EPIC-06) + +Date: 2025-11-23 +Author: Claude (BMAD Game Development Agent) +Epic ID: EPIC-06 +Epic Name: Progression System +Status: Ready for Implementation +Group: B - Operations + +--- + +## Overview + +Epic 06 implements the **Progression & Quality System** that guides players from Tier 1 (basic collection) to Tier 2 (automation) through clear milestones and rewarding celebrations. This epic is critical for **player retention** and **session depth** - clear progression goals increase Day 7 retention by 25-40% (benchmark: Clash of Clans progression systems). + +**Epic Goal:** Create a progression framework that tracks player achievements (building diversity, resource milestones, gold earned), validates Tier 2 unlock requirements, and celebrates milestones with engaging animations and rewards. + +**Business Value:** +- **Retention Driver:** Clear goals → 60%+ players unlock Tier 2 within 2-3 hours (target metric) +- **Engagement Hook:** Achievement tracking creates "completionist" gameplay loop +- **Tutorial Replacement:** Progression UI teaches mechanics organically (place buildings → earn gold → unlock Tier 2) +- **Quality Assurance:** Progression gates ensure players master Tier 1 before complexity increases + +**Technical Complexity:** MEDIUM (5 SP stories) - Straightforward logic with UI/animation focus. + +**Quality Focus:** This epic emphasizes **player experience quality** through: +- Clear visual feedback (progress bars, completion checkmarks) +- Satisfying celebration moments (unlock animations, rewards) +- Intuitive UI (always visible progression tracker) +- Smooth animations (60 FPS during celebrations) + +--- + +## Objectives and Scope + +### In-Scope (Must Have) + +✅ **Tier 2 Unlock Requirements System** +- Define requirements: All 5 building types placed + 10 buildings total + 500 gold earned +- Validate requirements before unlock (prevent cheating) +- Track progress in real-time (update on every building placement, resource collection) +- Persist progress to Firestore (survive app restarts) + +✅ **Progression Tracker UI** +- Always-visible widget: Shows "Tier 2 Progress: 3/5 types, 7/10 buildings, 350/500 gold" +- Progress bars with visual indicators (green = complete, gray = incomplete) +- Animated progress updates (smooth transitions, not instant jumps) +- Tap to expand: Show detailed breakdown of requirements + +✅ **Unlock Celebration Animation** +- Full-screen modal: "🎉 TIER 2 UNLOCKED! 🎉" +- Fireworks/confetti particle effects (2-second animation) +- List unlocked features: "Conveyors, Automation, Advanced Buildings" +- "Continue" button to dismiss (blocks gameplay during celebration) +- Log achievement to Firebase Analytics + +✅ **Achievement System Foundation** +- Define achievement data model: id, title, description, progress, unlocked +- Track achievements: "First Building Placed", "100 Gold Earned", "Tier 2 Unlocked" +- Persistent storage: Hive + Firestore sync +- Achievement notifications: Toast/snackbar when unlocked + +✅ **Quality Assurance Testing** +- 100% test coverage for progression logic (15 unit tests) +- Widget tests for UI components (5 tests) +- Manual testing checklist (celebration animations, edge cases) +- Performance: Progression UI updates at 60 FPS + +### Out-of-Scope (Future Phases) + +❌ **Advanced Features (Post-MVP)** +- Tier 3+ progression systems → Month 2-3 +- Achievement leaderboards (Firebase rankings) → v1.1 +- Daily/weekly challenges → Phase 2 +- Achievement rewards (gems, cosmetics) → Monetization phase + +❌ **Extended Progression** +- Building mastery levels (Level 10 → Gold Star) → Post-launch +- Prestige system (reset for permanent bonuses) → v2.0 +- Story-driven progression (narrative quests) → Out of scope + +### Success Criteria (Testable) + +| Metric | Target | Measurement | +|--------|--------|-------------| +| Tier 2 Unlock Rate | 60%+ players unlock within 2-3 hours | Firebase Analytics funnel | +| Progression UI Visibility | 100% players see tracker within first 60 seconds | Session recordings | +| Celebration Engagement | 90%+ players watch full celebration animation | Event tracking (dismiss vs complete) | +| Achievement Awareness | 50%+ players tap achievement notifications | Click-through rate tracking | +| Code Quality | 100% test coverage for progression logic | Unit test coverage report | + +--- + +## System Architecture Alignment + +### Clean Architecture Layers + +**Domain Layer (lib/domain/)** +``` +lib/domain/entities/ + ├── progression_state.dart # Player progression data: requirements, unlocked tiers + ├── achievement.dart # Achievement: id, title, description, progress, unlocked + └── tier_unlock_requirements.dart # Requirements: building types, count, gold + +lib/domain/use_cases/ + ├── progression/ + │ ├── check_tier_unlock.dart # Validate if requirements met + │ ├── track_progression.dart # Update progress on player actions + │ └── unlock_tier.dart # Execute unlock (log analytics, trigger celebration) + └── achievements/ + ├── check_achievements.dart # Check if any achievements newly unlocked + ├── unlock_achievement.dart # Mark achievement as unlocked + └── get_all_achievements.dart # Fetch achievement list + +lib/domain/repositories/ + ├── progression_repository.dart # Save/load progression state + └── achievement_repository.dart # Save/load achievements +``` + +**Data Layer (lib/data/)** +``` +lib/data/models/ + ├── progression_state_model.dart # JSON serialization, Hive TypeAdapter + └── achievement_model.dart # JSON serialization, Hive TypeAdapter + +lib/data/repositories/ + ├── progression_repository_impl.dart # Firestore + Hive caching + └── achievement_repository_impl.dart # Firestore + Hive caching +``` + +**Presentation Layer (lib/presentation/)** +``` +lib/presentation/screens/ + └── game/ + └── widgets/ + ├── progression_tracker_widget.dart # Always-visible tracker + ├── tier_unlock_celebration_modal.dart # Full-screen celebration + ├── achievement_notification.dart # Toast notification + └── progression_details_sheet.dart # Bottom sheet with details + +lib/presentation/providers/ + ├── progression_provider.dart # @riverpod: Real-time progression state + └── achievement_provider.dart # @riverpod: Achievement list + notifications +``` + +**Game Layer (lib/game/)** +``` +lib/game/components/ + └── celebration_particle_system.dart # Flame particle effects (fireworks, confetti) +``` + +### Technology Stack Alignment + +| Component | Technology | Justification | +|-----------|-----------|---------------| +| Progression Logic | Pure Dart | Simple validation, no external deps | +| UI Components | Flutter Material 3 | Modern design, smooth animations | +| Animations | Flutter AnimatedContainer, Tween | Built-in animations, 60 FPS performance | +| Particle Effects | Flame ParticleSystemComponent | Game engine integration, GPU-accelerated | +| State Management | Riverpod @riverpod | Reactive updates when progress changes | +| Storage | Hive (local) + Firestore (cloud) | Offline-first, syncs when online | +| Analytics | Firebase Analytics | Track unlock funnel, engagement metrics | + +### Integration Points + +**Epic 01 (Core Loop) Dependencies:** +- Building Placement: Increment building count, track building types +- Resource Collection: Increment gold earned + +**Epic 02 (Economy) Dependencies:** +- Gold Tracking: Monitor `PlayerEconomy.totalGoldEarned` +- Building Inventory: Check building type diversity + +**Epic 05 (Mobile UX) Dependencies:** +- Main Game Screen: Embed progression tracker widget +- Modal System: Show celebration on unlock + +**Provides to Epic 03 (Planning):** +- Tier 2 Unlock: Gates conveyor system access +- Tutorial Flow: Progression UI teaches building diversity + +**Provides to Epic 10 (Analytics):** +- Funnel Events: Progression milestones (25% → 50% → 75% → 100%) +- Cohort Analysis: Time-to-Tier-2 distribution + +--- + +## Detailed Design + +### Services and Modules + +#### 1. Progression Tracking System + +**File:** `lib/domain/use_cases/progression/track_progression.dart` + +**Responsibility:** Update progression state when player performs actions. + +**API:** +```dart +class TrackProgressionUseCase { + final ProgressionRepository _repository; + final CheckTierUnlockUseCase _checkTierUnlock; + + TrackProgressionUseCase(this._repository, this._checkTierUnlock); + + /// Call when player places a building + Future onBuildingPlaced( + ProgressionState current, + Building building, + ) async { + final buildingTypes = current.placedBuildingTypes.toSet() + ..add(building.type); + + final updated = current.copyWith( + placedBuildingTypes: buildingTypes.toList(), + totalBuildingsPlaced: current.totalBuildingsPlaced + 1, + ); + + await _repository.saveProgressionState(updated); + + // Check if Tier 2 now unlocked + if (_checkTierUnlock.execute(updated) && !current.tier2Unlocked) { + FirebaseAnalytics.instance.logEvent( + name: 'tier2_requirements_met', + parameters: { + 'buildings_placed': updated.totalBuildingsPlaced, + 'building_types': buildingTypes.length, + 'gold_earned': updated.totalGoldEarned, + }, + ); + } + + return updated; + } + + /// Call when player collects resources (gold earned) + Future onGoldEarned( + ProgressionState current, + int goldAmount, + ) async { + final updated = current.copyWith( + totalGoldEarned: current.totalGoldEarned + goldAmount, + ); + + await _repository.saveProgressionState(updated); + return updated; + } + + /// Call when Tier 2 is unlocked + Future onTier2Unlocked( + ProgressionState current, + ) async { + final updated = current.copyWith( + tier2Unlocked: true, + tier2UnlockTime: DateTime.now(), + ); + + await _repository.saveProgressionState(updated); + + FirebaseAnalytics.instance.logEvent( + name: 'tier2_unlocked', + parameters: { + 'time_to_unlock_minutes': updated.tier2UnlockTime! + .difference(current.accountCreationTime) + .inMinutes, + }, + ); + + return updated; + } +} +``` + +#### 2. Tier Unlock Validation + +**File:** `lib/domain/use_cases/progression/check_tier_unlock.dart` + +**Responsibility:** Check if player meets Tier 2 unlock requirements. + +**API:** +```dart +class CheckTierUnlockUseCase { + static const requiredBuildingTypes = 5; // All 5 types + static const requiredBuildingCount = 10; + static const requiredGoldEarned = 500; + + /// Returns true if all requirements met + bool execute(ProgressionState state) { + return state.placedBuildingTypes.length >= requiredBuildingTypes && + state.totalBuildingsPlaced >= requiredBuildingCount && + state.totalGoldEarned >= requiredGoldEarned; + } + + /// Calculate overall progress (0.0 to 1.0) + double calculateProgress(ProgressionState state) { + final buildingTypeProgress = + state.placedBuildingTypes.length / requiredBuildingTypes; + final buildingCountProgress = + state.totalBuildingsPlaced / requiredBuildingCount; + final goldProgress = + state.totalGoldEarned / requiredGoldEarned; + + // Average of all three requirements + return (buildingTypeProgress + buildingCountProgress + goldProgress) / 3.0; + } + + /// Get detailed progress breakdown for UI + Map getProgressDetails(ProgressionState state) { + return { + 'building_types': ProgressDetail( + label: 'Building Types', + current: state.placedBuildingTypes.length, + required: requiredBuildingTypes, + isComplete: state.placedBuildingTypes.length >= requiredBuildingTypes, + ), + 'building_count': ProgressDetail( + label: 'Total Buildings', + current: state.totalBuildingsPlaced, + required: requiredBuildingCount, + isComplete: state.totalBuildingsPlaced >= requiredBuildingCount, + ), + 'gold_earned': ProgressDetail( + label: 'Gold Earned', + current: state.totalGoldEarned, + required: requiredGoldEarned, + isComplete: state.totalGoldEarned >= requiredGoldEarned, + ), + }; + } +} + +class ProgressDetail { + final String label; + final int current; + final int required; + final bool isComplete; + + ProgressDetail({ + required this.label, + required this.current, + required this.required, + required this.isComplete, + }); + + double get progress => (current / required).clamp(0.0, 1.0); +} +``` + +#### 3. Achievement System + +**File:** `lib/domain/use_cases/achievements/check_achievements.dart` + +**Responsibility:** Check if player actions unlock any achievements. + +**API:** +```dart +class CheckAchievementsUseCase { + final AchievementRepository _repository; + + CheckAchievementsUseCase(this._repository); + + /// Check achievements based on progression state + Future> checkAndUnlock( + ProgressionState progression, + List currentAchievements, + ) async { + final newlyUnlocked = []; + + for (final achievement in currentAchievements.where((a) => !a.unlocked)) { + if (_checkAchievementCondition(achievement, progression)) { + final unlocked = achievement.copyWith( + unlocked: true, + unlockedAt: DateTime.now(), + ); + + await _repository.saveAchievement(unlocked); + newlyUnlocked.add(unlocked); + + FirebaseAnalytics.instance.logEvent( + name: 'achievement_unlocked', + parameters: {'achievement_id': achievement.id}, + ); + } + } + + return newlyUnlocked; + } + + bool _checkAchievementCondition( + Achievement achievement, + ProgressionState progression, + ) { + switch (achievement.id) { + case 'first_building': + return progression.totalBuildingsPlaced >= 1; + case 'five_buildings': + return progression.totalBuildingsPlaced >= 5; + case 'ten_buildings': + return progression.totalBuildingsPlaced >= 10; + case 'all_building_types': + return progression.placedBuildingTypes.length >= 5; + case '100_gold': + return progression.totalGoldEarned >= 100; + case '500_gold': + return progression.totalGoldEarned >= 500; + case 'tier2_unlocked': + return progression.tier2Unlocked; + default: + return false; + } + } +} +``` + +**Predefined Achievements:** +```dart +class AchievementDefinitions { + static final List tier1Achievements = [ + Achievement( + id: 'first_building', + title: 'First Steps', + description: 'Place your first building', + icon: '🏠', + unlocked: false, + ), + Achievement( + id: 'five_buildings', + title: 'Small Factory', + description: 'Place 5 buildings', + icon: '🏭', + unlocked: false, + ), + Achievement( + id: 'ten_buildings', + title: 'Growing Empire', + description: 'Place 10 buildings', + icon: '🌆', + unlocked: false, + ), + Achievement( + id: 'all_building_types', + title: 'Master Builder', + description: 'Place all 5 building types', + icon: '⚙️', + unlocked: false, + ), + Achievement( + id: '100_gold', + title: 'First Fortune', + description: 'Earn 100 gold', + icon: '💰', + unlocked: false, + ), + Achievement( + id: '500_gold', + title: 'Wealthy Trader', + description: 'Earn 500 gold', + icon: '💎', + unlocked: false, + ), + Achievement( + id: 'tier2_unlocked', + title: 'Automation Master', + description: 'Unlock Tier 2 (Conveyors)', + icon: '🎉', + unlocked: false, + ), + ]; +} +``` + +--- + +### Data Models and Contracts + +#### ProgressionState Entity + +```dart +import 'package:equatable/equatable.dart'; + +class ProgressionState extends Equatable { + final List placedBuildingTypes; + final int totalBuildingsPlaced; + final int totalGoldEarned; + final bool tier2Unlocked; + final DateTime? tier2UnlockTime; + final DateTime accountCreationTime; + + const ProgressionState({ + required this.placedBuildingTypes, + required this.totalBuildingsPlaced, + required this.totalGoldEarned, + required this.tier2Unlocked, + this.tier2UnlockTime, + required this.accountCreationTime, + }); + + /// Initial state for new players + factory ProgressionState.initial() { + return ProgressionState( + placedBuildingTypes: [], + totalBuildingsPlaced: 0, + totalGoldEarned: 0, + tier2Unlocked: false, + tier2UnlockTime: null, + accountCreationTime: DateTime.now(), + ); + } + + @override + List get props => [ + placedBuildingTypes, + totalBuildingsPlaced, + totalGoldEarned, + tier2Unlocked, + tier2UnlockTime, + accountCreationTime, + ]; + + ProgressionState copyWith({ + List? placedBuildingTypes, + int? totalBuildingsPlaced, + int? totalGoldEarned, + bool? tier2Unlocked, + DateTime? tier2UnlockTime, + }) { + return ProgressionState( + placedBuildingTypes: placedBuildingTypes ?? this.placedBuildingTypes, + totalBuildingsPlaced: totalBuildingsPlaced ?? this.totalBuildingsPlaced, + totalGoldEarned: totalGoldEarned ?? this.totalGoldEarned, + tier2Unlocked: tier2Unlocked ?? this.tier2Unlocked, + tier2UnlockTime: tier2UnlockTime ?? this.tier2UnlockTime, + accountCreationTime: accountCreationTime, + ); + } +} +``` + +#### Achievement Entity + +```dart +class Achievement extends Equatable { + final String id; + final String title; + final String description; + final String icon; // Emoji or asset path + final bool unlocked; + final DateTime? unlockedAt; + + const Achievement({ + required this.id, + required this.title, + required this.description, + required this.icon, + required this.unlocked, + this.unlockedAt, + }); + + @override + List get props => [id, title, description, icon, unlocked, unlockedAt]; + + Achievement copyWith({ + bool? unlocked, + DateTime? unlockedAt, + }) { + return Achievement( + id: id, + title: title, + description: description, + icon: icon, + unlocked: unlocked ?? this.unlocked, + unlockedAt: unlockedAt ?? this.unlockedAt, + ); + } +} +``` + +--- + +### APIs and Interfaces + +#### ProgressionRepository + +```dart +abstract class ProgressionRepository { + /// Get current progression state (local + cloud merge) + Future getProgressionState(); + + /// Save progression state to local cache and Firestore + Future saveProgressionState(ProgressionState state); + + /// Reset progression (for testing or prestige systems) + Future resetProgression(); +} +``` + +#### AchievementRepository + +```dart +abstract class AchievementRepository { + /// Get all achievements (local + cloud merge) + Future> getAllAchievements(); + + /// Save single achievement + Future saveAchievement(Achievement achievement); + + /// Get unlocked achievements count + Future getUnlockedCount(); +} +``` + +--- + +### Workflows and Sequencing + +#### Workflow 1: Player Progresses Toward Tier 2 + +``` +┌──────────┐ ┌────────────────┐ ┌──────────────┐ ┌────────────┐ +│ Player │ │ Presentation │ │ Domain │ │ Data │ +└────┬─────┘ └────────┬───────┘ └──────┬───────┘ └─────┬──────┘ + │ │ │ │ + │ 1. Place Building │ │ │ + ├───────────────────>│ │ │ + │ │ 2. TrackProgression│ │ + │ │ (building+1) │ │ + │ ├───────────────────>│ │ + │ │ │ 3. SaveState │ + │ │ ├──────────────────>│ + │ │ │ │ + │ │ 4. CheckTierUnlock │ │ + │ │<───────────────────┤ │ + │ │ │ │ + │ │ Result: 3/5 types, │ │ + │ │ 7/10 bldgs,│ │ + │ │ 350/500g │ │ + │ │ │ │ + │ 5. UI Update: │ │ │ + │ Progress bar │ │ │ + │ fills +10% │ │ │ + │<───────────────────┤ │ │ + │ │ │ │ + │ [Player continues │ │ │ + │ playing...] │ │ │ + │ │ │ │ + │ 6. Place 10th │ │ │ + │ Building (last │ │ │ + │ requirement) │ │ │ + ├───────────────────>│ │ │ + │ │ 7. TrackProgression│ │ + │ ├───────────────────>│ │ + │ │ │ │ + │ │ 8. CheckTierUnlock │ │ + │ │ = TRUE ✅ │ │ + │ │<───────────────────┤ │ + │ │ │ │ + │ │ 9. UnlockTier2 │ │ + │ ├───────────────────>│ │ + │ │ │ 10. SaveState │ + │ │ │ (unlocked) │ + │ │ ├──────────────────>│ + │ │ │ │ + │ │ 11. Show │ │ + │ │ Celebration │ │ + │ │ Modal │ │ + │ │ │ │ + │ 12. 🎉 TIER 2 │ │ │ + │ UNLOCKED! 🎉 │ │ │ + │ [Fireworks] │ │ │ + │<───────────────────┤ │ │ + │ │ │ │ +``` + +#### Workflow 2: Tier 2 Unlock Celebration + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Tier 2 Unlock Celebration Modal │ +│ │ +│ [Fullscreen overlay with semi-transparent background] │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 🎉 TIER 2 UNLOCKED! 🎉 │ │ +│ │ │ │ +│ │ [Fireworks particle animation] │ │ +│ │ [Confetti falling from top] │ │ +│ │ │ │ +│ │ ✨ Congratulations! You've mastered the basics! ✨ │ │ +│ │ │ │ +│ │ New Features Unlocked: │ │ +│ │ ✓ Conveyors (Automated resource transport) │ │ +│ │ ✓ Advanced Buildings (Smelter, Workshop) │ │ +│ │ ✓ Complex Production Chains │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────┐ │ │ +│ │ │ Continue to Tier 2 │ │ │ +│ │ └──────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────┘ + +Animation Timeline: +0.0s: Modal fades in (300ms) +0.3s: Title text appears with scale animation +0.6s: Fireworks start (particle system, 2-second duration) +0.8s: Confetti falls (particle system, 2-second duration) +1.0s: Feature list animates in (staggered, 100ms each item) +2.0s: "Continue" button appears with pulse animation +``` + +--- + +## Non-Functional Requirements + +### Performance + +| Metric | Requirement | Test Method | +|--------|-------------|-------------| +| Progression UI Updates | <16ms (60 FPS) | Flutter DevTools performance overlay | +| Celebration Animation | 60 FPS sustained | FPS counter during 2-second animation | +| State Save Time | <50ms to Hive | Performance test with Stopwatch | +| Achievement Check | <10ms per action | Unit test benchmark | +| Modal Load Time | <300ms from trigger to visible | Manual timing test | + +**Optimization Strategies:** +1. **Debouncing:** Update progression UI max once per 500ms (prevent spam on rapid building placement) +2. **Memoization:** Cache `CheckTierUnlockUseCase.getProgressDetails()` result until state changes +3. **Lazy Loading:** Load achievement list only when achievements screen opened (not on app start) +4. **Particle Pooling:** Reuse particle components in celebration animation + +### Security + +**Anti-Cheat Measures:** +```dart +// Validate progression state on Firestore write +// Cloud Function: validate_progression.js +exports.validateProgression = functions.firestore + .document('users/{userId}/progression') + .onWrite((change, context) => { + const after = change.after.data(); + + // Check if gold earned matches building count (rough validation) + const expectedMinGold = after.totalBuildingsPlaced * 10; // ~10 gold per building + if (after.totalGoldEarned < expectedMinGold * 0.5) { + // Suspicious: Too little gold for this many buildings + console.error(`Suspicious progression: ${context.params.userId}`); + // Flag for manual review, don't block (could be false positive) + } + + // Check if building types is valid (max 5 types in Tier 1) + if (after.placedBuildingTypes.length > 5) { + // Invalid: More than 5 building types + console.error(`Invalid building types: ${context.params.userId}`); + return change.after.ref.update({ placedBuildingTypes: after.placedBuildingTypes.slice(0, 5) }); + } + + return null; + }); +``` + +### Reliability/Availability + +**Error Handling:** +```dart +try { + final updated = await _trackProgression.onBuildingPlaced(current, building); + _progressionProvider.update(updated); +} catch (e, stackTrace) { + FirebaseCrashlytics.instance.recordError(e, stackTrace); + // Fallback: Continue without updating progression (don't block gameplay) + showSnackBar('Progress tracking temporarily unavailable'); +} +``` + +**Offline Support:** +- All progression state cached in Hive (works offline) +- Sync to Firestore when connection restored +- Conflict resolution: Latest timestamp wins (last-write-wins) + +### Observability + +**Firebase Analytics Events:** +```dart +// Track progression milestones +FirebaseAnalytics.instance.logEvent( + name: 'progression_milestone', + parameters: { + 'milestone': '25_percent', // 25%, 50%, 75%, 100% + 'time_to_milestone_minutes': DateTime.now().difference(accountCreation).inMinutes, + }, +); + +// Track Tier 2 unlock +FirebaseAnalytics.instance.logEvent( + name: 'tier2_unlocked', + parameters: { + 'time_to_unlock_minutes': timeToUnlock, + 'buildings_placed': state.totalBuildingsPlaced, + 'gold_earned': state.totalGoldEarned, + }, +); + +// Track achievement unlocks +FirebaseAnalytics.instance.logEvent( + name: 'achievement_unlocked', + parameters: { + 'achievement_id': achievement.id, + 'unlock_order': unlockedCount + 1, // 1st, 2nd, 3rd achievement unlocked + }, +); +``` + +**Funnel Analysis:** +- Track % of players at each progression milestone (0% → 25% → 50% → 75% → 100%) +- Identify drop-off points (e.g., 40% quit at 50% progress) +- Cohort analysis: Time-to-Tier-2 distribution (median, p50, p90, p99) + +--- + +## Dependencies and Integrations + +### Upstream Dependencies + +| Epic/Story | Required Artifacts | +|------------|-------------------| +| **EPIC-01: Core Loop** | `Building` entity, building placement logic | +| **EPIC-02: Economy** | `PlayerEconomy.totalGoldEarned`, resource tracking | +| **EPIC-05: Mobile UX** | Main game screen, modal system | + +### Downstream Dependents + +| Epic | How It Uses EPIC-06 | +|------|---------------------| +| **EPIC-03: Planning** | Tier 2 unlock gates conveyor access | +| **EPIC-07: Tutorial** | Progression UI teaches mechanics organically | +| **EPIC-10: Analytics** | Progression milestones for funnel analysis | + +--- + +## Acceptance Criteria (Authoritative) + +✅ **AC-1: Tier 2 Requirements Validation** +- Given: Player has 5 building types, 10 buildings, 500 gold +- When: `CheckTierUnlockUseCase.execute()` called +- Then: Returns true (all requirements met) +- Test: Unit test with mocked progression state + +✅ **AC-2: Progression Tracker UI** +- Given: Player has 3 building types, 7 buildings, 350 gold +- When: Progression tracker widget rendered +- Then: Shows "Tier 2 Progress: 3/5 types, 7/10 buildings, 350/500 gold" +- Test: Widget test with mocked state + +✅ **AC-3: Real-Time Progress Updates** +- Given: Player at 7/10 buildings +- When: Player places 8th building +- Then: UI updates to "8/10 buildings" within 500ms (smooth animation) +- Test: Integration test + manual test + +✅ **AC-4: Unlock Celebration Animation** +- Given: Player meets all Tier 2 requirements +- When: 10th building placed (final requirement) +- Then: Full-screen modal appears with fireworks, confetti, feature list, "Continue" button +- Test: Widget test + manual test (verify 60 FPS) + +✅ **AC-5: Achievement Unlocks** +- Given: Player earns 100 gold (achievement: "First Fortune") +- When: `CheckAchievementsUseCase` runs +- Then: Achievement marked as unlocked, notification shown, Firebase event logged +- Test: Unit test + widget test for notification + +✅ **AC-6: Persistence** +- Given: Progression at 8/10 buildings +- When: Player closes app → reopens app +- Then: Progression state restored (8/10 buildings shown) +- Test: Integration test with Hive + +✅ **AC-7: Performance** +- Given: Progression UI rendered +- When: Player rapidly places 10 buildings (1 per second) +- Then: UI updates at 60 FPS (no frame drops) +- Test: Performance test with Flutter DevTools + +--- + +## Traceability Mapping + +| Requirement | Epic/Story | Acceptance Criteria | +|-------------|-----------|---------------------| +| **FR-006: Progression System** | EPIC-06 (this doc) | AC-1 through AC-7 | +| FR-006.1: Tier 2 Requirements | STORY-06.1 | AC-1, AC-2 | +| FR-006.2: Unlock Celebration | STORY-06.2 | AC-4 | +| FR-006.3: Achievement System | STORY-06.3 | AC-5 | +| FR-006.4: Progression UI | STORY-06.4 | AC-2, AC-3 | +| **NFR-001: Performance** | Cross-cutting | AC-7 | +| **NFR-003: Quality** | Cross-cutting | All ACs (test coverage) | + +--- + +## Risks, Assumptions, Open Questions + +### Risks + +🟡 **RISK-1: Requirements Too Easy/Hard** +- **Impact:** MEDIUM - If too easy: Players unlock Tier 2 in <1 hour (bored). If too hard: <40% unlock within 3 hours (frustrated) +- **Likelihood:** MEDIUM - Balance is subjective, needs playtesting +- **Mitigation:** + 1. A/B test different requirements (500g vs 750g, 10 buildings vs 15) + 2. Monitor Day 1 analytics: Unlock rate at 1h, 2h, 3h marks + 3. Adjust requirements in hotfix if needed (Firestore Remote Config) +- **Contingency:** Reduce requirements to 8 buildings, 400 gold if <50% unlock rate + +🟡 **RISK-2: Celebration Animation Lag (Low-End Devices)** +- **Impact:** MEDIUM - Frame drops during celebration ruin "magical moment" +- **Likelihood:** LOW - Flame particle systems are GPU-accelerated +- **Mitigation:** + 1. Test on Snapdragon 660 (target device) + 2. Use object pooling for particles (reuse components) + 3. Fallback: Reduce particle count on low-end devices (detect via device specs) +- **Contingency:** Simplify animation (static confetti PNG instead of particles) + +### Assumptions + +1. **Linear Progression:** Assumes all players follow Tier 1 → Tier 2 path (no skipping) +2. **Single Unlock Celebration:** Assumes celebration shown once per player (not repeatable) +3. **No Negative Progress:** Assumes players cannot lose progress (buildings can't be deleted in MVP) + +### Open Questions + +❓ **Q1:** Should we show progression tracker after first building placed (tutorial)? +- **Decision Needed By:** Sprint 5 (before STORY-06.4) +- **Owner:** UX Designer +- **Impact:** Low (tutorial flow decision) + +❓ **Q2:** Should achievements grant rewards (gems, cosmetics)? +- **Decision Needed By:** Monetization planning (Month 2) +- **Owner:** Game Designer +- **Impact:** Medium (affects monetization balance) + +❓ **Q3:** Should Tier 2 unlock be reversible (for testing)? +- **Decision Needed By:** Sprint 5 (before STORY-06.1) +- **Owner:** Tech Lead +- **Impact:** Low (dev tooling decision) + +--- + +## Test Strategy Summary + +### Unit Tests (Target: 15 tests) + +**Progression Logic (10 tests):** +- ✅ Check unlock: 5 types, 10 buildings, 500 gold = TRUE +- ✅ Check unlock: 4 types, 10 buildings, 500 gold = FALSE +- ✅ Calculate progress: 3 types, 7 buildings, 350 gold = 65% +- ✅ Track building placed: 7 → 8 buildings +- ✅ Track gold earned: 350 → 450 gold +- ✅ Unlock Tier 2: Set `tier2Unlocked = true`, log analytics + +**Achievement Logic (5 tests):** +- ✅ Check achievement: 100 gold earned → unlock "First Fortune" +- ✅ Check achievement: 10 buildings → unlock "Growing Empire" +- ✅ Multiple achievements unlock simultaneously +- ✅ Already unlocked achievement not re-triggered + +### Widget Tests (5 tests) + +- ✅ Progression tracker renders: "3/5 types, 7/10 buildings, 350/500 gold" +- ✅ Progress bars fill correctly (60% gold = 60% filled) +- ✅ Checkmarks shown for completed requirements (green ✓) +- ✅ Celebration modal renders: Title, fireworks, feature list, button +- ✅ Achievement notification toast appears + +### Integration Tests (3 tests) + +- ✅ End-to-end: Place buildings → progress updates → Tier 2 unlocked → celebration shown +- ✅ Persistence: Save progression → close app → reopen → state restored +- ✅ Offline sync: Update offline → go online → syncs to Firestore + +### Manual Tests (Checklist) + +- [ ] Celebration animation runs at 60 FPS (visual inspection) +- [ ] Fireworks/confetti look visually appealing (aesthetic check) +- [ ] Progress tracker always visible on game screen (UI placement) +- [ ] Achievement notifications don't block critical gameplay +- [ ] Tier 2 unlock feels "rewarding" (playtest with 5 users) + +**Total Tests:** 15 unit + 5 widget + 3 integration + 5 manual = **28 tests** + +--- + +## Implementation Notes + +**Sprint Allocation:** +- **Sprint 5 (Week 7):** STORY-06.1 (Requirements), STORY-06.4 (Tracker UI) +- **Sprint 6 (Week 8):** STORY-06.2 (Celebration), STORY-06.3 (Achievements) + +**Developer Notes:** +- Use `AnimatedContainer` for smooth progress bar fills (60 FPS) +- Flame `ParticleSystemComponent` for fireworks (GPU-accelerated) +- Riverpod `@riverpod` with `.autoDispose` for progression state (memory management) +- Firebase Analytics events must match naming convention: `tier2_*`, `achievement_*` + +**Code Review Checklist:** +- [ ] Progression requirements validated both client-side and server-side (anti-cheat) +- [ ] Celebration animation tested on Snapdragon 660 (60 FPS confirmed) +- [ ] Achievement conditions cover all edge cases (0 buildings, negative gold) +- [ ] UI updates debounced (max 1 update per 500ms, prevent spam) +- [ ] Tests: 100% coverage for progression logic, achievement checks + +--- + +**Document Status:** ✅ Ready for Development +**Next Steps:** Sprint 5 planning, assign STORY-06.1 and STORY-06.4 +**Questions:** Contact Game Designer (requirement balance) or UX Designer (celebration animation specs)