Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/game.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ npm run dev
- **Fish:** stand on the 🎣 pad to hook fish; they get chopped into fish slices automatically
- **Sell fish (stage 1):** pick slices up from the pile, drop them on the counter's 🐟 pad, and walk over the cash to collect it. Walk-in customers pay $4 a slice; once the drive-up window is built, drivers on the road buy 4–8 at a time at $6
- **Upgrade:** stand on a price tile (the squares) and hold **E**, or the **Buy** button that comes up at the side, to pay into it. On a computer the button can be turned off under Controls in the settings; on a touch screen, with no E, it's always there. Stand on one for 2 seconds without buying and a reminder says how. Squares are for buying (the tiles, and the Upgrade squares' menus) and circles for doing something (fishing, dropping things off, the casino's games). The tiles lie square with the world, with their icon and price turned to face the camera. Some tiles also need a star rating (see below). The chip under the rating shows the stage and how many of its upgrades are built
- **Graphics (✨ in the settings): Auto, Low or High.** High draws the whole world the way the people are drawn: low poly but rounded, with the small details (the rounded, dressed-up people under Seasons, below; real deck boards, lashed log fences and pilings, fish stalls with crushed ice, a tiled restaurant with a torii and stone lanterns, dry-stone terraces, her house with siding and shutters, and every machine, sled and campaign in the same style). The sea rolls in low-poly facets that glint, foam laps the shore and rings the floes and posts, the paddies and the hot spring ripple, cloud shadows drift over the ground, shadows have soft edges, and roofs, posts and fences get snow in winter, the deck petals in spring and leaves in autumn. Little effects mark the moments that matter: a splash when a fish is pulled out, chips off the board as the cleaver comes down, coins hopping up when cash lands in your pocket and a glint where a customer pays, gold sparkles round a tile when an upgrade is bought, smoke from her chimney and steam from the rice cookers. Low draws everything as it was before all this: block-limbed people, a flat sea, plain boxes and logs, no effects, and the renderer is lighter (a shadow map half the size with hard edges, and no more than one and a half pixels a point). Either way, whatever stands still is drawn merged with its neighbours, so the scenery takes a few draw calls instead of one for every post and plank, and the icons on the floor tiles, pads, signs and menu tags are drawn in the game's own style rather than taken from the phone's emoji, so they look the same everywhere. Auto, the default, lets the game pick, and shows under the choices which one it's using: it starts each visit on High and watches the frame rate, and if that stays under 45 a second for six seconds (not counting the first few seconds, or a pause while the tab is hidden), it drops to Low for the rest of the visit. Choosing Low or High stops that; choosing Auto again starts over on High. The choice is kept on the device. On Low, the look panel leaves out cheeks and shoes, which Low doesn't draw
- **Graphics (✨ in the settings): Auto, Low or High.** High draws the whole world the way the people are drawn: low poly but rounded, with the small details (the rounded, dressed-up people under Seasons, below; real deck boards, lashed log fences and pilings, fish stalls with crushed ice, a tiled restaurant with a torii and stone lanterns, dry-stone terraces, her house with siding and shutters, and every machine, sled and campaign in the same style). The sea rolls in low-poly facets that glint, foam laps the shore and rings the floes and posts, the paddies and the hot spring ripple, cloud shadows drift over the ground, shadows have soft edges, and roofs, posts and fences get snow in winter, the deck petals in spring and leaves in autumn. Little effects mark the moments that matter: a splash when a fish is pulled out, chips off the board as the cleaver comes down, coins hopping up when cash lands in your pocket and a glint where a customer pays, gold sparkles round a tile when an upgrade is bought, smoke from her chimney and steam from the rice cookers. Low draws everything as it was before all this: block-limbed people, a flat sea, plain boxes and logs, no effects, and the renderer is lighter (a shadow map half the size with hard edges, and no more than one and a half pixels a point). Either way, whatever stands still is drawn merged with its neighbours, so the scenery takes a few draw calls instead of one for every post and plank, and the icons on the floor tiles, pads, signs and menu tags are drawn in the game's own style rather than taken from the phone's emoji, so they look the same everywhere. Auto, the default, lets the game pick, and shows under the choices which one it's using: it starts each visit on High and watches the frame rate, and if that stays under 45 a second for six seconds (not counting the first few seconds, or a pause while the tab is hidden), it drops to Low for the rest of the visit. If the browser takes the 3D view away while you play (a phone short of memory), Auto goes to Low for good, and a dialog says the 3D view stopped, your game is saved, with a Reload button; if the 3D view can't start at all, it says to restart the browser. Choosing Low or High stops that; choosing Auto again starts over on High. The choice is kept on the device. On Low, the look panel leaves out cheeks and shoes, which Low doesn't draw
- **Upgrade square:** each stage has a 📈 Upgrade square (by the counter in stage 1, by the kitchen line in stage 2, and in stage 3 a 🔨 Craft square at the crafting bench in the cellar's workshop, which sells every room's upgrades too). Stand on it to open the stage's repeatable upgrades and tap one to buy its next level: a better product (prices), marketing (customers) or the crew (speed). The list in the top left shows every modifier in play as a percentage, with your rating counted into Customers (it brings them in ×0.6 at ★1 to ×1.4 at ★5); tap the stage chip to fold it away or back (on phones it starts folded, so the HUD leaves the game in view). Every level shows in the world too (see [Economy](#economy))
- **Fertilizer shed (stage 2):** once the takeout kiosk opens, a little thatched storehouse like the farmhouse goes up on a plank deck off the south end of the farm path, by the water wheel, with a green noren over its door and a lantern that glows at dusk. Stand on its 🌿 square to buy rice fertilizer: compost, then fish meal, then spring minerals, each making the terraces ripen half as fast again ($20,000, $40,000 and $80,000; all three make rice grow 3.4 times as fast). A labelled sack of each one bought lies on a pallet beside the shed, and the HUD's list shows it as Rice growth
- **Stage up:** with all nine market upgrades built, a gold 🏯 tile appears in the middle of the dock: Open Floe Sushi, $12,000. Paying it off plays the stage-up: the counters and the south fence come down, the restaurant, its garden and the terraces go up, and day turns to dusk. Cash, rating and upgrades carry over; the counters' leftover fish and cash move to the restaurant, and Korki's statue moves to the garden (the casino boat stays where it is)
Expand Down
7 changes: 5 additions & 2 deletions docs/graphics.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The player picks **Auto**, **Low** or **High** in the ✨ row of the settings.

- **High** draws the world the way the people are drawn: low poly but rounded, with small details, a rolling sea, cloud shadows, seasonal snow, petals and leaves, and the effects.
- **Low** draws everything exactly as it was before the rework, with a lighter renderer (a half-size shadow map with hard edges, at most 1.5 pixels a point). It's for phones that can't keep up.
- **Auto**, the default, starts on High and watches the frames in two-second looks. A look is bad if it ran under 50 fps, or it stuttered: its p95 frame slower than 25 ms or its p99 slower than 50 ms. When four of the last five looks are bad it drops to Low, and it starts on Low on that device from then on (`localStorage` key `floe-market-graphics-auto`). A hitch counts as a slow frame; only a gap of over a second (the tab hidden, the phone locked) makes it settle and start watching again. It never switches back up by itself; choosing Auto in the settings starts it on High again.
- **Auto**, the default, starts on High and watches the frames in two-second looks. A look is bad if it ran under 50 fps, or it stuttered: its p95 frame slower than 25 ms or its p99 slower than 50 ms. When four of the last five looks are bad it drops to Low, and it starts on Low on that device from then on (`localStorage` key `floe-market-graphics-auto`). A hitch counts as a slow frame; only a gap of over a second (the tab hidden, the phone locked) makes it settle and start watching again. It never switches back up by itself; choosing Auto in the settings starts it on High again. It also drops to Low for good when the browser takes the 3D view away while the game's on screen (a WebGL context loss: a phone short of graphics memory, or its graphics chip restarting), since Low needs about half the memory (`lowAfterLoss`, called by render.ts). If the view isn't back within 3 seconds, or the browser won't make one at all, a dialog (`#glLost`, errors.ts) says so and offers a reload.

The choice is kept on the device (`localStorage` key `floe-market-graphics`), never in the save or the cloud.

Expand Down Expand Up @@ -180,7 +180,10 @@ Whatever has stood still for a second is drawn merged with its neighbours: one m
- instanced and skinned meshes;
- anything under an object with `userData.noBatch = true`, as people have.

Use `noBatch` for anything that moves all the time.
Use `noBatch` for anything that moves all the time. People who stay put for good (the casino yacht's staff and
guests) call `standStill()` (characters.ts): no `noBatch` and no breathing, so they're merged while they hold still,
and an arm that moves drops out of the merge while it does. On the yacht that took about 85 draw calls off the frame
at the quay.
- **Lots of moving copies:** use one `InstancedMesh` instead, as the effects and the foam rings (sea.ts) do.

### Hidden things aren't placed
Expand Down
15 changes: 9 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -836,12 +836,14 @@
@keyframes press{ 0%,20%{ background:var(--coral); color:#fff; transform:translateY(2px); box-shadow:none; } 25%,100%{ background:var(--chip); color:inherit; transform:none; } }
@media (prefers-reduced-motion: reduce){ #hint{ transition:opacity .3s; } #hint .stick, #hint kbd{ animation:none; } }

#stale{ position:fixed; inset:0; z-index:8; display:grid; place-items:center; background:rgba(23,48,66,.55); }
#stale[hidden]{ display:none; }
#stale .card{ text-align:center; background:var(--pill); color:var(--pill-ink); padding:22px 26px; border-radius:24px; max-width:min(86vw,360px); box-shadow:0 4px 0 var(--shade); }
#stale p{ margin:0 0 14px; font-size:19px; font-weight:600; }
#stale button{ font:800 18px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; background:var(--hot); color:#fff; border:0; border-radius:999px; padding:8px 22px; cursor:pointer; box-shadow:0 3px 0 rgba(150,45,25,.45); }
#stale button:focus-visible{ outline:3px solid var(--pill-ink); outline-offset:2px; }
#stale, #glLost{ position:fixed; inset:0; z-index:8; display:grid; place-items:center; background:rgba(23,48,66,.55); }
/* (the 3D view gone, errors.ts: over the loading screen too, as it can go before the game's loaded) */
#glLost{ z-index:10; }
#stale[hidden], #glLost[hidden]{ display:none; }
:is(#stale, #glLost) .card{ text-align:center; background:var(--pill); color:var(--pill-ink); padding:22px 26px; border-radius:24px; max-width:min(86vw,360px); box-shadow:0 4px 0 var(--shade); }
:is(#stale, #glLost) p{ margin:0 0 14px; font-size:19px; font-weight:600; }
:is(#stale, #glLost) button{ font:800 18px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; background:var(--hot); color:#fff; border:0; border-radius:999px; padding:8px 22px; cursor:pointer; box-shadow:0 3px 0 rgba(150,45,25,.45); }
:is(#stale, #glLost) button:focus-visible{ outline:3px solid var(--pill-ink); outline-offset:2px; }

#stick{
position:fixed; width:110px; height:110px; margin:-55px 0 0 -55px; border-radius:50%;
Expand Down Expand Up @@ -1639,6 +1641,7 @@ <h2 id="scoresTitle">Scoreboard</h2>
</section>
<div id="hint" class="pill" role="status" hidden><div class="art" aria-hidden="true"></div><p></p></div>
<div id="stale" role="alertdialog" aria-modal="true" aria-labelledby="staleWhy" hidden><div class="card"><p id="staleWhy">Floe Market is open in another tab, so this one stopped saving.</p><button type="button">Play here instead</button></div></div>
<div id="glLost" role="alertdialog" aria-modal="true" aria-labelledby="glLostWhy" hidden><div class="card"><p id="glLostWhy"></p><button type="button">Reload</button></div></div>
<section id="shop" hidden aria-label="Upgrades"><div class="shead"><h2 id="shopTitle"></h2><span id="shopPurse" hidden></span><div id="shopTabs" role="tablist" aria-label="Upgrades" hidden></div><button id="shopShut" type="button" aria-label="Fold the menu away" hidden>✕</button></div><div id="shopRows"></div><div id="shopPick" hidden></div><div id="shopKeys" hidden><span>Click a lit one to buy it</span><span><kbd>E</kbd> buys the one under the mouse</span><span><kbd>Esc</kbd> or walk off to close</span></div></section>
<section id="look" hidden aria-labelledby="lookTitle">
<div class="lhead"><h2 id="lookTitle">Your look</h2><button id="lookDice" type="button" aria-label="Surprise me" title="Surprise me"><span aria-hidden="true">🎲</span></button><button id="lookDone" type="button">Done</button></div>
Expand Down
2 changes: 1 addition & 1 deletion src/blackjackTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function buildTable() {
[[-0.24, 0xC8323F, 6], [-0.12, 0x22874A, 4], [0, 0x1F2A33, 7], [0.12, 0x6A3FA0, 3], [0.24, 0xC8323F, 5]].forEach(([x, c, n]) => {
const s = mesh(chip, c, x, TOP + n * 0.0065, RACK.z - 0.04, true); s.scale.y = n * 0.013; g.add(s);
});
const dealer = allYear(new Person(SUITS[0], 'fancy'));
const dealer = allYear(new Person(SUITS[0], 'fancy')).standStill();
dealer.position.set(0, 0, -0.95);
g.add(dealer);
// close up, smaller than most: the dealer's top hat reaches the top of the view, and their bubble comes down over it
Expand Down
13 changes: 8 additions & 5 deletions src/casinoSalon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { clink } from './sfx';
import { allYear, animPerson, Person, SUITS } from './characters';
import { byTheStall, STALL } from './casinoRestroom';
import { atTheWall } from './wallOfShame';
import { K } from './kit';
import { DECKS, inSalon, pushOutOfBox, SALON, SHIP, stage, UPPER } from './layout';
import { player } from './player';
import { bake, bakePainted, canvasTex, FONT, G, mesh, scene, type Part } from './render';
Expand Down Expand Up @@ -171,7 +172,8 @@ const SIGN_TEX = canvasTex(512, 160, (c, w, h) => {
c.lineJoin = 'round'; c.lineWidth = 14; c.strokeStyle = '#2A0F16'; c.strokeText('CASINO', w / 2, h / 2 + 6);
c.fillStyle = '#FFD24A'; c.fillText('CASINO', w / 2, h / 2 + 6);
});
/** The marquee's bulbs, in two sets that take turns to light: the chase round the CASINO sign. */
/** The marquee's bulbs, in two sets that take turns to light: the chase round the CASINO sign. Flat colours, so a
* bulb a few pixels across is a dot whatever its shape: the kit's small ball (56 triangles, not 252). */
const bulbsOn = new MeshBasicMaterial({ color: 0xFFE08A }), bulbsOff = new MeshBasicMaterial({ color: 0xB07A2A });
/** The top of the jackstaff at the bow (casino.ts), which the strings of lights run down to from the mast. */
export const JACK = { x: SHIP.bow - 0.55, y: FY + 3.0 };
Expand Down Expand Up @@ -278,7 +280,7 @@ function buildRoof() {
for (const s of [-1, 1]) {
for (let k = 1; k < 18; k++) {
const t = k / 18, tx = MAST.x + 0.07, ty = mastTop - 0.4;
strings.push({ geo: G.sphere, at: [JACK.x + (tx - JACK.x) * t, JACK.y + (ty - JACK.y) * t - Math.sin(Math.PI * t) * 0.1, s * MAST.spread * t], scale: [0.06, 0.06, 0.06] });
strings.push({ geo: K.ball, at: [JACK.x + (tx - JACK.x) * t, JACK.y + (ty - JACK.y) * t - Math.sin(Math.PI * t) * 0.1, s * MAST.spread * t], scale: [0.06, 0.06, 0.06] });
}
}
const lights = mesh(bake(strings), bulbsOn);
Expand All @@ -298,7 +300,7 @@ function buildSigns(g: Group) {
face.position.set(SIGN.x, SIGN.y, z + side * 0.015); if (side < 0) face.rotation.y = Math.PI;
g.add(face);
let i = 0;
const bulb = (x: number, y: number) => sets[i++ % 2].push({ geo: G.sphere, at: [x, y, z + side * 0.02], scale: [0.04, 0.04, 0.04] });
const bulb = (x: number, y: number) => sets[i++ % 2].push({ geo: K.ball, at: [x, y, z + side * 0.02], scale: [0.04, 0.04, 0.04] });
for (let x = -SIGN.w / 2; x <= SIGN.w / 2 + 0.01; x += SIGN.w / 14) { bulb(SIGN.x + x, SIGN.y + SIGN.h / 2 + 0.06); bulb(SIGN.x + x, SIGN.y - SIGN.h / 2 - 0.06); }
for (const e of [-1, 1]) for (let y = -SIGN.h / 2 + 0.14; y < SIGN.h / 2 - 0.08; y += 0.2) bulb(SIGN.x + e * (SIGN.w / 2 + 0.06), SIGN.y + y);
}
Expand Down Expand Up @@ -356,7 +358,8 @@ function buildBar(g: Group) {
function guest(i: number, hatless = false) {
const who = crowd();
if (hatless) who.hat = 'bare';
return allYear(new Person(who.woman ? DRESSES[i % DRESSES.length] : SUITS[i % SUITS.length], 'fancy', who));
// (they all stay where they're put: the gamblers at the machines, the drinkers on their stools, the fan by the tables)
return allYear(new Person(who.woman ? DRESSES[i % DRESSES.length] : SUITS[i % SUITS.length], 'fancy', who)).standStill();
}

/** One of the salon's slot machines (`i` along the row), its reels spinning when a guest plays it: for `len` seconds,
Expand Down Expand Up @@ -394,7 +397,7 @@ function buildCrowd(g: Group) {
});
// the bar: two bartenders behind it, one polishing a glass and one shaking a cocktail, and guests on three stools
const bartenders = [-1.1, 1.3].map(z => {
const b = allYear(new Person(0x1F1F1F, 'waiter'));
const b = allYear(new Person(0x1F1F1F, 'waiter')).standStill();
b.position.set(BAR.x - 0.5, UPPER, z); b.rotation.y = Math.PI / 2;
g.add(b);
return b;
Expand Down
16 changes: 15 additions & 1 deletion src/characters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ export class Person extends Group {
phase = 0;
/** Seconds of breathing, from somewhere different for each of them, so a queue doesn't breathe as one. */
breath: number;
/** Stays where they're put for good, and doesn't breathe (standStill). */
still = false;
/** A chef's toque: its band, which grows taller, and the puff on top. */
private toqueBand?: Mesh;
private toquePuff?: Mesh;
Expand Down Expand Up @@ -253,6 +255,18 @@ export class Person extends Group {
} finally { Math.random = luck; }
}

/**
* Stays where they're put for good (the casino yacht's staff and guests): without the breathing, which no one sees
* from the game's camera, they hold still and are drawn merged with the scenery round them (batch.ts), a dozen people
* in a few draw calls instead of six each and as many again for their shadows. An arm that moves (a lever pulled, a
* glass raised) drops out of the merge while it does, and back in after.
*/
standStill() {
this.still = true;
delete this.userData.noBatch;
return this;
}

/** How far out from the middle the arms hang: a woman's a little closer in. */
get armSpread() { return this.style.woman ? 0.28 : 0.3; }

Expand Down Expand Up @@ -861,7 +875,7 @@ export function animPerson(p: Person, moving: boolean, dt: number, carrying: boo
b.position.y = -0.3 * (1 - Math.cos(swing));
b.rotation.x += ((moving ? LEAN : 0) - b.rotation.x) * Math.min(1, dt * 8);
b.rotation.z = moving ? s * ROCK : 0;
b.scale.y = moving ? 1 : 1 + Math.sin(p.breath * 2.4) * BREATH;
b.scale.y = moving || p.still ? 1 : 1 + Math.sin(p.breath * 2.4) * BREATH;
}

const glass = new MeshLambertMaterial({ color: 0xBDEBFA, transparent: true, opacity: .65 });
Expand Down
Loading
Loading