- Add database migrations for market_listings, missions and character cosmetics - Create controllers for marketplace, missions, presence and character customization - Implement admin world dashboard with live platform metrics - Add feature tests for world functionality - Include phase 2 design documents and gap analysis
181 lines
6.1 KiB
PHP
Executable File
181 lines
6.1 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Web;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Character;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\MarketListing;
|
|
use App\Models\Mission;
|
|
use App\Models\Transaction;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Marketplace between citizens and mission journal endpoints.
|
|
*/
|
|
class CityMarketController extends Controller
|
|
{
|
|
/**
|
|
* Active listings in the caller's city plus own listings.
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$me = Character::ensureForUser($request->user());
|
|
|
|
$listings = MarketListing::query()
|
|
->where('city_id', $me->city_id)
|
|
->where('is_active', true)
|
|
->with('seller:id,name')
|
|
->orderByDesc('id')
|
|
->limit(50)
|
|
->get()
|
|
->map(fn (MarketListing $l): array => [
|
|
'id' => $l->id,
|
|
'item' => $l->item,
|
|
'qty' => (int) $l->qty,
|
|
'unit_price' => (float) $l->unit_price,
|
|
'seller_id' => $l->seller_character_id,
|
|
'seller_name' => $l->seller?->name,
|
|
'is_mine' => (int) $l->seller_character_id === (int) $me->id,
|
|
]);
|
|
|
|
$missions = Mission::query()
|
|
->where('city_id', $me->city_id)
|
|
->where('is_active', true)
|
|
->get()
|
|
->map(function (Mission $m) use ($me) {
|
|
$pivot = DB::table('character_missions')
|
|
->where('character_id', $me->id)
|
|
->where('mission_id', $m->id)
|
|
->first();
|
|
|
|
return [
|
|
'id' => $m->id,
|
|
'code' => $m->code,
|
|
'title' => $m->title,
|
|
'description' => $m->description,
|
|
'target' => (int) $m->target,
|
|
'reward' => (float) $m->reward,
|
|
'progress' => (int) ($pivot->progress ?? 0),
|
|
'is_completed' => (bool) ($pivot->is_completed ?? false),
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'listings' => $listings,
|
|
'missions' => $missions,
|
|
'money' => (float) $me->money,
|
|
'inventory' => $me->inventory()->get()->mapWithKeys(fn ($i) => [$i->item => (int) $i->qty])->all(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Publish inventory for sale.
|
|
*/
|
|
public function sell(Request $request): JsonResponse
|
|
{
|
|
$me = Character::ensureForUser($request->user());
|
|
$data = $request->validate([
|
|
'item' => ['required', 'in:food,water,materials,product'],
|
|
'qty' => ['required', 'integer', 'min:1', 'max:100'],
|
|
'unit_price' => ['required', 'numeric', 'min:0.5', 'max:1000'],
|
|
]);
|
|
|
|
$row = InventoryItem::query()
|
|
->where('character_id', $me->id)
|
|
->where('item', $data['item'])
|
|
->first();
|
|
|
|
if ($row === null || (int) $row->qty < (int) $data['qty']) {
|
|
return response()->json(['message' => __('city.no_inventory')], 422);
|
|
}
|
|
|
|
DB::transaction(function () use ($me, $data, $row): void {
|
|
$row->qty = (int) $row->qty - (int) $data['qty'];
|
|
$row->save();
|
|
MarketListing::create([
|
|
'seller_character_id' => $me->id,
|
|
'city_id' => $me->city_id,
|
|
'item' => $data['item'],
|
|
'qty' => (int) $data['qty'],
|
|
'unit_price' => (float) $data['unit_price'],
|
|
'is_active' => true,
|
|
]);
|
|
});
|
|
|
|
return response()->json(['message' => __('city.market_listed')]);
|
|
}
|
|
|
|
/**
|
|
* Purchase a listing from another citizen.
|
|
*/
|
|
public function buy(Request $request): JsonResponse
|
|
{
|
|
$me = Character::ensureForUser($request->user());
|
|
$data = $request->validate([
|
|
'listing_id' => ['required', 'integer', 'exists:market_listings,id'],
|
|
'qty' => ['nullable', 'integer', 'min:1', 'max:100'],
|
|
]);
|
|
|
|
$listing = MarketListing::query()->findOrFail($data['listing_id']);
|
|
|
|
if (! $listing->is_active || (int) $listing->seller_character_id === (int) $me->id) {
|
|
return response()->json(['message' => __('city.market_unavailable')], 422);
|
|
}
|
|
|
|
$qty = min((int) ($data['qty'] ?? $listing->qty), (int) $listing->qty);
|
|
$total = round($listing->unit_price * $qty, 2);
|
|
|
|
if ((float) $me->money < $total) {
|
|
return response()->json(['message' => __('city.no_money')], 422);
|
|
}
|
|
|
|
DB::transaction(function () use ($me, $listing, $qty, $total): void {
|
|
$seller = Character::query()->lockForUpdate()->findOrFail($listing->seller_character_id);
|
|
$me->money = round((float) $me->money - $total, 2);
|
|
$me->save();
|
|
$seller->money = round((float) $seller->money + $total, 2);
|
|
$seller->save();
|
|
|
|
$row = InventoryItem::query()
|
|
->where('character_id', $me->id)
|
|
->where('item', $listing->item)
|
|
->first();
|
|
if ($row) {
|
|
$row->qty = (int) $row->qty + $qty;
|
|
$row->save();
|
|
} else {
|
|
InventoryItem::create([
|
|
'character_id' => $me->id,
|
|
'item' => $listing->item,
|
|
'qty' => $qty,
|
|
]);
|
|
}
|
|
|
|
$listing->qty = (int) $listing->qty - $qty;
|
|
if ($listing->qty <= 0) {
|
|
$listing->is_active = false;
|
|
}
|
|
$listing->save();
|
|
|
|
Transaction::create([
|
|
'from_character_id' => $me->id,
|
|
'to_character_id' => $seller->id,
|
|
'item' => $listing->item,
|
|
'qty' => $qty,
|
|
'amount' => $total,
|
|
]);
|
|
});
|
|
|
|
$me->refresh();
|
|
|
|
return response()->json([
|
|
'message' => __('city.market_bought'),
|
|
'money' => (float) $me->money,
|
|
'inventory' => $me->inventory()->get()->mapWithKeys(fn ($i) => [$i->item => (int) $i->qty])->all(),
|
|
]);
|
|
}
|
|
}
|