The red carpet: 3-slot velvet holding pen — park them, they pay hype; patience by archetype, storms, and the entrance dividend on admit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b7bb8f49ab
commit
5ebdfc61eb
@ -1498,3 +1498,36 @@ card aggro +2 + tabMisreturned ✓ · THE POUR screenshot.
|
||||
patrons ever share a full name.
|
||||
- No cash meter exists on purpose: tips/tab totals are incidents + toasts
|
||||
(the report is the moral ledger, per the joint bust).
|
||||
|
||||
## SESSION — FABLE-SOLO-14 · 2026-07-21
|
||||
|
||||
**Branch:** main (solo, Fable, ultra) · **Gate:** lint ✓ build ✓ test ✓ (718 tests, 45 files)
|
||||
**Deployed:** https://monsterrobot.games/not-tonight/ (rsync, curl 200)
|
||||
|
||||
### The red carpet (John's ask: "make ppl wait ages")
|
||||
QueueManager owns it (pure, 7 new tests); DoorScene renders it.
|
||||
- **CARPET button** beside UNHOOK ROPE (rope shrank 192→130px). Sends the
|
||||
patron at the rope to a 3-slot red strip between rope and door (brass posts
|
||||
+ velvet drawn in buildStreet; dolls clickable, impatience-sway ramps).
|
||||
- **Economy** (`CARPET_TUNING`): 0.02 hype/s per body on display, ADDITIVE to
|
||||
ordinary queue theatre (freeing the rope re-arms the per-wait hype cap — the
|
||||
carpet is a hype engine, that's the point). Patience 30–55s × archetype
|
||||
scale (influencer 1.8 … regular 0.5); past it they STORM (aggro +3, toast,
|
||||
`carpetStorm` incident, drained via `takeStorms()` — no new bus event names,
|
||||
EventMap is frozen contract).
|
||||
- **Recall**: click the doll (rope must be free) → `recallFromCarpet` puts
|
||||
them up through the shared `presentUp()` path (extracted from callNext — a
|
||||
recalled patron gets passback checks, Franko tips, tray, the lot).
|
||||
- **The entrance**: ADMIT off the carpet pays vibe +2 + hype 0.012/s stood
|
||||
(cap 0.6), `carpetEntrance` incident. Deny pays nothing extra — you made
|
||||
them wait for THAT.
|
||||
- Verified in pane: park → rope keeps working → recall(4s) → admit paid 0.048
|
||||
hype + incident ✓ · storm at 25s with toast/incident ✓ · screenshots.
|
||||
|
||||
### Notes
|
||||
- Kayden never uses the carpet (he doesn't know what it's for). Bodies keep
|
||||
ticking/storming while the player is inside — QueueManager.update runs on.
|
||||
- Art audit for the next MODELBEAST batch (placeholder rects live for):
|
||||
`monitor`, `boothLamp`, `dishwasher`, `poolTable`, `wetFloor`, `highTable`,
|
||||
plus new candidates: red-carpet strip + brass bollards (street), the pour
|
||||
glass, the card jar. Recipe in the Phase-3 deploy note.
|
||||
|
||||
@ -16,6 +16,16 @@ export const DOOR_UI = {
|
||||
idTrayEmpty: 'no ID handed over',
|
||||
dressCode: "DAZZA'S RULES",
|
||||
dressCodeEmpty: 'no rules yet.\nuse ur judgement\n(dont)',
|
||||
carpet: 'CARPET',
|
||||
} as const;
|
||||
|
||||
/** The red carpet: a velvet holding pen for making the right people wait. */
|
||||
export const CARPET_UI = {
|
||||
sent: 'they take position on the carpet like it was always the plan.',
|
||||
full: 'the carpet is at capacity. it is a small carpet.',
|
||||
storm: 'someone on the carpet is done being furniture. they leave, loudly, mid-pose.',
|
||||
entrance: 'an ENTRANCE. the room parts. worth every minute of the wait.',
|
||||
recallBusy: 'finish with the one at the rope first.',
|
||||
} as const;
|
||||
|
||||
/** Shown once, first thirty seconds, then never again. A stranger must be able to play. */
|
||||
|
||||
@ -15,11 +15,12 @@ import {
|
||||
DOOR_TUTORIAL,
|
||||
DOOR_UI,
|
||||
BOARD_SHUFFLE_TOASTS,
|
||||
CARPET_UI,
|
||||
FRANKO_TIPS,
|
||||
PATDOWN_LINES,
|
||||
RULE_RETRACTIONS,
|
||||
} from '../../data/strings/door';
|
||||
import { QueueManager } from './QueueManager';
|
||||
import { CARPET_TUNING, QueueManager, entranceBonus } from './QueueManager';
|
||||
import { PatronUpView } from './PatronUpView';
|
||||
import { IdCardView } from './IdCardView';
|
||||
import { PhoneWidget } from './PhoneWidget';
|
||||
@ -72,6 +73,7 @@ export class DoorScene extends Phaser.Scene {
|
||||
private sobriety!: SobrietyModal;
|
||||
|
||||
private queueSprites: QueueSprite[] = [];
|
||||
private carpetSprites: QueueSprite[] = [];
|
||||
private trayCard: Phaser.GameObjects.Container | null = null;
|
||||
private buttons: Record<string, Button> = {};
|
||||
private vibeBar!: MeterBar;
|
||||
@ -250,6 +252,17 @@ export class DoorScene extends Phaser.Scene {
|
||||
.text(DOOR_X, 184, 'step inside', { fontFamily: MONO, fontSize: '7px', color: '#6a4a5a' })
|
||||
.setOrigin(0.5, 0);
|
||||
|
||||
// The red carpet: a strip of somewhere-better between rope and door,
|
||||
// flanked by brass posts. Bodies parked here are rendered by
|
||||
// syncCarpetSprites; the strip is set dressing.
|
||||
this.add.rectangle(420, 232, 124, 12, 0x8a1428).setDepth(2);
|
||||
this.add.rectangle(420, 227, 124, 2, 0xa8283c).setDepth(2);
|
||||
for (const px of [356, 484]) {
|
||||
this.add.rectangle(px, 216, 3, 22, 0xc8a24a).setDepth(2);
|
||||
this.add.rectangle(px, 205, 7, 4, 0xe0c060).setDepth(2);
|
||||
}
|
||||
this.add.rectangle(420, 206, 126, 2, 0x7a2438).setDepth(2); // the velvet itself
|
||||
|
||||
this.buildRain();
|
||||
this.buildRope();
|
||||
}
|
||||
@ -344,10 +357,15 @@ export class DoorScene extends Phaser.Scene {
|
||||
|
||||
// verdicts
|
||||
this.buttons.rope = new Button(this, {
|
||||
x: 442, y: 256, w: 192, h: 24, label: DOOR_UI.rope, size: 9,
|
||||
x: 442, y: 256, w: 130, h: 24, label: DOOR_UI.rope, size: 9,
|
||||
fill: 0x5a2030, hover: 0x8a2038,
|
||||
onClick: () => this.callNext(),
|
||||
});
|
||||
this.buttons.carpet = new Button(this, {
|
||||
x: 576, y: 256, w: 58, h: 24, label: DOOR_UI.carpet, size: 7,
|
||||
fill: 0x6a1424, hover: 0x9a2038,
|
||||
onClick: () => this.callCarpet(),
|
||||
});
|
||||
this.buttons.test = new Button(this, {
|
||||
x: 442, y: 284, w: 94, h: 22, label: DOOR_UI.test, size: 7,
|
||||
onClick: () => this.runSobriety(),
|
||||
@ -446,6 +464,12 @@ export class DoorScene extends Phaser.Scene {
|
||||
this.showToast(DOOR_UI.ropeEmpty);
|
||||
return;
|
||||
}
|
||||
this.presentUp(p);
|
||||
}
|
||||
|
||||
/** Everything that happens once SOMEBODY is at the rope — the rope and the
|
||||
* carpet recall share this path, so a recalled patron is a full patron. */
|
||||
private presentUp(p: Patron): void {
|
||||
this.testedThisPatron = false;
|
||||
this.pattedThisPatron = false;
|
||||
// Passback: this exact card already went in tonight. The tell is set HERE —
|
||||
@ -476,6 +500,79 @@ export class DoorScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- the red carpet ----------------------------------------------------
|
||||
|
||||
/** The patron at the rope takes position between the brass posts. */
|
||||
private callCarpet(): void {
|
||||
const p = this.queue.patronUp;
|
||||
if (!p || this.busy) return;
|
||||
if (this.queue.toCarpet() === null) {
|
||||
this.showToast(CARPET_UI.full);
|
||||
return;
|
||||
}
|
||||
this.idCard.close();
|
||||
this.trayCard?.destroy();
|
||||
this.trayCard = null;
|
||||
this.up.clear();
|
||||
this.setVerdictButtons(false);
|
||||
this.night.sfx?.play('ropeUnhook');
|
||||
this.showToast(CARPET_UI.sent);
|
||||
this.logIncidentDoor('carpet', p.id, 'Sent to the carpet to be seen waiting.');
|
||||
}
|
||||
|
||||
/** Click a carpet doll: wave them up to the rope for the actual ruling. */
|
||||
private recallCarpet(patronId: string): void {
|
||||
if (this.busy) return;
|
||||
if (this.queue.patronUp) {
|
||||
this.showToast(CARPET_UI.recallBusy);
|
||||
return;
|
||||
}
|
||||
const p = this.queue.recallFromCarpet(patronId);
|
||||
if (!p) return;
|
||||
this.presentUp(p);
|
||||
}
|
||||
|
||||
private logIncidentDoor(kind: string, patronId: string | undefined, detail: string): void {
|
||||
this.night.bus.emit('incident:log', { clockMin: this.night.state.clockMin, kind, patronId, detail });
|
||||
}
|
||||
|
||||
/** The carpet is a view of QueueManager, same discipline as the queue line. */
|
||||
private syncCarpetSprites(): void {
|
||||
const slots = this.queue.carpetSnapshot;
|
||||
const same =
|
||||
slots.length === this.carpetSprites.length &&
|
||||
slots.every((s, i) => this.carpetSprites[i]?.patron.id === s.patron.id);
|
||||
if (!same) {
|
||||
for (const s of this.carpetSprites) s.image.destroy();
|
||||
this.carpetSprites = slots.map((slot, i) => {
|
||||
const x = 372 + i * 34;
|
||||
const img = this.add
|
||||
.image(x, QUEUE_FOOT_Y + 6, renderDoll(this, slot.patron, 'queue'))
|
||||
.setOrigin(0.5, 1)
|
||||
.setScale(1.5)
|
||||
.setDepth(205)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
img.on('pointerdown', () => this.recallCarpet(slot.patron.id));
|
||||
img.setData('baseX', x);
|
||||
return { patron: slot.patron, image: img, link: null };
|
||||
});
|
||||
}
|
||||
// Impatience is visible: the sway grows as the patience runs out.
|
||||
for (let i = 0; i < this.carpetSprites.length; i++) {
|
||||
const s = this.carpetSprites[i]!;
|
||||
const slot = slots[i];
|
||||
if (!slot) continue;
|
||||
const fuse = Math.min(1, slot.standMs / slot.patienceMs);
|
||||
s.image.x = s.image.getData('baseX') + Math.sin(this.elapsedMs / (300 - 140 * fuse)) * (1 + fuse * 3);
|
||||
}
|
||||
|
||||
for (const p of this.queue.takeStorms()) {
|
||||
this.showToast(CARPET_UI.storm);
|
||||
this.night.sfx?.play('doorBang');
|
||||
this.logIncidentDoor('carpetStorm', p.id, 'Ran out of patience on the carpet and left, loudly.');
|
||||
}
|
||||
}
|
||||
|
||||
/** Nudge the player toward a tab that just became relevant, without switching it. */
|
||||
private flashTab(tab: ClipboardTab): void {
|
||||
const b = this.tabButtons[tab];
|
||||
@ -565,6 +662,14 @@ export class DoorScene extends Phaser.Scene {
|
||||
}
|
||||
this.applyOutcome(p, verdict, outcome);
|
||||
this.applyEncounter(p, verdict);
|
||||
// The entrance dividend: an admit off the carpet pays for every minute
|
||||
// they were made to stand there being looked at.
|
||||
const stoodMs = this.queue.carpetMsFor(p.id);
|
||||
if (verdict === 'admit' && stoodMs > 0) {
|
||||
this.night.bus.emit('meters:delta', { vibe: CARPET_TUNING.entranceVibe, hype: entranceBonus(stoodMs) });
|
||||
this.time.delayedCall(600, () => this.showToast(CARPET_UI.entrance));
|
||||
this.logIncidentDoor('carpetEntrance', p.id, `Admitted after ${Math.round(stoodMs / 1000)}s on the carpet.`);
|
||||
}
|
||||
this.queue.resolveUp(verdict === 'deny');
|
||||
|
||||
// Highlight AFTER the call, never before. Lighting up the broken rules the
|
||||
@ -923,6 +1028,10 @@ export class DoorScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.syncQueueSprites();
|
||||
this.syncCarpetSprites();
|
||||
this.buttons.carpet?.setEnabled(
|
||||
!this.busy && this.queue.patronUp !== null && this.queue.carpetSnapshot.length < CARPET_TUNING.slots,
|
||||
);
|
||||
|
||||
const rope = this.buttons.rope;
|
||||
if (rope) {
|
||||
@ -981,6 +1090,8 @@ export class DoorScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private teardown(): void {
|
||||
for (const s of this.carpetSprites) s.image.destroy();
|
||||
this.carpetSprites = [];
|
||||
for (const off of this.offs) off();
|
||||
this.offs = [];
|
||||
this.phone?.destroy();
|
||||
|
||||
@ -69,6 +69,43 @@ export const QUEUE_TUNING = {
|
||||
encountersPerNight: 3,
|
||||
} as const;
|
||||
|
||||
// The red carpet (design §2 queue theatre, escalated): a velvet holding pen
|
||||
// where you park somebody gorgeous and make them WAIT, visibly, at length.
|
||||
// Every body on display sells the room; every body has a limit.
|
||||
export const CARPET_TUNING = {
|
||||
/** brass posts only stretch so far */
|
||||
slots: 3,
|
||||
/** hype per real second per body on display */
|
||||
hypePerSecPerBody: 0.02,
|
||||
/** the entrance: admitting off the carpet pays hype scaled by the stand */
|
||||
entranceHypePerSec: 0.012,
|
||||
entranceHypeCap: 0.6,
|
||||
entranceVibe: 2,
|
||||
/** patience in real seconds, before the archetype scale */
|
||||
patienceSecBase: [30, 55] as const,
|
||||
/**
|
||||
* Who tolerates being furniture. Influencers LIVE for it; a regular gives
|
||||
* it thirty seconds and tells the whole suburb.
|
||||
*/
|
||||
patienceScale: {
|
||||
influencer: 1.8, financeBro: 1.3, ownersMate: 1.2, fresh18: 1.1,
|
||||
almost18: 1.0, inspector: 1.0, punter: 0.9, quietMessy: 0.7, regular: 0.5,
|
||||
} as Readonly<Record<import('../../data/types').Archetype, number>>,
|
||||
/** storming off the carpet is a scene the whole queue watches */
|
||||
stormAggro: 3,
|
||||
} as const;
|
||||
|
||||
/** The entrance dividend for `standMs` on the carpet, capped. */
|
||||
export function entranceBonus(standMs: number): number {
|
||||
return Math.min(CARPET_TUNING.entranceHypeCap, (standMs / 1000) * CARPET_TUNING.entranceHypePerSec);
|
||||
}
|
||||
|
||||
export interface CarpetSlot {
|
||||
patron: Patron;
|
||||
standMs: number;
|
||||
patienceMs: number;
|
||||
}
|
||||
|
||||
export interface QueueSnapshot {
|
||||
waiting: Patron[];
|
||||
visible: Patron[];
|
||||
@ -106,6 +143,13 @@ export class QueueManager {
|
||||
|
||||
/** everyone who reached the front tonight, for the summary screen */
|
||||
readonly seen: Patron[] = [];
|
||||
/** bodies currently on display between the brass posts */
|
||||
private readonly carpet: CarpetSlot[] = [];
|
||||
/** storm-offs since the scene last asked — drained by takeStorms() */
|
||||
private readonly storms: Patron[] = [];
|
||||
/** patronId -> how long they stood, recorded at recall (the entrance maths) */
|
||||
private readonly carpetStand = new Map<string, number>();
|
||||
private readonly carpetRng: RngStream;
|
||||
|
||||
constructor(
|
||||
private readonly bus: EventBus,
|
||||
@ -134,6 +178,7 @@ export class QueueManager {
|
||||
this.encounterRng = rng.stream('encounters');
|
||||
this.castRng = rng.stream('castPick');
|
||||
this.passbackRng = rng.stream('passback');
|
||||
this.carpetRng = rng.stream('carpet');
|
||||
bus.on('door:verdict', ({ patron, verdict }) => {
|
||||
// Twin B arrives a few minutes after A's verdict, WHATEVER it was — he
|
||||
// was parking the car either way. A plain patron, no script: his own
|
||||
@ -222,6 +267,7 @@ export class QueueManager {
|
||||
update(deltaMs: number): void {
|
||||
const sec = deltaMs / 1000;
|
||||
this.phoneCooldownMs = Math.max(0, this.phoneCooldownMs - deltaMs);
|
||||
this.tickCarpet(deltaMs);
|
||||
|
||||
const idle = this.up === null && this.waiting.length > 0;
|
||||
if (idle) {
|
||||
@ -256,6 +302,69 @@ export class QueueManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** The carpet ticks whether or not the rope is busy — display doesn't pause. */
|
||||
private tickCarpet(deltaMs: number): void {
|
||||
if (this.carpet.length === 0) return;
|
||||
const sec = deltaMs / 1000;
|
||||
this.bus.emit('meters:delta', { hype: CARPET_TUNING.hypePerSecPerBody * this.carpet.length * sec });
|
||||
for (let i = this.carpet.length - 1; i >= 0; i--) {
|
||||
const slot = this.carpet[i]!;
|
||||
slot.standMs += deltaMs;
|
||||
if (slot.standMs < slot.patienceMs) continue;
|
||||
this.carpet.splice(i, 1);
|
||||
this.storms.push(slot.patron);
|
||||
this.bus.emit('meters:delta', { aggro: CARPET_TUNING.stormAggro });
|
||||
}
|
||||
}
|
||||
|
||||
get carpetSnapshot(): readonly CarpetSlot[] {
|
||||
return this.carpet;
|
||||
}
|
||||
|
||||
/** Storm-offs since last asked. The scene narrates; the meters already paid. */
|
||||
takeStorms(): Patron[] {
|
||||
return this.storms.splice(0, this.storms.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* The patron at the rope takes position between the brass posts. Frees the
|
||||
* rope — the theatre is that you keep working while they stand there.
|
||||
*/
|
||||
toCarpet(): Patron | null {
|
||||
const p = this.up;
|
||||
if (!p || this.carpet.length >= CARPET_TUNING.slots) return null;
|
||||
this.up = null;
|
||||
this.waitMs = 0;
|
||||
this.hypeEarnedThisWait = 0;
|
||||
const [lo, hi] = CARPET_TUNING.patienceSecBase;
|
||||
const scale = CARPET_TUNING.patienceScale[p.archetype] ?? 1;
|
||||
this.carpet.push({
|
||||
patron: p,
|
||||
standMs: 0,
|
||||
patienceMs: this.carpetRng.int(lo, hi) * scale * 1000,
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Wave them off the carpet and up to the rope. Only while the rope is free. */
|
||||
recallFromCarpet(patronId: string): Patron | null {
|
||||
if (this.up !== null) return null;
|
||||
const i = this.carpet.findIndex((s) => s.patron.id === patronId);
|
||||
if (i < 0) return null;
|
||||
const slot = this.carpet.splice(i, 1)[0]!;
|
||||
this.carpetStand.set(slot.patron.id, slot.standMs);
|
||||
this.up = slot.patron;
|
||||
this.waitMs = 0;
|
||||
this.hypeEarnedThisWait = 0;
|
||||
this.bus.emit('door:patronUp', { patron: slot.patron });
|
||||
return slot.patron;
|
||||
}
|
||||
|
||||
/** How long this patron stood on the carpet before their recall, in ms. */
|
||||
carpetMsFor(patronId: string): number {
|
||||
return this.carpetStand.get(patronId) ?? 0;
|
||||
}
|
||||
|
||||
/** The Rope. Nothing steps up until the player says so. */
|
||||
callNext(): Patron | null {
|
||||
if (this.up !== null) return null;
|
||||
|
||||
@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { EventBus } from '../src/core/EventBus';
|
||||
import { SeededRNG } from '../src/core/SeededRNG';
|
||||
import { _resetPatronSerial } from '../src/patrons/generator';
|
||||
import { QueueManager, QUEUE_TUNING } from '../src/scenes/door/QueueManager';
|
||||
import { CARPET_TUNING, QueueManager, QUEUE_TUNING, entranceBonus } from '../src/scenes/door/QueueManager';
|
||||
import type { Patron } from '../src/data/types';
|
||||
|
||||
const NIGHT = new Date('2026-07-18T00:00:00');
|
||||
@ -289,3 +289,85 @@ describe('QueueManager — determinism', () => {
|
||||
expect(nightFingerprint(1)).not.toEqual(nightFingerprint(2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('the red carpet', () => {
|
||||
const carpetSetup = () => {
|
||||
const s = setup(9);
|
||||
fillTo(s.q, 6);
|
||||
return s;
|
||||
};
|
||||
|
||||
it('toCarpet parks the up patron and frees the rope', () => {
|
||||
const { q } = carpetSetup();
|
||||
const p = q.callNext()!;
|
||||
expect(q.toCarpet()?.id).toBe(p.id);
|
||||
expect(q.patronUp).toBeNull();
|
||||
expect(q.carpetSnapshot.length).toBe(1);
|
||||
expect(q.callNext()).not.toBeNull(); // the rope keeps working
|
||||
});
|
||||
|
||||
it('the brass posts only stretch to CARPET_TUNING.slots', () => {
|
||||
const { q } = carpetSetup();
|
||||
for (let i = 0; i < CARPET_TUNING.slots; i++) {
|
||||
expect(q.callNext()).not.toBeNull();
|
||||
expect(q.toCarpet()).not.toBeNull();
|
||||
}
|
||||
expect(q.callNext()).not.toBeNull();
|
||||
expect(q.toCarpet()).toBeNull(); // full — the fourth stays at the rope
|
||||
expect(q.patronUp).not.toBeNull();
|
||||
});
|
||||
|
||||
it('bodies on display pay hype per second, on top of the queue theatre', () => {
|
||||
const { q, total } = carpetSetup();
|
||||
q.callNext();
|
||||
q.toCarpet();
|
||||
// Rope free + line waiting: ordinary queue theatre still flows. The
|
||||
// carpet's dividend is ADDITIVE — that is the whole point of the posts.
|
||||
const before = total('hype');
|
||||
q.update(1000);
|
||||
expect(total('hype') - before).toBeCloseTo(
|
||||
CARPET_TUNING.hypePerSecPerBody + QUEUE_TUNING.hypePerSec,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
it('patience runs out: a storm-off, aggro, and the slot comes back', () => {
|
||||
const { q, total } = carpetSetup();
|
||||
const p = q.callNext()!;
|
||||
q.toCarpet();
|
||||
const before = total('aggro');
|
||||
// Worst-case patience is base-max × the biggest archetype scale.
|
||||
q.update(CARPET_TUNING.patienceSecBase[1] * 1.8 * 1000 + 1000);
|
||||
expect(q.carpetSnapshot.length).toBe(0);
|
||||
expect(q.takeStorms().map((x) => x.id)).toEqual([p.id]);
|
||||
expect(q.takeStorms()).toEqual([]); // drained means drained
|
||||
expect(total('aggro') - before).toBeGreaterThanOrEqual(CARPET_TUNING.stormAggro);
|
||||
});
|
||||
|
||||
it('recall puts them back up and remembers the stand for the entrance', () => {
|
||||
const { q } = carpetSetup();
|
||||
const p = q.callNext()!;
|
||||
q.toCarpet();
|
||||
q.update(3000);
|
||||
expect(q.recallFromCarpet('nobody')).toBeNull();
|
||||
const back = q.recallFromCarpet(p.id);
|
||||
expect(back?.id).toBe(p.id);
|
||||
expect(q.patronUp?.id).toBe(p.id);
|
||||
expect(q.carpetMsFor(p.id)).toBe(3000);
|
||||
});
|
||||
|
||||
it('recall refuses while somebody else is at the rope', () => {
|
||||
const { q } = carpetSetup();
|
||||
const p = q.callNext()!;
|
||||
q.toCarpet();
|
||||
q.callNext();
|
||||
expect(q.recallFromCarpet(p.id)).toBeNull();
|
||||
expect(q.carpetSnapshot.length).toBe(1);
|
||||
});
|
||||
|
||||
it('entranceBonus scales with the stand and caps', () => {
|
||||
expect(entranceBonus(0)).toBe(0);
|
||||
expect(entranceBonus(10_000)).toBeCloseTo(10 * CARPET_TUNING.entranceHypePerSec);
|
||||
expect(entranceBonus(10_000_000)).toBe(CARPET_TUNING.entranceHypeCap);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user