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
3 changes: 2 additions & 1 deletion components/OceanScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { Wash } from '@/lib/ocean/wash';
import { outcropsInView } from '@/lib/ocean/outcrop';
import { drawOutcrops, OutcropCache } from '@/lib/ocean/outcrop-sprite';
import { drawDumbo, drawLure, drawWhaleFall } from '@/lib/ocean/sightings-deep';
import { drawOarfish, drawSiphonophore, drawTurtle } from '@/lib/ocean/sightings-shallow';
import { drawManOWar, drawOarfish, drawSiphonophore, drawTurtle } from '@/lib/ocean/sightings-shallow';
import { jellyForBlock } from '@/lib/ocean/lineage';
import { HUES, waterAt, type Ground, type Water } from '@/lib/ocean/palette';
import { hash32 } from '@/lib/ocean/random';
Expand Down Expand Up @@ -348,6 +348,7 @@ export default function OceanScene({
else if (event.kind === 'turtle') drawTurtle(bctx, W, H, age, event.seed, px, ambient, water.dark, jelly);
else if (event.kind === 'siphonophore') drawSiphonophore(bctx, W, H, age, event.seed, px, ambient, water.dark);
else if (event.kind === 'oarfish') drawOarfish(bctx, W, H, age, event.seed, px, ambient, water.dark);
else if (event.kind === 'manowar') drawManOWar(bctx, W, H, age, event.seed, px, ambient, water.dark);
else if (event.kind === 'lure') drawLure(bctx, W, H, age, event.seed, px, ambient, water.dark);
else if (event.kind === 'dumbo') drawDumbo(bctx, W, H, age, event.seed, px, ambient, water.dark);
}
Expand Down
31 changes: 27 additions & 4 deletions lib/ocean/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export type EventKind =
| 'lure'
| 'dumbo'
| 'whalefall'
| 'oarfish';
| 'oarfish'
| 'manowar';

interface Spec {
zones: number[];
Expand All @@ -48,12 +49,17 @@ export const EVENTS: Record<EventKind, Spec> = {
// Arrives over its span, then stays on the floor for the rest of the sitting.
whalefall: { zones: [3, 4], perHour: 0.04, seconds: 20, phrase: 'a whale fall on the floor', weight: 800 },
oarfish: { zones: [1, 2], perHour: 0.006, seconds: 60, phrase: 'an oarfish, hanging in the dark', weight: 1100 },
// A surface animal, so only in the first of the light.
manowar: { zones: [0], perHour: 0.8, seconds: 50, phrase: "a Portuguese man o' war drifted over", weight: 250 },
};
const ORDER: EventKind[] = ['leviathan', 'eye', 'storm', 'whale'];
/* The second roll, added after the first had been out in the world: its
events only land in minutes the first left well alone, so no saved
sitting loses or moves anything it already had. */
const LATER: EventKind[] = ['oarfish', 'whalefall', 'siphonophore', 'dumbo', 'lure', 'turtle'];
/* The third, the same again: only in minutes both rolls before it left
well alone, so nothing either of them put in a sitting moves. */
const LATEST: EventKind[] = ['manowar'];
const GAP_MINUTES = 8;
const MAX_SECONDS = 90;

Expand All @@ -71,10 +77,18 @@ function rawAt(biome: Biome, minute: number): EventKind | null {
}

function laterAt(biome: Biome, minute: number): EventKind | null {
return rollAt(biome, minute, 'event-later', LATER);
}

function latestAt(biome: Biome, minute: number): EventKind | null {
return rollAt(biome, minute, 'event-latest', LATEST);
}

function rollAt(biome: Biome, minute: number, salt: string, kinds: EventKind[]): EventKind | null {
const zone = depthAt(minute * 60).zone;
const u = hash32(biome.key, 'event-later', minute) / 4294967296;
const u = hash32(biome.key, salt, minute) / 4294967296;
let edge = 0;
for (const kind of LATER) {
for (const kind of kinds) {
const spec = EVENTS[kind];
if (!spec.zones.includes(zone)) continue;
edge += spec.perHour / 60;
Expand All @@ -95,7 +109,7 @@ export interface OceanEvent {
/** The event that begins in a given minute, if any. */
export function eventAtMinute(biome: Biome, minute: number): OceanEvent | null {
if (minute < 1) return null;
const kind = rawAt(biome, minute) ?? laterEventAt(biome, minute);
const kind = rawAt(biome, minute) ?? laterEventAt(biome, minute) ?? latestEventAt(biome, minute);
if (!kind) return null;
// Raw hits, not kept ones, so the rule never needs to look further back.
if (rawAt(biome, minute)) for (let m = Math.max(1, minute - GAP_MINUTES); m < minute; m++) if (rawAt(biome, m)) return null;
Expand All @@ -112,6 +126,15 @@ function laterEventAt(biome: Biome, minute: number): EventKind | null {
return kind;
}

/** The third roll's event, kept only with both rolls before it quiet for the gap either side (their raw hits, kept or not). */
function latestEventAt(biome: Biome, minute: number): EventKind | null {
const kind = latestAt(biome, minute);
if (!kind) return null;
for (let m = Math.max(1, minute - GAP_MINUTES); m <= minute + GAP_MINUTES; m++) if (rawAt(biome, m) || laterAt(biome, m)) return null;
for (let m = Math.max(1, minute - GAP_MINUTES); m < minute; m++) if (latestAt(biome, m)) return null;
return kind;
}

/** The first event of a kind at or before `t`, for the ones that stay once they arrive. */
export function firstEventUpTo(biome: Biome, kind: EventKind, t: number): OceanEvent | null {
for (let m = 1; m * 60 <= t; m++) {
Expand Down
17 changes: 15 additions & 2 deletions lib/ocean/ocean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,16 +252,29 @@ test('kelp only slides up the page, and is gone by the end of the sunlit water',

test('the later sightings all turn up, and the first roll never hears of them', () => {
const seen = new Set<EventKind>();
for (let k = 0; k < 600 && seen.size < 9; k++) {
for (let k = 0; k < 600 && seen.size < 10; k++) {
for (const e of eventsUpTo(rollBiome(oceanKey('later', k), courseKey('later')), 4 * 3600)) seen.add(e.kind);
}
for (const kind of ['turtle', 'siphonophore', 'lure', 'dumbo', 'whalefall'] as EventKind[]) assert.ok(seen.has(kind), `no ${kind} in 600 sittings`);
for (const kind of ['turtle', 'siphonophore', 'lure', 'dumbo', 'whalefall', 'manowar'] as EventKind[]) assert.ok(seen.has(kind), `no ${kind} in 600 sittings`);
// Golden: a first-roll event that was there before the later roll existed is still there.
const biome = rollBiome(oceanKey('gold', 1), courseKey('gold'));
const first = eventsUpTo(biome, 4 * 3600).filter((e) => ['whale', 'storm', 'eye', 'leviathan'].includes(e.kind));
for (const e of first) assert.equal(eventAtMinute(biome, Math.floor(e.start / 60))?.kind, e.kind);
});

test('a man o\' war only drifts over in the sunlit water, and never near another sighting', () => {
let seen = 0;
for (let k = 0; k < 400; k++) {
const all = eventsUpTo(rollBiome(oceanKey('war', k), courseKey('war')), 3 * 3600);
for (const e of all.filter((v) => v.kind === 'manowar')) {
seen++;
assert.ok(e.start < 15 * 60, `a man o' war at ${e.start}s`);
for (const o of all) if (o !== e) assert.ok(Math.abs(o.start - e.start) > 7 * 60, `a ${o.kind} within ${Math.abs(o.start - e.start)}s of a man o' war`);
}
}
assert.ok(seen > 20, `only ${seen} men o' war in 400 sittings`);
});

test('a whale fall stays on the floor once it has come', () => {
for (let k = 0; k < 400; k++) {
const biome = rollBiome(oceanKey('fall', k), courseKey('fall'));
Expand Down
Loading
Loading