diff --git a/docs/superpowers/plans/2026-06-28-escape-from-jail.md b/docs/superpowers/plans/2026-06-28-escape-from-jail.md new file mode 100644 index 0000000..7037b38 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-escape-from-jail.md @@ -0,0 +1,476 @@ +# Escape from Jail Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a Bust Out mechanic to the jail page — spend 1000 cash, roll against a dexterity+speed formula, succeed to escape or fail to extend your sentence by 1.5×. + +**Architecture:** A new `JailService` holds all bust-out logic (mirrors the existing `HealthService::revive()` pattern). A new `JailController` handles the HTTP action. The existing jail view gains a Bust Out card with a CSRF form. No new database migrations. + +**Tech Stack:** Laravel 11, PHP 8.2, Blade templating, Eloquent ORM, Laravel Sail (Docker). All `artisan` commands must be run via `./vendor/bin/sail artisan`. + +## Global Constraints + +- Project runs in **Laravel Sail** — all `php artisan` commands must use `./vendor/bin/sail artisan` +- Dark crime theme: Tailwind classes `bg-syndicate-*`, `text-crime-*`, `bg-crime-*`, `text-yellow-*` — match the existing jail page colour palette (yellow for jail, not purple/green) +- Services are instantiated with `new` in controllers — no DI container injection (see existing `CharacterController`, `GymController`) +- No PHPUnit in this project — manual verification via `./vendor/bin/sail artisan tinker` and browser testing only +- Flash messages use `session()->flash('success', ...)` / `session()->flash('error', ...)`; displayed via existing layout partials +- CSRF required on all POST forms (`@csrf`) +- Routes file: `routes/game.php` + +--- + +## File Map + +| Action | File | +|--------|------| +| Modify | `config/game.php` — add two keys to `jail` block | +| Create | `app/Services/Game/JailService.php` | +| Create | `app/Http/Controllers/Game/JailController.php` | +| Modify | `routes/game.php` — add bust-out route and pass new vars to jail view | +| Modify | `resources/views/game/jail.blade.php` — add Bust Out card | + +--- + +### Task 1: Config + JailService + +**Files:** +- Modify: `config/game.php` (lines 81–86) +- Create: `app/Services/Game/JailService.php` + +**Interfaces:** +- Produces: + - `JailService::bustOut(Character $character): array{success: bool, message: string}` + - `JailService::sendToJail(Character $character, int $seconds): void` + - `JailService::releaseFromJail(Character $character): void` + - `JailService::calculateBustOutChance(Character $character): float` (returns 0.0–1.0) + +- [ ] **Step 1: Add two config keys to the `jail` block in `config/game.php`** + +Open `config/game.php`. The existing `jail` block is at lines 81–86: + +```php +// Jail system configuration +'jail' => [ + 'sentence_length' => 3600, + 'breakout_chance' => 0.05, + 'breakout_cost' => 1000, + 'sentence_multiplier' => 1.5, + 'bust_out_stat_multiplier' => 0.003, // per combined dex+speed point + 'bust_out_max_chance' => 0.80, // cap at 80% +], +``` + +Replace the existing block (4 keys) with the 6-key version above. + +- [ ] **Step 2: Verify config is readable** + +```bash +./vendor/bin/sail artisan tinker +``` + +In the tinker prompt: +```php +config('game.jail.bust_out_stat_multiplier'); // expect 0.003 +config('game.jail.bust_out_max_chance'); // expect 0.8 +exit +``` + +- [ ] **Step 3: Create `app/Services/Game/JailService.php`** + +```php +dexterity + $character->speed) * $multiplier); + } + + /** + * Attempt to bust out of jail. + * + * Deducts breakout_cost cash regardless of outcome. + * On success: releases character immediately. + * On failure: multiplies remaining sentence by sentence_multiplier. + * + * @return array{success: bool, message: string} + */ + public function bustOut(Character $character): array + { + $cost = config('game.jail.breakout_cost', 1000); + + if ($character->money < $cost) { + return [ + 'success' => false, + 'message' => 'You need $' . number_format($cost) . ' to attempt a bust-out.', + ]; + } + + $character->money -= $cost; + $successRate = $this->calculateBustOutChance($character); + $roll = random_int(0, 9999) / 10000; + + if ($roll <= $successRate) { + $this->releaseFromJail($character); + $character->save(); + + return [ + 'success' => true, + 'message' => "You escaped! You're free.", + ]; + } + + // Failure: extend sentence + $multiplier = config('game.jail.sentence_multiplier', 1.5); + $remaining = max(0, $character->in_jail_until->diffInSeconds(now())); + $extended = (int) ceil($remaining * $multiplier); + + $character->in_jail_until = now()->addSeconds($extended); + $character->save(); + + $newReleaseTime = $character->in_jail_until->diffForHumans(); + + return [ + 'success' => false, + 'message' => "Bust failed — your sentence was extended to {$newReleaseTime}.", + ]; + } + + /** + * Send a character to jail for the given number of seconds. + */ + public function sendToJail(Character $character, int $seconds): void + { + $character->status = 'in_jail'; + $character->in_jail_until = now()->addSeconds($seconds); + $character->save(); + } + + /** + * Release a character from jail immediately. + */ + public function releaseFromJail(Character $character): void + { + $character->status = 'active'; + $character->in_jail_until = null; + } +} +``` + +- [ ] **Step 4: Smoke-test the service in tinker** + +```bash +./vendor/bin/sail artisan tinker +``` + +```php +use App\Services\Game\JailService; +use App\Models\Character; + +$svc = new JailService(); + +// Fake a character with known stats +$char = new Character(['dexterity' => 5, 'speed' => 5]); +$chance = $svc->calculateBustOutChance($char); +// expect: 0.05 + (5+5)*0.003 = 0.08 +echo $chance; // 0.08 + +$char2 = new Character(['dexterity' => 50, 'speed' => 50]); +echo $svc->calculateBustOutChance($char2); // 0.35 + +$char3 = new Character(['dexterity' => 100, 'speed' => 100]); +echo $svc->calculateBustOutChance($char3); // 0.8 (capped) +exit +``` + +- [ ] **Step 5: Commit** + +```bash +git add config/game.php app/Services/Game/JailService.php +git commit -m "feat: add JailService and bust-out config keys" +``` + +--- + +### Task 2: JailController + Route + +**Files:** +- Create: `app/Http/Controllers/Game/JailController.php` +- Modify: `routes/game.php` + +**Interfaces:** +- Consumes: + - `JailService::bustOut(Character): array{success: bool, message: string}` (Task 1) + - `JailService::calculateBustOutChance(Character): float` (Task 1) +- Produces: + - Route `game.jail.bust-out` (POST `/game/jail/bust-out`) + - Updated jail view route passes `$bustOutChance` and `$bustOutCost` to the view + +- [ ] **Step 1: Create `app/Http/Controllers/Game/JailController.php`** + +```php +jailService = new JailService(); + } + + /** + * Attempt to bust out of jail. + */ + public function bustOut(Request $request): RedirectResponse + { + $character = $request->user()->character; + + if (!$character || !$character->isInJail()) { + return redirect()->route('game.dashboard'); + } + + $result = $this->jailService->bustOut($character); + + if ($result['success']) { + return redirect()->route('game.dashboard') + ->with('success', $result['message']); + } + + return redirect()->route('game.jail') + ->with('error', $result['message']); + } +} +``` + +- [ ] **Step 2: Add the bust-out route and update the jail GET route in `routes/game.php`** + +The existing jail route group starts at line 81: + +```php +// Jail route (for when character is in jail) +Route::middleware(['auth'])->group(function () { + Route::get('/game/jail', function () { + $character = auth()->user()->character; + if (!$character || !$character->isInJail()) { + return redirect()->route('game.dashboard'); + } + $releaseTime = $character->in_jail_until ? $character->in_jail_until->diffForHumans() : 'unknown'; + $bustOutChance = (int) round((new \App\Services\Game\JailService())->calculateBustOutChance($character) * 100); + $bustOutCost = config('game.jail.breakout_cost', 1000); + return view('game.jail', compact('character', 'releaseTime', 'bustOutChance', 'bustOutCost')); + })->name('game.jail'); + + Route::post('/game/jail/bust-out', [\App\Http\Controllers\Game\JailController::class, 'bustOut']) + ->name('game.jail.bust-out'); +}); +``` + +Replace the existing jail group (lines 81–91 of `routes/game.php`) with the block above. + +- [ ] **Step 3: Verify routes are registered** + +```bash +./vendor/bin/sail artisan route:list --name=game.jail +``` + +Expected output includes two rows: +``` +GET /game/jail game.jail +POST /game/jail/bust-out game.jail.bust-out +``` + +- [ ] **Step 4: Commit** + +```bash +git add app/Http/Controllers/Game/JailController.php routes/game.php +git commit -m "feat: add JailController and bust-out route" +``` + +--- + +### Task 3: Jail View — Bust Out Card + +**Files:** +- Modify: `resources/views/game/jail.blade.php` + +**Interfaces:** +- Consumes (variables passed from the route closure, Task 2): + - `$character` — Character model (has `->money`) + - `$releaseTime` — string, e.g. "in 45 minutes" + - `$bustOutChance` — int (percentage, e.g. 8 for 8%) + - `$bustOutCost` — int (e.g. 1000) + - `$bustOutCost` formatted with `number_format()` in the view + +- [ ] **Step 1: Replace `resources/views/game/jail.blade.php` with the updated version** + +```blade +@extends('layouts.game') + +@section('title', 'Jail') + +@section('game-content') +
Your character has been arrested and is currently serving time.
+ + {{-- Status card --}} +Spend cash for a chance to escape early. Failure extends your sentence.
+ +Or wait for your sentence to complete automatically.
+ + + + Back to Dashboard + +