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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ a day and also tells you the time and the weather.
older pet stays at the back, comes out to play and to teach the young one a
trick, sleeps next to it at night, and swaps places with a tap.
- **It is forgiving.** The pet never dies. Neglect makes it hungry, sad or
sick, and that can always be fixed.
sick, and that can always be fixed. Away for longer, it stays at Grandma's
until you fetch it.
- **Quiet at night.** The panel dims or switches off in the hours you choose,
and the pet sleeps.
- **English and German**, and further languages can be added with one file.
Expand Down
14 changes: 10 additions & 4 deletions app/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
"no_game": "Gerade läuft kein Spiel.",
"away": "{name} ist unterwegs.",
"not_away": "{name} ist doch daheim.",
"at_home": "{name} ist doch hier und nicht bei Oma.",
"too_young": "{name} ist noch zu klein für Ausflüge.",
"sick_trip": "{name} ist krank und bleibt lieber daheim.",
"hungry_trip": "{name} ist zu hungrig für einen Ausflug.",
Expand Down Expand Up @@ -259,6 +260,10 @@
"evolve_hold": "Gedrückt halten, bis der Knopf gefüllt ist",
"evolved": "{name} {what}",
"recall": "🏠 Heimrufen",
"grandma": {
"fetch": "🏡 Bei Oma abholen",
"info": "{name} ist bei Oma."
},
"unpack": "🎒 Auspacken",
"replay": {
"watch": {
Expand Down Expand Up @@ -340,7 +345,7 @@
"save": "Speichern"
},
"welcome": {
"text": "{name} war {time} allein und hat etwas zu erzählen:",
"text": "Nach {time} allein hat {name} etwas zu erzählen:",
"ok": "Schön"
},
"story": {
Expand Down Expand Up @@ -397,8 +402,8 @@
"adult": "Ich bin groß geworden – jetzt bin ich ein {species}!"
},
"postcard": {
"one": "Hallo!\n{name} war einen Tag bei mir, während das Display aus war. {did} Jetzt ist es wieder zu Hause – es hat dich vermisst!\nLiebe Grüße, deine Oma",
"other": "Hallo!\n{name} war {n} Tage bei mir, während das Display aus war. {did} Jetzt ist es wieder zu Hause – es hat dich vermisst!\nLiebe Grüße, deine Oma"
"one": "Hallo!\n{name} war einen Tag bei mir. {did} Jetzt ist es wieder zu Hause – es hat dich vermisst!\nLiebe Grüße, deine Oma",
"other": "Hallo!\n{name} war {n} Tage bei mir. {did} Jetzt ist es wieder zu Hause – es hat dich vermisst!\nLiebe Grüße, deine Oma"
},
"grandma": {
"bake": "Wir haben zusammen Kekse gebacken.",
Expand Down Expand Up @@ -532,7 +537,8 @@
"pet": "gestreichelt",
"medicine": "Medizin",
"heart": "Herz aufgefangen ❤",
"away": "war bei Oma (Pi war lange aus)",
"away": "war bei Oma",
"grandma": "von Oma abgeholt",
"new_egg": "ein neues Ei ist da",
"start": "Links oder rechts? gespielt",
"unpack": "Rucksack ausgepackt",
Expand Down
12 changes: 9 additions & 3 deletions app/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
"no_game": "There's no game going on.",
"away": "{name} is out and about.",
"not_away": "{name} is right here at home.",
"at_home": "{name} is right here, not at Grandma's.",
"too_young": "{name} is still too little for trips.",
"sick_trip": "{name} is sick and would rather stay home.",
"hungry_trip": "{name} is too hungry for a trip.",
Expand Down Expand Up @@ -259,6 +260,10 @@
"evolve_hold": "Keep holding until the button fills up",
"evolved": "{name} {what}",
"recall": "🏠 Call home",
"grandma": {
"fetch": "🏡 Fetch from Grandma's",
"info": "{name} is staying at Grandma's."
},
"unpack": "🎒 Unpack",
"replay": {
"watch": {
Expand Down Expand Up @@ -397,8 +402,8 @@
"adult": "I've grown up – I'm a {species} now!"
},
"postcard": {
"one": "Hello!\n{name} stayed with me for a day while the display was off. {did} Now it's back home – it missed you!\nLove, Grandma",
"other": "Hello!\n{name} stayed with me for {n} days while the display was off. {did} Now it's back home – it missed you!\nLove, Grandma"
"one": "Hello!\n{name} stayed with me for a day. {did} Now it's back home – it missed you!\nLove, Grandma",
"other": "Hello!\n{name} stayed with me for {n} days. {did} Now it's back home – it missed you!\nLove, Grandma"
},
"grandma": {
"bake": "We baked cookies together.",
Expand Down Expand Up @@ -532,7 +537,8 @@
"pet": "petted",
"medicine": "Medicine",
"heart": "caught a heart ❤",
"away": "stayed at Grandma's (the Pi was off for a long time)",
"away": "stayed at Grandma's",
"grandma": "fetched from Grandma's",
"new_egg": "a new egg arrived",
"start": "played Left or right?",
"unpack": "unpacked the backpack",
Expand Down
10 changes: 9 additions & 1 deletion app/pet.html
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@
<p class="next" id="trip-info" hidden></p>
<div class="actions">
<button class="soft" id="recall" hidden data-t="page.recall"></button>
<button class="soft" id="grandma" hidden data-t="page.grandma.fetch"></button>
<button class="soft" id="unpack" hidden data-t="page.unpack"></button>
<button class="soft" id="watch" hidden></button>
<button class="soft" id="collect" hidden data-t="page.collect"></button>
Expand Down Expand Up @@ -910,6 +911,12 @@ <h2 data-t="page.tabs.more"></h2>
$("trip-info").hidden = false;
$("trip-info").textContent = s.asleep ? t("page.trip.full_asleep") : t("page.trip.back", { name: s.name });
}
// at Grandma's (nobody was there for a long time): it waits there to be fetched
if (s.grandma) {
$("trip-info").hidden = false;
$("trip-info").textContent = t("page.grandma.info", { name: s.name });
}
$("grandma").hidden = !s.grandma;
$("unpack").hidden = !s.backpack;
$("unpack").disabled = !s.actions.unpack;
// the scuffles of the last trip, until they have been watched or the next trip sets off
Expand Down Expand Up @@ -949,7 +956,7 @@ <h2 data-t="page.tabs.more"></h2>

// Sleeping: what the button does is what it says
const sleep = $("sleep");
sleep.hidden = egg || !!s.trip;
sleep.hidden = egg || !!s.trip || !!s.grandma;
sleep.innerHTML = s.asleep ? svg("sun") + `<span>${t("page.wake")}</span>` : svg("moon") + `<span>${t("page.sleep")}</span>`;
sleep.disabled = !(s.asleep ? s.actions.wake : s.actions.sleep);
sleep.classList.toggle("wanted", !s.asleep && s.needs.includes("energy"));
Expand Down Expand Up @@ -1915,6 +1922,7 @@ <h2 data-t="page.tabs.more"></h2>
before = current;
intoBag(await act("pickup"), "page.picked_bag", "page.picked_fruit");
});
$("grandma").addEventListener("click", () => act("grandma"));
$("recall").addEventListener("click", async () => {
const r = await act("trip", "recall");
if (r && r.accepted) say(t("page.trip.heading_home", { name: r.state.name }));
Expand Down
2 changes: 1 addition & 1 deletion core/src/companion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function outWindow(t: number): [number, number] {
* comes first. Returns "awake", "asleep" (the two sleep side by side) or null.
*/
export function out(front: PetState, t: number, friend: boolean): "awake" | "asleep" | null {
if (friend || front.trip || front.stage === "egg") {
if (friend || front.trip || front.grandma || front.stage === "egg") {
return null;
}
if (front.asleep) {
Expand Down
130 changes: 109 additions & 21 deletions core/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ import * as trip from "./trip";
import * as wild from "./wild";

export const TICK = 60;
export const MAX_CATCH_UP = 24 * 3600; // off for longer → the pet was "at Grandma's"
// After a break the simulation catches up at most this far; by then a pet that somebody had
// looked after has long gone to Grandma's (GRANDMA_AFTER), and one that nobody ever did goes there
export const MAX_CATCH_UP = 3 * 86400;
// Nobody has done anything with it for this long: it goes to Grandma's and waits there, frozen,
// until somebody fetches it (action "grandma"), so that a week away doesn't leave it sick for days.
// Longer than a day, so that an owner who comes once a day keeps looking after it themselves;
// shorter than it takes to get sick without care.
export const GRANDMA_AFTER = 36 * 3600;
// Grandma looks after it: it comes home with at least this much of every stat
export const GRANDMA_STATS = 50.0;
export const CRITICAL = 25;

// Change per hour: [awake, asleep]
Expand All @@ -48,7 +57,7 @@ const TRAIN_KEYS = battle.SKILLS.map((skill) => `train_${skill}`);
export const EVENT_LIMIT = 100;

export const ACTIONS = ["feed", "play", "sleep", "wake", "clean", "pet", "medicine", "evolve", "game", "trick",
"trip", "plant", "wear", "deco", "unpack", "train", "collect", "unwrap", "pickup", "look_in"];
"trip", "plant", "wear", "deco", "unpack", "train", "collect", "unwrap", "pickup", "look_in", "grandma"];
export const FOODS = ["meal", ...food.EDIBLE];
export const GAME_MOVES = ["start", "left", "right"];
export const TRIP_MOVES = [...trip.AREAS, "recall"];
Expand All @@ -64,7 +73,7 @@ export const ACTIVITY_SECONDS: Record<string, number> = {
egg_wobble: 2, hatch: 6, evolve: 8, game_wait: care.GAME_TIMEOUT, game_turn: 2.5,
trick: 4, build: 10, throw_meal: THROW_SECONDS + 6, throw_snack: THROW_SECONDS + 4,
trip_start: 4, trip_home: 4, unpack: 6, plant: 3, pick: 4, collect: 4, unwrap: 4,
discover: 4, pickup: 4,
discover: 4, pickup: 4, grandma_go: 5, grandma_home: 4,
train_strength: 5, train_speed: 3, train_smarts: 3,
// waiting for the signal, and showing the sequence: as long as the longest one can take
train_ready: battle.SIGNAL_DELAY[1] + battle.SIGNAL_WINDOW,
Expand Down Expand Up @@ -235,6 +244,12 @@ export function wakeUp(state: PetState, t: number, temp: number | null = null):
*/
export function tick(state: PetState, t: number, bedtime: Bedtime, weather: sky.WeatherCode = null,
temp: number | null = null, location: Location = null): void {
if (!state.grandma && leavesForGrandma(state, t)) {
goToGrandma(state, t);
}
if (state.grandma) {
return; // at Grandma's nothing happens to it: no needs, no growing (see stayAtGrandmas)
}
const rng = new Random(Math.trunc(t));
const s = state.stats;
const h = TICK / 3600;
Expand Down Expand Up @@ -361,30 +376,84 @@ export function advance(state: PetState, now: number, bedtime: Bedtime, weather:
state.updated_at = now;
return true;
}
if (now - state.updated_at > MAX_CATCH_UP) {
const days = Math.max(1, Math.round((now - state.updated_at) / 86400));
for (const k of STATS) {
state.stats[k] = Math.max(state.stats[k], 50.0);
}
state.poops = 0;
state.next_poop_at = null;
state.sick = false;
state.asleep = false;
state.trip = null;
state.updated_at = now;
// item: about how many days it was off, for Grandma's postcard
logEvent(state, now, "away", "system", { item: String(days) });
return true;
}
let changed = false;
while (state.updated_at + TICK <= now) {
const until = Math.min(now, state.updated_at + MAX_CATCH_UP);
while (!state.grandma && state.updated_at + TICK <= until) {
state.updated_at += TICK;
tick(state, state.updated_at, bedtime, weather, temp, location);
changed = true;
}
if (!state.grandma && now > until) {
// caught up as far as it goes, and still nobody there: Grandma takes it in
state.trip = null;
goToGrandma(state, state.updated_at);
}
if (state.grandma) {
stayAtGrandmas(state, now);
return true;
}
return changed;
}

// ---------- At Grandma's ----------

/**
* Does it go to Grandma's at the time t? When nobody has done anything with it for GRANDMA_AFTER
* (and it hasn't just come back from her), as soon as it is awake and at home.
*/
export function leavesForGrandma(state: PetState, t: number): boolean {
const since = Math.max(state.seen_at, state.recent.grandma?.at(-1) ?? 0.0);
return since > 0 && t - since >= GRANDMA_AFTER && !state.asleep && !state.trip;
}

/** Grandma takes it in at the time t; she also clears away what was lying around. */
export function goToGrandma(state: PetState, t: number): void {
state.grandma = t;
state.game = state.training = null;
state.poops = 0;
state.next_poop_at = null;
setActivity(state, "grandma_go", t);
}

/**
* At Grandma's, time stands still for it, as among the ancestors: it doesn't get older or grow on,
* and what it waits for (the next visit of its friend) waits too. The pet at the back goes with it
* (Pet.advance).
*/
export function stayAtGrandmas(state: PetState, now: number): void {
const gap = Math.max(0, now - state.updated_at);
state.stage_since += gap;
state.born_at += gap;
if (state.friend) {
state.friend.next += gap;
if (state.friend.until !== undefined) {
state.friend.until += gap;
}
}
state.updated_at = Math.max(state.updated_at, now);
}

/**
* Fetched from Grandma's: it comes home looked after, and Grandma writes a postcard about it
* (story.ts). `seen_at` stays, so the story tells what happened since somebody was last there.
*/
export function comeBack(state: PetState, now: number): void {
const days = Math.max(1, Math.round((now - state.grandma) / 86400));
for (const k of STATS) {
state.stats[k] = Math.max(state.stats[k], GRANDMA_STATS);
}
state.sick = false;
state.asleep = false;
state.cold_until = 0.0;
state.grandma = 0.0;
state.recent.grandma = [now];
state.idle_next = 0.0;
idle.noticed(state);
setActivity(state, "grandma_home", now);
// item: about how many days it was there, for Grandma's postcard
logEvent(state, now, "away", "system", { item: String(days) });
}

// ---------- Derived values ----------

/** Mood derived from the stats (the first match wins). */
Expand Down Expand Up @@ -420,7 +489,7 @@ export function activityOf(state: PetState, now: number): string {

/** Stats that need looking after (for the icons and the app). */
export function needs(state: PetState): string[] {
if (state.stage === "egg") {
if (state.stage === "egg" || state.grandma) {
return [];
}
const result: string[] = NEEDS.filter((k) => state.stats[k] < CRITICAL);
Expand All @@ -439,6 +508,10 @@ export function availableActions(state: PetState, now: number): Record<string, b
const away = !!state.trip;
const ready = life.isDue(state, now) && !asleep && !away;
const nothing = Object.fromEntries([...ACTIONS, "recall"].map((action) => [action, false]));
if (state.grandma) {
// at Grandma's it can only be fetched – and things at home can be arranged
return { ...nothing, grandma: true, plant: true, wear: true, deco: true, look_in: state.house > 0 };
}
if (state.stage === "egg") {
return { ...nothing, pet: true, evolve: ready };
}
Expand Down Expand Up @@ -642,7 +715,22 @@ export function applyAction(state: PetState, action: string, now: number, bedtim
throw new RequestError(check[1]);
}
const value = checkValue(action, item, options.value);
state.seen_at = now; // somebody is there (for idle.welcome)
if (action === "grandma") {
// fetched from Grandma's; somebody is there, but what happened since they were last is still to be told
const reason = state.grandma ? null : "at_home";
if (reason === null) {
comeBack(state, now);
} else {
logEvent(state, now, action, by, { accepted: false, reason });
}
return { accepted: reason === null, reason };
}
if (state.grandma && !["plant", "wear", "deco", "look_in"].includes(action)) {
comeBack(state, now); // an app from before Grandma: anything it does with the pet fetches it
}
if (!state.grandma) {
state.seen_at = now; // somebody is there (for idle.welcome)
}
if (action === "look_in") {
// only a look: it doesn't do anything with the pet, so it isn't in the history either
const reason = !state.house ? "no_house" : state.asleep ? "inside" : null;
Expand Down
4 changes: 2 additions & 2 deletions core/src/notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as model from "./model";
import type { PetState } from "./state";
import type { Location } from "./sun";

export const HORIZON = model.MAX_CATCH_UP; // longer away and it was at Grandma's anyway
export const HORIZON = 24 * 3600; // a day ahead at most (a little later it is at Grandma's anyway)
// The needs it calls for, in order of priority
export const CALLS = ["health", "satiety", "hygiene", "mood"] as const;
export type Call = (typeof CALLS)[number];
Expand All @@ -27,7 +27,7 @@ export interface Reminder {

/** The needs it would call for right now. */
export function calls(state: PetState): Call[] {
if (state.stage === "egg") {
if (state.stage === "egg" || state.grandma) { // at Grandma's it is looked after
return [];
}
return CALLS.filter((need) => need === "health" ? state.sick : state.stats[need] < model.CRITICAL);
Expand Down
Loading
Loading