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(), ]); } }