v1.3.0: per-player money & reputation in engine + actions (ISS-110)
CI / build + unit + e2e (pull_request) Successful in 13m20s
CI / build + unit + e2e (pull_request) Successful in 13m20s
This commit is contained in:
+17
-9
@@ -1,7 +1,7 @@
|
||||
import { randomInt, randomUUID } from 'node:crypto';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { getGameDb, persist, type GameDb } from '@/lib/db/client';
|
||||
import { getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { LOCAL_PLAYER, getLocalPlayer, getPlayer, updatePlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm, initialBearingDeg } from '@/lib/geo';
|
||||
@@ -251,12 +251,13 @@ export async function deliverPackages(inst: GameDb, clockMs: number): Promise<nu
|
||||
for (const pk of onBoard) {
|
||||
if (pk.status === 'loaded' && clockMs <= pk.deadlineSimTime) {
|
||||
await inst.db.update(schema.packages).set({ status: 'delivered' }).where(eq(schema.packages.id, pk.id));
|
||||
// Fresh player read per award so multiple deliveries in one tick all count.
|
||||
const fresh = await getLocalPlayer(inst);
|
||||
// v1.3.0 (ISS-110): the award goes to the DELIVERED PLANE'S OWNER — fresh read per award so
|
||||
// multiple deliveries in one tick all count.
|
||||
const fresh = await getPlayer(inst, p.ownerId);
|
||||
// v0.37.0 (ISS-073): special packages add the registry's flat rep bonus on top of the
|
||||
// distance-based gain — same double-rounding convention as before (gain rounded to cents, total re-rounded).
|
||||
const repBonus = pk.special ? SPECIALS[pk.special]?.repBonus ?? 0 : 0;
|
||||
await updateLocalPlayerEconomy(inst, {
|
||||
await updatePlayerEconomy(inst, p.ownerId, {
|
||||
cash: fresh.cash + pk.reward,
|
||||
reputation: Math.round((fresh.reputation + reputationGainFor(distanceKm) + repBonus) * 100) / 100,
|
||||
});
|
||||
@@ -306,14 +307,15 @@ export async function deliverPackages(inst: GameDb, clockMs: number): Promise<nu
|
||||
// ISS-082: auto top-up — same target/price terms as flyPlane's dispatch refuel.
|
||||
const target = Math.min(type.maxFuel, Math.max(landedFuel, nextRequiredL * 1.5));
|
||||
const liters = Math.max(0, target - landedFuel);
|
||||
// Fresh player read (same pattern as the delivery awards above) so a top-up in this tick sees current cash.
|
||||
const fresh = await getLocalPlayer(inst);
|
||||
// v1.3.0 (ISS-110): the PLANE OWNER's economy pays — fresh read (same pattern as the
|
||||
// delivery awards above) so a top-up in this tick sees current cash.
|
||||
const fresh = await getPlayer(inst, p.ownerId);
|
||||
const cost = Math.round(liters * FUEL_PRICE_PER_L * (1 - repRate(fresh.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT)) * 100) / 100;
|
||||
if (cost === 0 || fresh.cash >= cost) {
|
||||
queue.shift();
|
||||
chainedTo = nextDest;
|
||||
topUpFuel = target;
|
||||
if (cost > 0) await updateLocalPlayerEconomy(inst, { cash: Math.round((fresh.cash - cost) * 100) / 100 });
|
||||
if (cost > 0) await updatePlayerEconomy(inst, p.ownerId, { cash: Math.round((fresh.cash - cost) * 100) / 100 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,7 +384,10 @@ export async function spawnPoolPackages(
|
||||
lon: f.properties.lon,
|
||||
}));
|
||||
const rand = rng ?? mulberry32(randomInt(0, 2 ** 31));
|
||||
// v0.38.0 (ISS-072): spawned jobs carry the local player's CURRENT reputation quality — read once per spawn batch.
|
||||
// v0.38.0 (ISS-072): spawned jobs carry a reputation-based job quality — read once per spawn batch.
|
||||
// ponytail: stays on the seeded 'local' row in v1.3.0 (ISS-110) — pools are SHARED world state
|
||||
// (packages have no owner), so there is no single owning player whose rep should set job quality;
|
||||
// per-player job quality / pools is a later multiplayer step (#112).
|
||||
const player = await getLocalPlayer(inst);
|
||||
|
||||
let n = 0;
|
||||
@@ -431,9 +436,12 @@ export async function spawnPoolPackages(
|
||||
// package history is dropped, everything else matches GET /api/state exactly. Failures are logged,
|
||||
// not fatal: polling still works either way. Background ticks (~1 s) cover action mutations for
|
||||
// other clients too.
|
||||
// v1.3.0 (ISS-110): the shared broadcast channel has no acting session — it carries the seeded
|
||||
// local player's account until per-player SSE frames land (#112). Session-scoped reads (/api/state,
|
||||
// /api/events connect frame, action responses) all source their own row.
|
||||
async function broadcastState(): Promise<void> {
|
||||
try {
|
||||
broadcast(JSON.stringify(sseFrame(await getStateSnapshot())));
|
||||
broadcast(JSON.stringify(sseFrame(await getStateSnapshot(LOCAL_PLAYER.id))));
|
||||
} catch (err) {
|
||||
console.error('flight: state broadcast failed', err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user