feat(city): add player marketplace, mission journal, character customization and presence
- Player-to-player marketplace with listings, buying and selling - Mission system with progress tracking and money rewards - Character appearance customization (skin, hair, face, beard, accessory) - Multiplayer presence feed showing nearby citizens in near-real-time - Admin world dashboard with live platform metrics - Bus and train transportation with temporary ride state - Mobile joystick controls and minimap HUD - Education bonus applied to work salary
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user