Files
thecrimesynticate/docs/superpowers/plans/2026-06-28-escape-from-jail.md

476 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<?php
namespace App\Services\Game;
use App\Models\Character;
class JailService
{
/**
* Calculate bust-out success chance for a character (0.0 to 1.0).
*/
public function calculateBustOutChance(Character $character): float
{
$base = config('game.jail.breakout_chance', 0.05);
$multiplier = config('game.jail.bust_out_stat_multiplier', 0.003);
$cap = config('game.jail.bust_out_max_chance', 0.80);
return min($cap, $base + ($character->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
<?php
namespace App\Http\Controllers\Game;
use App\Http\Controllers\Controller;
use App\Services\Game\JailService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class JailController extends Controller
{
protected JailService $jailService;
public function __construct()
{
$this->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')
<div class="max-w-2xl mx-auto space-y-4">
{{-- Flash messages --}}
@if(session('success'))
<div class="bg-green-900/30 border border-green-700 text-green-300 rounded-lg px-4 py-3 text-sm">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="bg-red-900/30 border border-red-700 text-red-300 rounded-lg px-4 py-3 text-sm">
{{ session('error') }}
</div>
@endif
<div class="bg-syndicate-800 border border-syndicate-700 rounded-lg">
<div class="p-8 text-center">
<div class="mb-6">
<span class="text-6xl">🚔</span>
</div>
<h1 class="font-display text-2xl font-bold text-gray-100 mb-4">You're in Jail</h1>
<p class="text-gray-400 mb-6 text-sm">Your character has been arrested and is currently serving time.</p>
{{-- Status card --}}
<div class="bg-yellow-900/20 border border-yellow-800 rounded-lg p-6 mb-6 text-left">
<h3 class="font-display text-base font-semibold text-yellow-300 mb-3">Your Status</h3>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-400">Status</span>
<span class="text-yellow-400 font-medium">In Jail</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Release Time</span>
<span class="text-gray-200 font-medium">{{ $releaseTime }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Cash on Hand</span>
<span class="text-gray-200 font-medium">${{ number_format($character->money) }}</span>
</div>
</div>
</div>
{{-- Bust Out card --}}
<div class="bg-syndicate-700 border border-syndicate-600 rounded-lg p-6 mb-6 text-left">
<h3 class="font-display text-base font-semibold text-gray-100 mb-1">Bust Out</h3>
<p class="text-gray-400 text-xs mb-4">Spend cash for a chance to escape early. Failure extends your sentence.</p>
<div class="space-y-2 text-sm mb-4">
<div class="flex justify-between">
<span class="text-gray-400">Success Chance</span>
<span class="text-yellow-400 font-medium">{{ $bustOutChance }}%</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Cost</span>
<span class="text-gray-200 font-medium">${{ number_format($bustOutCost) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">On Failure</span>
<span class="text-red-400 font-medium">Sentence × 1.5</span>
</div>
</div>
@if($character->money >= $bustOutCost)
<form action="{{ route('game.jail.bust-out') }}" method="POST">
@csrf
<button type="submit"
class="w-full flex items-center justify-center px-6 py-3 bg-yellow-700 hover:bg-yellow-600 text-white font-medium rounded-lg transition">
<span class="mr-2">🔓</span>
Bust Out ({{ $bustOutChance }}% chance)
</button>
</form>
@else
<button type="button" disabled
title="You need ${{ number_format($bustOutCost) }} to attempt a bust-out"
class="w-full flex items-center justify-center px-6 py-3 bg-syndicate-600 text-gray-500 font-medium rounded-lg cursor-not-allowed">
<span class="mr-2">🔒</span>
Not Enough Cash
</button>
@endif
</div>
<div class="space-y-4">
<p class="text-sm text-gray-600">Or wait for your sentence to complete automatically.</p>
<a href="{{ route('game.dashboard') }}"
class="inline-flex items-center text-gray-500 hover:text-gray-300 transition text-sm">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Back to Dashboard
</a>
</div>
</div>
</div>
</div>
@endsection
```
- [ ] **Step 2: Verify the jail page renders**
In a browser (or via `curl`), navigate to `/game/jail` while logged in with a jailed character. Confirm:
- Status card shows release time and cash on hand
- Bust Out card shows correct % and cost
- If character has ≥ $1,000: button is active (yellow), form submits to `game.jail.bust-out`
- If character has < $1,000: button is disabled, grey, shows tooltip
To temporarily put a character in jail for testing:
```bash
./vendor/bin/sail artisan tinker
```
```php
use App\Models\Character;
$c = Character::first();
$c->status = 'in_jail';
$c->in_jail_until = now()->addHour();
$c->save();
exit
```
Then visit `/game/jail`.
- [ ] **Step 3: Test the full bust-out flow**
With the jailed character, click "Bust Out":
- **Success path:** You are redirected to `/game` with a green "You escaped! You're free." flash.
- **Failure path:** You stay on `/game/jail` with a red "Bust failed — your sentence was extended to..." flash and the release time has increased.
Verify cash was deducted in both cases:
```bash
./vendor/bin/sail artisan tinker
```
```php
use App\Models\Character;
Character::first()->money; // should be 1000 less than before
exit
```
- [ ] **Step 4: Commit**
```bash
git add resources/views/game/jail.blade.php
git commit -m "feat: add bust-out card to jail page"
```