Skip to main content

Animals

Animals

Status: IN PROGRESS — Tracked open issues; see Known Issues section

Last updated: October 28, 2026

Zoo Simulator -- Animals & Health Subsystem

Status: IN PROGRESS — Tracked open issues; see Known Issues section

Deep-dive into animal species, health mechanics, aging, breeding, genetics, training, and enrichment. All formulas and constants extracted from Java source code.


Animal Model

Source: llitd.model.zoosimulator.Animal

Each animal has:

Field Type Range Notes
health Integer 0-100 Death at 0
happiness Integer 0-100 Affected by habitat quality + enrichment
age Integer 0-999 In months. 1 game day = 1 month
growthStage GrowthStage enum Derived from age
sex Sex MALE/FEMALE Set at birth/purchase
personality Personality 7 types SHY, PLAYFUL, AGGRESSIVE, CALM, CURIOUS, LAZY, ENERGETIC
species Species the constants of llitd.model.zoosimulator.enums.Species Base species enum
generation Integer 1+ 1 = purchased, 2+ = bred
trainingLevel Integer 1-5 Advances every 3 completed behaviors
enrichmentBonus Integer 0+ Sum of all active enrichment bonuses
visitorAppeal Long 0+ Computed from traits, rarity, training
marketValue Long 0+ Computed from species price, rarity, traits, generation

Species Metadata — DB-Backed

Source: zoo_simulator.species_defs + zoo_simulator.personality_defs tables, served via (Caffeine cache, 1-hour TTL).

Prior to these values lived in hardcoded Java switch statements inside (base value, personality multiplier, compatible habitats) and (gestation, cooldown, offspring count). They now live with the designers.

`zoo_simulator.species_defs`

One row per base species. The count is the constants of llitd.model.zoosimulator.enums.Species and the rows of zoo_simulator.species_defs; this doc does not restate it. New species arrive as an append to that enum and a migration, and the table wins where a file and the table differ. The petting-zoo, nocturnal, ape and Zoo Bucks snow leopard rows (display name Common Snow Leopard) are data/exports/zoo-follow-ups-continued/species_defs-additions.jsonl until that migration is applied.

Column Type Purpose
species ENUM PK The constants of Species (LION, TIGER, …)
display_name VARCHAR(100) Voice-facing — Alexa reads this aloud
base_value BIGINT Used by calculateAnimalValue and calculateVisitorAppeal (÷10)
compatible_habitats JSON array HabitatType names that match this species' preferred theme. **Advisory since ** — any species can be placed in any standard habitat; a match grants the soft Habitat Theme Bonus and steers auto-placement, it no longer gates placement
gestation_days INT Days between startBreeding and deliverOffspring
cooldown_days INT Days after delivery before the pair is removed
offspring_min / offspring_max INT Inclusive bounds — rollExpectedOffspring picks nextInt(min, max + 1)

`zoo_simulator.personality_defs`

One row per Personality enum value. 7 rows seeded.

Column Type Purpose
personality ENUM(7) PK Personality enum value
display_name VARCHAR(50) Voice-facing label
value_multiplier DOUBLE Multiplier applied in calculateAnimalValue and calculateVisitorAppeal

Tuning

Edit the DB row and wait up to 1 hour for cache TTL to expire, or call . No code deploy needed for species tuning.


Habitat Theme Bonus — Soft Placement Model (feedback 7354)

Themes are a soft incentive, not a hard placement gate. Player feedback 7354 asked "animals can go anywhere, why are there themes?" — the old model threw a compatibility error when a species was placed in a habitat whose HabitatType was not in its compatible_habitats, which made themes feel like an arbitrary lock. The model is now: any animal can live in any standard habitat; matching the habitat theme grants a happiness bonus, and an off-theme placement is allowed with no penalty.

Not to be confused with the Zone Theme Coherence Bonus (, simulation.md) — that is a separate, already-soft system that rewards curating a ZoneTheme's matching species with extra visitor morale. This section is about habitat-type placement (SAVANNA, ARCTIC, …), which used to be a hard gate and is now a happiness bonus. The two reward thematic play through different channels (animal welfare vs visitor satisfaction).

What "theme match" means

A placement is on-theme when the habitat's HabitatType is one of the species' compatible_habitats (DB-backed, served by ). compatible_habitats is now advisory: it drives the bonus and the auto-placement preference, not a gate.

The bonus

Property Value
Trigger An animal is placed (buy or move) into a habitat whose type matches its preferred theme
Effect One-time +10 happiness (), capped at the 100 ceiling
Knock-on Happiness feeds calculateVisitorAppeal and calculateAnimalValue (both fold health+happiness into a 0.5–1.5 statsMultiplier), so a theme match measurably raises visitor appeal and market value
Ongoing The daily updateAnimalStats already adds +2 happiness/day for an animal in a compatible habitat — the placement bonus is the immediate lift, the daily +2 is the standing incentive to keep animals on-theme
Premium animals Acquired at 100 happiness with their own appeal bonus, so the placement bonus is a no-op for them — it is skipped at the cap, which preserves their special visitorAppeal instead of recomputing it

The bonus is bounded by the 100 happiness ceiling, so repeatedly re-placing an animal between matching habitats cannot farm happiness past full — and the daily simulation already lifts a well-kept animal toward that ceiling regardless of theme.

Placement is never blocked on theme

  • removed the old throw — only capacity still throws.
  • allows an off-theme move (was a hard "not compatible" block).
  • and the move flow both speak a theme line: on-theme → "Theme match … happiness bonus"; off-theme → "Off theme placement … miss the theme happiness bonus, but they still live here happily." Voice-output rules apply (no parens, brackets, slashes, or raw enum names).
  • Auto-placement (ZooHabitatSelector.findTargetHabitat) still prefers a compatible habitat when no explicit habitat is named, then falls back to any habitat with space — so a default buy still lands on-theme when one is available.

Premium / fantasy zones — also soft (decision)

Premium animals map to a base Species that is habitat-compatible by construction (economy.md Premium Animal placement table) and are acquired at full happiness, so they neither hit the old gate nor need the bonus. There is no separate premium/fantasy-zone placement gate in the standard habitat system, and none is added — the soft model is uniform across standard and premium animals. (Fantasy zones remain a Golden-Ticket purchase with their own appeal multiplier; that is a zone-cost mechanic, not a per-animal placement gate.)


Base Species

NLU recognition for non-species names: The game accepts common animal names not in the enum (jaguar, leopard, panther, bat, etc.) by mapping them to the closest species in ZooPurchaseValidator and to the most plausible habitat type in . This makes "build habitat jaguar" create a JUNGLE habitat and "buy animal leopard" purchase a TIGER. (player feedback 229-231). Cheetah is no longer mapped to TIGER — it is its own species as of ; jaguar/leopard/panther still map to TIGER. Synonyms added by 21673: buffalo to BISON, orca to WHALE (both in ZooPurchaseValidator and the canonical ). Speech hears otter as order and polar bear as bipolar, so the species slot of a buy and of a breeding command reads those words in : order and orders mean OTTER, and bipolar means POLAR_BEAR. Buy animal order, buy 2 orders, and start breeding order name the otter. When the cross-game buy catches the phrase first, reads that same buy slot, so buy 2 orders is purchased and is not refused as unavailable in Zoo Simulator. Feed animals in order and view animals in order name none, because feed and view read extractSpecies, which does not carry this alias. A species word still wins, so start breeding lions in order stays the lion. Bat is its own species, not a mapped name. The same alias pass reads bunny and bunnies as RABBIT and lamb and lambs as SHEEP; sheep is its own plural and is read as the whole word. Snow leopard stays the Golden Ticket premium animal: its display name wins the premium pass, so "buy snow leopard" opens the premium buy and never reaches the bare leopard to TIGER mapping. The Zoo Bucks SNOW_LEOPARD is said "common snow leopard" or "standard snow leopard", while "snow leopard" stays the Golden Ticket premium animal. A premium display name that is also a base species name, said after common or standard, is not the premium (), so the buy redirect, the sell refusal and the species passes agree on it, and the game speaks the species that way in its options and purchase lines (, read by ZooLookup.spokenSpecies).

View filter (feedback 424): accepts premium-name / species / habitat-type / zone-theme hints after the verb — view animals lion, view animals white tiger, view animals savanna, view animals africa. Resolution order: PremiumAnimal display name first (longest-match, hyphen-normalized), then base species (exact or fuzzy via ), then habitat type (with woodland/rainforest/tundra synonyms), then zone theme. Bare view animal / view animals still returns the full roster. Backend opening-only pattern view animals consumes the biome/species/premium suffix as the extracted value.

Premium-name precedence (generalization of feedback 1062): Multi-word PremiumAnimal display names (WHITE_TIGER "white tiger", SABER_TOOTH_TIGER "saber-tooth tiger", GOLDEN_EAGLE "golden eagle", PHILIPPINE_EAGLE "philippine eagle", GIANT_PANDA "giant panda") shadow the single-word base Species substrings (TIGER, EAGLE, PANDA). returns a sealed SpeciesMatch union — SpeciesValue (base species) or PremiumValue (premium-by-display-name) — and the legacy extractSpecies returns empty when input matches a premium name so callers cannot silently fall through. Every freeform-input handler (feed / view / view-genetics / breed / start-breeding / train / add-enrichment / sell) consults the premium union: feed/train/enrichment/view filter to animals with that customSpeciesName; sell rejects premium animals (paid in Golden Tickets, not refunded in Zoo Bucks); breed routes to the existing premium-pair finder. Hyphen-normalized matching ensures voice transcription "saber tooth tiger" matches PremiumAnimal display name "Saber-Tooth Tiger".

Source: llitd.model.zoosimulator.enums.Species

Category Species
Mammals - Savanna/Safari LION, TIGER, ELEPHANT, GIRAFFE, ZEBRA
Mammals - Arctic/Cold POLAR_BEAR
Mammals - Forest/Jungle BEAR, PANDA, MONKEY, GORILLA
Mammals - European Woodland WOLF, DEER, FOX
Mammals - North American COYOTE, BISON
Mammals - Big Cats CHEETAH, SNOW_LEOPARD
Mammals - Large Semi-Aquatic HIPPO
Mammals - Other KANGAROO
Mammals - Small / Backyard (starter tier) SQUIRREL, RACCOON, OPOSSUM, SKUNK
Petting zoo (starter tier, under the squirrel's price) DONKEY, SHEEP, GOAT, RABBIT, DUCK
Nocturnal small mammals BAT, HEDGEHOG
Primates BABOON, ORANGUTAN
Birds EAGLE, PENGUIN, FLAMINGO, PARROT, OWL
Reptiles CROCODILE, SNAKE, TURTLE
Aquatic DOLPHIN, SHARK, SEAL, OTTER, WHALE

Species Prices (Purchase Cost)

Source: — buy cost reads species_defs.base_value via (before that the buy path charged a flat 1000 and this table was aspirational). uses the same base values for bred-offspring appraisal. Bulk buys sum the per-species price; sell-back refunds 50 percent of purchase price, floor 500. Full purchase and sale rules: economy.md Standard Animal Purchase & Sale Pricing.

Price (Zoo Bucks) Species
5,000 PANDA, TIGER, ELEPHANT, WHALE, SNOW_LEOPARD
4,500 POLAR_BEAR, GORILLA
4,000 LION, BEAR, GIRAFFE, CHEETAH, HIPPO, ORANGUTAN
3,500 SHARK, DOLPHIN
2,500 ZEBRA, KANGAROO, CROCODILE, BISON
2,000 EAGLE, PENGUIN, SEAL, MONKEY, WOLF, DEER, BABOON
1,800 OTTER, FLAMINGO
1,500 FOX, OWL, COYOTE
1,000 PARROT, SNAKE, TURTLE
600 BAT
500 HEDGEHOG
450 SKUNK
400 RACCOON
300 OPOSSUM
250 SQUIRREL
200 DONKEY
150 SHEEP, GOAT
100 RABBIT, DUCK

Species Sound Cues (SFX) — Purchase + Habitat Ambient (fb 7354)

Animals and habitats carry real audio cues. SFX is emitted inline as a <KEY>replacemewithsound token that the Alexa renderer resolves to a CDN clip at render time (mechanism + renderer rules: § Sound Effects). The token is stripped from player-facing text, so the per-species mapping is asserted by JUnit (), not a scenario_test.

Per-species purchase cue —

On a successful animal purchase the success line is prefixed with the species' cue. Most species play their own sound; a few share a close cousin's cue, and species with no sourced sound fall back to the neutral CHORD_SWELL purchase sting (an honest "purchase confirmed" tone — never the wrong animal).

Mapping Species Cue key
Own cue LION, TIGER, ELEPHANT, GORILLA, MONKEY, BEAR, POLAR_BEAR, WOLF, PENGUIN, DOLPHIN, CROCODILE, ZEBRA, GIRAFFE, KANGAROO, PANDA, DEER, TURTLE, SHARK, WHALE, HIPPO, CHEETAH, COYOTE, OWL, OTTER, SQUIRREL, RACCOON, BISON, OPOSSUM, SKUNK, DONKEY, SHEEP, DUCK, BAT, BABOON same-name SoundKey
Same animal, another game's clip GOAT WF_GOAT_BLEAT (the Willow Farm goat bleat)
Same genus SNOW_LEOPARD LEOPARD
No clip yet RABBIT, HEDGEHOG, ORANGUTAN CHORD_SWELL until an audio.sound_requests row is fulfilled; then add the case
Shared cue PARROT, FLAMINGO, EAGLE PARROT
Shared cue SEAL SEALION
Shared cue SNAKE RATTLE_SNAKE
Canid analog FOX COYOTE (same yip-bark family)
Safety-net default (any future un-cased Species) CHORD_SWELL

Every Species now resolves to its own cue; CHORD_SWELL is a safety-net default only. SQUIRREL and RACCOON use existing audio.audio_files clips; BISON, OPOSSUM and SKUNK use Soundstripe animal analogs (bison = cow-moo, opossum = cat defensive snarl, skunk = small-mammal growl), sourced via import_sfx like the CHEETAH panther analog; FOX keeps the COYOTE canid analog. A dedicated fox scream would still need its own sourcing, but Soundstripe currently has no usable clip for them. Once sourced (a SoundKey constant + audio.audio_files rows), add the case to resolveAnimalSfx.

CHEETAH cue audio is a big-cat analog (finding 759, fb 9343). The CHEETAH SoundKey maps same-name, but its audio.audio_files clips are Soundstripe panther snarl and growl recordings (a big-cat analog), not a literal cheetah recording. Soundstripe has no cheetah SFX, and a real cheetah signature call is a chirp that genuinely sounds bird-like - which was the exact player complaint. An April 2024 manual placeholder CHEETAH_tTaqp.mp3 held an actual bird vocalization, uploaded before cheetah was a playable species, so buying a cheetah played a bird. That placeholder is retired and the key now serves panther snarl and growl variants (Soundstripe 6240, 6226, 6238, normalized to short purchase-cue clips). Guarded by the repository root's scripts/regression_shield/audit_zoo_animal_sfx.py: the retired bird file may never resurface and the cue must resolve to a valid MPEG-2 small variant.

Habitat ambient bed —

view habitat N prepends a per-biome ambient loop (ZOO_AMBIENT_<BIOME>) played under voice at a low bed level — an accessibility cue so a player can tell habitats apart by sound. NURSERY reuses the HATCHERY bed; an unknown type falls back to GARDEN_AMBIENT. Regression: .

Other zoo cues

Feeding (SUCCESS), construction (CONSTRUCTION_SOUNDS), grid entry (CREAKY_DOOR), hallway movement (FOOTSTEPS_HALLWAY), and the new-director launch (GARDEN_AMBIENT).

Sourcing pipeline

All zoo SFX are sourced through /asset-curator (Soundstripe-first, ElevenLabs fallback). A cue is "done" only when BOTH the SoundKey enum constant exists AND audio.audio_files has a valid small (24 kHz MPEG-2) variant row keyed on the replace_id — a missing enum constant is invisible to game code; a missing audio_files row fails silently at runtime. As of every cue referenced by zoo code resolves to at least one valid variant (no silent-failure keys).


Growth Stages

Source: llitd.model.zoosimulator.enums.GrowthStage

Stage Age Range (months) Size Multiplier Can Breed Care Multiplier
NEWBORN 0-2 0.2x No 2.0x
INFANT 3-5 0.4x No 1.5x
JUVENILE 6-11 0.6x No 1.2x
ADOLESCENT 12-23 0.8x No 1.0x
ADULT 24-125 1.0x Yes 1.0x
ELDERLY 126-999 0.95x No 1.3x

Stage is determined by GrowthStage.fromAge(ageInMonths) -- iterates all stages, returning the first where ageInMonths >= minAge && ageInMonths < maxAge. Falls back to ELDERLY.

Growth stage transitions are processed daily in :

  • Recalculates stage from current age
  • When transitioning to ADULT, sets isBaby = false

Aging System

Source:

Constants:
  AVERAGE_LIFESPAN_MONTHS = 180        (15 years of game time)
  ELDERLY_THRESHOLD = 0.7              (70% of lifespan)
  ELDERLY_AGE_MONTHS = 126             (floor(180 * 0.7) -- elderly stage onset)
  SENESCENCE_RAMP_MONTHS = 54          (180 - 126 -- elderly threshold to lifespan)
  SENESCENCE_DECLINE_AT_LIFESPAN = 4   (HP/day decline once at the average lifespan)
  ELDERLY_ILLNESS_MULTIPLIER = 1.5     (50% more likely to get sick)

Decline to death at lifespan (human_question 22000)

Owner decision (human_question 22000, topic zoo_elderly_lifecycle): elderly animals decline to death around the average lifespan so the breeding loop no longer silts up with immortal, sterile elderly. AVERAGE_LIFESPAN_MONTHS = 180 is now meaningful. Two changes make it real:

  1. Age-scaled senescence decline. The elderly daily health decline is no longer a flat -2 -- it scales with how far past the elderly threshold the animal has aged:

    monthsElderly  = age - 126               (>= 0)
    senescence     = monthsElderly / 54      (0.0 at 126, 1.0 at 180, >1 beyond)
    declinePerDay  = round(4 * senescence)   (0 at the threshold, ~4 at lifespan, growing beyond)
  2. Elderly forgo natural recovery. The +2/day natural recovery (Natural Health Recovery, below) applies only to non-elderly animals -- an aged body no longer bounces back, so the senescence decline is the elderly animal's net daily trajectory. This is the load-bearing half of the fix: previously the +2 recovery exactly offset the -2 elderly decline, so a well-vetted elderly animal sat at net zero and lived forever.

Trajectory. A fully healthy animal entering the elderly stage at 100 health declines slowly at first (about 0/day at 126, 1/day by ~133, 3/day by ~160, 4/day by ~174) and reaches 0 in the late 170s of months -- i.e. it passes near the 180-month average lifespan. Untreated illness brings the end sooner; excellent care (vets keeping it illness-free) lets it live out close to the full span before senescence wins. Beyond 180 the decline keeps growing, so no animal is immortal.

Daily aging flow (in performDailyHealthCheck):

  1. animal.age += 1 (1 day = 1 month)
  2. If age >= 126: apply elderly senescence decline (health -= round(4 * (age-126)/54), clamped at 0)
  3. Non-sick, non-elderly animals get +2 natural recovery; elderly animals do not
  4. Elderly animals have 1.5x illness chance multiplier
  5. Death at health <= 0. A death while not sick (elderly senescence) is an old-age passing; a death while sick is an illness death (see Death section + voice copy below)

Monotonic State Persistence Guard (fb 9407 / report 162 F3 / finding 811)

Aging and pregnancy are monotonic across saves: an animal's age only ever increases and an active GESTATING pregnancy only leaves the breeding-pairs list by delivering (which flips it to COOLDOWN) or by losing a parent, which the daily tick removes (§ Breeding Flow). Player feedback 9407 reported the opposite — a zebra pregnancy "due in 9 days" read as gone and baby elephants dropped "back down to 3 months". No aging code writes age backward; the cause is a stale in-memory account clobbering a fresher persisted copy at turn-exit save. When a second request (a concurrent surface, a rapid re-issue, or an out-of-band cache write such as an ISP grant) advances the persisted account after this turn loaded, the whole-graph overwrite at save time reverts that fresher state. It is the same clobber the starter-Golden-Ticket reconcile already patched for one currency field, surfacing on the two monotonic parts of the graph a player watches.

now reconciles the outgoing account against the freshest persisted snapshot before overwriting it (ZooAccountRegressionGuard.reconcile): it restores any animal age a stale copy rolled below the persisted value and re-adds any GESTATING pair the stale copy dropped while it still holds both of that pair's parents. It never removes anything: a real sale/removal drops the id and is honoured, and a resetZoo persists an empty account before the guard ever compares. A pair whose parent the outgoing copy no longer holds was removed by the daily tick, and restoring it would bring back a pregnancy with no mother or father. Read-only turns skip the save entirely, so the guard only costs a snapshot read on turns that mutate. Guarded by JUnit (the deterministic red-before proof) plus scenario_test 2582 (a forward-progression end-to-end guard for the pregnancy timer).


Health System

Source:

Illness Probabilities

BASE_ILLNESS_CHANCE = 0.05           (5% per animal per day)
LOW_HEALTH_ILLNESS_MULTIPLIER = 2.0  (if health < 50)
NO_VET_ILLNESS_MULTIPLIER = 1.5     (if zoo has 0 veterinarians)
ELDERLY_ILLNESS_MULTIPLIER = 1.5     (if animal is elderly)

Compounding: All multipliers stack multiplicatively.

illnessChance = 0.05
if health < 50:  illnessChance *= 2.0
if vets == 0:    illnessChance *= 1.5
if elderly:      illnessChance *= 1.5

Worst case (elderly, low health, no vets): 0.05 * 2.0 * 1.5 * 1.5 = 0.225 (22.5%)

Illness Severity Assignment

Source:

All new illnesses always start as MILD, regardless of the animal's current health. This design prevents instant death spirals for low-health animals and gives players a chance to respond. Escalation to higher severity happens via progressIllness() if left untreated.

Escalation path: MILD (3 untreated days) → MODERATE → SEVERE (8 total sick days from illness start)

Illness names by severity (assigned when reaching that severity):

Severity Illnesses
MILD Minor Infection, Mild Fever, Digestive Upset, Minor Injury
MODERATE Respiratory Infection, Bacterial Infection, Severe Fever
SEVERE Critical Infection, Life-Threatening Illness

Treatment Costs and Recovery

Severity Treatment Cost Untreated Decline/Day Treated Recovery/Day Cure Threshold
MILD 500 -3 +10 >= 90 HP
MODERATE 1,500 -7 +7 >= 75 HP
SEVERE 3,000 -15 +5 >= 60 HP

Vet/Sickness Rebalance (fb 5739)

Customer pain. Player AMAY26SP… reported "Im doing everything I can but it seems like no matter how many vets I hire my animals still get sick". The constant trickle of sickness alerts felt punishing even with a fully-staffed vet pool. Audit of the old math confirmed the complaint.

Before (broken). Sickness chance had a hard-threshold sufficient-vet multiplier: 0.6x when vets * 5 >= animalCount, no modifier between 0 and that threshold. With 20 animals and 4 vets (capacity 20, ratio 1.0), per-animal daily illness was 0.05 × 0.6 = 0.03 → ~1 new sickness every 1.7 days across the zoo. That triggered the sick-animals alert almost every turn even with full coverage. With 10 animals and 1 vet (capacity 5, ratio 0.5), no multiplier kicked in at all → 0.05 per animal per day → ~1 new sickness every 2 days. Hiring vets short of the hard threshold gave zero preventative benefit.

After (graduated). Replace the threshold with a continuous capacity ratio:

coverage      = (vets * 5) / max(1, animalCount)
vetMultiplier = vets == 0     ? 1.5
              : coverage <= 1 ? 1.0 - coverage * 0.7
              :                 max(0.05, 0.30 / coverage)

Past full coverage, extra vets keep lowering illness, down to a 0.05x floor. With the curve capped at full coverage, a zoo of a hundred animals met a new sick animal about every day however many vets it hired, which is the "sick every day" report the vets are meant to answer. .vetIllnessMultiplier(int, long) is the definition; where this block and the code differ, the code is right.

Scenario Vets Animals capacityRatio vetMultiplier Per-animal daily illness Approx zoo-wide rate
No vets 0 10 — 1.5x 0.075 ~1 every 1.3 days
Partial vet 1 10 0.50 0.65x 0.0325 ~1 every 3 days
Full coverage 2 10 1.00 0.30x 0.015 ~1 every 6.7 days
Over-staffed 4 10 2.00 0.15x 0.0075 ~1 every 13 days
Big zoo full 4 20 1.00 0.30x 0.015 ~1 every 3.3 days
Big zoo partial 2 20 0.50 0.65x 0.0325 ~1 every 1.5 days
Hundred animals, double staffed 40 100 2.00 0.15x 0.0075 ~1 every 1.3 days

Effect on the player. With full coverage on a 10-animal zoo, sickness arrives roughly every week instead of every other day — alert-fatigue drops by 3x while not eliminating challenge. The "no matter how many vets" frustration disappears because every vet hired beyond zero now produces a measurable reduction (linear in capacity ratio, capped at full coverage at 0.30x). Partial coverage is no longer a dead zone.

Untouched. Low-health (×2.0) and elderly (×1.5) multipliers still stack on top. Worst case (elderly, low-health, no vets): 0.05 × 2.0 × 1.5 × 1.5 = 22.5% — unchanged from the prior worst case. The change only flattens the "vets help" curve; it does not soften any of the other illness drivers.

Code. switches from the SUFFICIENT_VET_ILLNESS_MULTIPLIER hard-threshold to the graduated formula above. The SUFFICIENT_VET_ILLNESS_MULTIPLIER and NO_VET_ILLNESS_MULTIPLIER constants are retained: NO_VET still applies the 1.5x penalty when vets == 0, and the new graduated path replaces the sufficient-vet branch.

Voice surface. view staff will now report effective coverage: Your veterinarians cover 100% of your animals — illness chance reduced 70%. So the player can see why their bill is paying off. view sick animals also gets a single-line coverage hint when vets > 0.

Veterinary Auto-Treatment

Source:

maxTreatments = veterinarians * VET_DAILY_TREATMENT_CAP

VET_DAILY_TREATMENT_CAP in is the definition of the per-vet cap; where this page and the constant differ, the constant is right.

  • Streams all animals in zoo
  • Filters to sick + not yet treated today
  • Treats up to maxTreatments animals
  • Each treatment: deducts cost from zooBucks, applies recovery rate, checks if cured

Natural Health Recovery

Source:

Healthy (non-sick) animals regain +2 HP/day, capped at 100 -- except elderly animals (age >= 126), whose bodies no longer recover so the age-scaled senescence decline (Aging System above) can carry them to death near the average lifespan.

Death

Animals die when health <= 0. They are removed from their habitat immediately via iterator removal during the daily health check, freeing the slot for a new animal.

Old-age vs illness death (human_question 22000). The cause is attributed by whether the animal was sick at the start of the tick: a non-sick elderly animal driven to 0 by senescence passes of old age; an animal that was already sick and dies is an illness death. (A non-elderly non-sick animal can never lose health, so any non-sick death is necessarily elderly senescence.) The day summary speaks the two differently:

  • Old-age passing (, fed by DailySimulationResult.oldAgeDeaths): a gentle, non-punitive farewell -- "A gentle farewell. {name} lived a long, full life and passed away peacefully of old age. Their habitat is now open for a new animal." Aging to the end of a full life is the intended lifecycle, not a player mistake.
  • Illness death: keeps the "Sad News" framing -- "{name} has passed away." -- which nudges better care.

Daily Health Check Flow

Source:

For each animal in every habitat across all zones:

Before the health check: autoTreatWithVets() runs first (phase 2 in ), treating up to 8 sick animals per vet and setting treatedToday on each.

1. ageAnimal(animal)              -- age += 1
2. if isElderly(animal):
     applyElderlyEffects(animal)  -- health -= 2
3. if animal.isSick:
     progressIllness(animal)      -- days_sick++; if NOT treatedToday: health declines, severity may escalate
   else:
     if checkIfAnimalGetsSick():
       makeAnimalSick(animal)     -- assign severity + illness name
4. if !animal.isSick:
     improveHealth(animal)        -- +2 HP natural recovery
5. animal.treatedToday = false    -- cleared AFTER progressIllness reads it, ready for tomorrow's vets
6. if animal.health <= 0:
     remove from habitat          -- animal dies

A treated animal gains its recovery rate (+10 / +7 / +5) and neither declines nor escalates that day, so vet care is net positive at every severity and a SEVERE animal from 40 HP reaches the 60 HP cure threshold in four days. An animal the vets could not reach (no vet, cap exhausted, or no Zoo Bucks) declines by its untreated rate (-3 / -7 / -15) and escalates on schedule.


Breeding System

Source:

Breeding Eligibility

Source:

An animal can breed if ALL of these are true:

  1. Growth stage has canBreed == true (ADULT only, ages 24-125)
  2. breedingCooldownDays is null or <= 0
  3. health >= 30
  4. isSick != true

Breed-Failure Diagnosis (voice copy)

Source: (per-species) and diagnoseZooWideNoBreeding (the "breed" command with no species).

When a species the player owns has NO eligible pair, the breed-failure voice copy DIAGNOSES the actual situation instead of restating the requirement. A mature zoo whose whole herd has aged out reads to a voice-only player as "I have hundreds of lions but cant breed them" -- they cannot see that every animal is ELDERLY, so they invent a sex-distribution theory (player feedback 8837-8840). The diagnosis counts the species' animals by effectiveStage (stored growthStage, else GrowthStage.fromAge), in priority order:

  1. Owns none -> buy an adult male and female at the market.
  2. All elderly (no adults, no juveniles) -> "All N of your lions are elderly and too old to breed. ... Buy young adult lions at the market ... A lion from the market is a young adult and can breed the same day." This is the aged-out dead-end. Recovery is immediate because assigns a young adult age, ZooLifeStage.MARKET_MIN_AGE_MONTHS up to ZooLifeStage.YOUNG_ADULT_UNTIL_MONTHS (always ADULT, with its whole adult life ahead).

The sex of a bought animal. A buy that names a sex ("buy a female monkey", "buy 2 male lions") buys every animal of that sex, read as a whole word by . A buy of two or more that names none gets a guaranteed male and female first, and the rest at random. A single buy says the sex it bought: "Purchased 1 monkey, a female." 3. No adults, mix of elderly + juveniles -> some too old, some too young; buy young adults or wait. 4. No adults, only juveniles -> not old enough yet; reach breeding age as adults at two years old. 5. Adults exist but no pair -> lone sex, then ready adults of both sexes living apart, then the held-back sex's cooldown or health, each named specifically. A ready adult is healthy and off cooldown. Ready males and females with no pair between them are apart, and that is said before any cooldown: a zoo that keeps breeding always has some pair cooling, and naming it would hide the ready animals that only need moving together. Cooldown is named only when a sex has no ready adult and some of its adults are cooling; otherwise health is.

The same aging facts surface in (breeding help lists all six growth stages including ELDERLY and states adults breed from two until about ten and a half years old).

RESOLVED (human_question 22000): elderly decline to death at lifespan. The owner chose option (b). Elderly animals are no longer immortal -- they take an age-scaled senescence decline and forgo natural recovery, so a herd that aged out together now declines to death near AVERAGE_LIFESPAN_MONTHS = 180, clearing habitat slots and unsilting the breeding loop. The aged-out diagnostic copy still teaches "sell elderly, buy young" as the fast recovery, but elderly herds now also self-clear over time. See the Aging System section above for the math and the gentle old-age passing voice copy.

Pair Formation

Source:

  • Scans all habitats for animals of the requested species
  • Both animals must be in the same habitat
  • Separates into males and females
  • Creates all possible male-female pair combinations
  • Each pair gets a compatibility score and rare variant chance calculated

Compatibility Score

Source:

score = 50                                          // base
score += (maleHealth + femaleHealth) / 10           // 0-20 from health
score += (maleHappiness + femaleHappiness) / 20     // 0-10 from happiness
if both ages in 24-84:
  score += 10                                       // prime breeding age bonus
return clamp(0, 100, score)

Range: 50-90 typical. Maximum theoretical: 100.

Gestation Days (Species-Specific)

Source: zoo_simulator.species_defs.gestation_days (DB-backed via )

Gestation (days) Species
22 ELEPHANT, WHALE
15 GIRAFFE
12 LION, TIGER, BEAR, POLAR_BEAR, GORILLA, CHEETAH, HIPPO, ORANGUTAN, SNOW_LEOPARD
11 DOLPHIN, SHARK
10 PANDA
9 ZEBRA, KANGAROO, BISON
8 SEAL, OTTER, MONKEY, COYOTE, BABOON
7 CROCODILE
6 SNAKE, TURTLE
5 EAGLE, SKUNK, BAT
4 PENGUIN, FLAMINGO, OPOSSUM, RACCOON, DONKEY, SHEEP, GOAT, HEDGEHOG
3 PARROT, SQUIRREL, RABBIT, DUCK

Cooldown Days (Species-Specific)

Source: zoo_simulator.species_defs.cooldown_days (DB-backed via )

The rest a pair takes is scaled to its adult years. An animal ages a month each game day, so a cooldown is spent out of the 102 adult days between breeding age (24 months) and the elderly stage (126 months). The rest after a litter is the species cooldown below halved, and capped so gestation plus rest fits four times into the adult years: every species raises at least four litters before it is elderly. is the definition; where this paragraph and it differ, the code is right. Breeding is the player's lever to grow a zoo, so the rest is never longer than the species value below. No shop item speeds breeding, so nothing bought is devalued by it.

Cooldown (days) Species
60 ELEPHANT, WHALE
45 PANDA, BEAR, POLAR_BEAR, HIPPO, ORANGUTAN
30 GIRAFFE, LION, TIGER, GORILLA, CHEETAH, SNOW_LEOPARD
25 DOLPHIN, SHARK
21 ZEBRA, KANGAROO, MONKEY, BISON, BABOON
14 SEAL, OTTER, CROCODILE, COYOTE
10 EAGLE, PENGUIN, FLAMINGO, PARROT, BAT
8 RACCOON, SKUNK, DONKEY, HEDGEHOG
7 SNAKE, TURTLE, SQUIRREL, OPOSSUM, SHEEP, GOAT, RABBIT, DUCK

Expected Offspring

Source: zoo_simulator.species_defs.offspring_min / offspring_max (DB-backed via )

Species Offspring Count
WOLF, FOX, COYOTE 3-6 (random, canid tier — fb 4215/5253/5421/5422 round 6; COYOTE added )
SNAKE 3-7 (random)
RABBIT, DUCK 3-6 (random, petting zoo starter tier: the big first litter)
LION, TIGER, CHEETAH 2-5 (random, big-cat tier — fb 4215/5421/5422, max bumped ; CHEETAH added )
TURTLE, HEDGEHOG 2-5 (random)
SHEEP, GOAT 1-3 (random, petting zoo starter tier)
PARROT, SQUIRREL, OPOSSUM 2-4 (random; SQUIRREL/OPOSSUM are the small-mammal starter tier)
RACCOON, SKUNK 2-3 (random, small-mammal starter tier)
BEAR, POLAR_BEAR, PANDA 1-3 (random, bear tier — fb 5421)
SNOW_LEOPARD 1-3 (random, a smaller litter than the big-cat tier)
ELEPHANT, GIRAFFE, ZEBRA, DEER, HIPPO, BISON, DONKEY 1-2 (random, hoofed and large-mammal tier; HIPPO and BISON added )
PENGUIN 1-2 (random)
All others, including WHALE, BAT, BABOON, ORANGUTAN 1 (WHALE is a single-calf marine mammal, like DOLPHIN; the bat pup and the primate infant are single births)

Tuning history. Round 1 deferred offspring tuning until post-seasons. Tuning landed (2026-05-12) after seasons shipped. Round 2 (2026-05-22) bumped LION/TIGER from 2-4 to 2-5 to give big cats a litter feel closer to the canid + snake tiers without disturbing per-pair economic ceilings — peak big-cat output is now 5 babies x (5000 zoo bucks x 60pct) = 15000 zoo bucks per pair, still under the SNAKE/PARROT max-tier value. 5pct birth-mortality preserved; per-pair economic ceiling stays under 2x prior even at canid max (6 babies x 60pct of base value = 1080 zoo bucks per FOX pair, vs 900 prior cap for SNAKE). A small-mammal starter tier cited in the original design intent (then envisioned as RABBIT/HAMSTER/MOUSE) is now delivered as SQUIRREL, RACCOON, OPOSSUM, SKUNK — a low-cost 250 to 450 ZB entry rung with fast gestation and small litters (2-4 / 2-3). See the price, gestation, cooldown, and offspring tables above.

Lucky-Litter Hidden Bonus

A small hidden bonus fires at delivery time on top of rollExpectedOffspring. It is intentionally NOT shown in the start-breeding preview voice line — the player can't predict it, but they can influence it by keeping pairs healthy and happy.

Rule (in ):

compatibilityScore Bonus chance Bonus size
< 60 0% 0
60 0% 0
80 12.5% +1 newborn
100 25% +1 newborn
in between linear interpolation 0 or +1

The bonus is capped at +1 per delivery. Per-pair expected value: at most +0.25 babies, ~5% of a 5-baby litter — bounded enough to keep the economy stable while making pair quality feel meaningful.

Why hidden. The previewed expectedOffspring count is the floor; the player gets a pleasant surprise when a perfect pair delivers an extra. If we exposed the formula, optimal players would just farm 100-compat pairs for guaranteed +25% expected output and the bonus loses its "felt" quality. Compatibility was already chosen as the input because it folds health + happiness + age-window — three things the player already optimizes — into a single non-linear gate.

Unique Animal Names Per Zoo (queue / fb 904)

Baby names are picked from the baby pools by ZooAnimalNames.forBirth, the one naming rule for bought and born animals alike: one word no animal in the zoo has, never two names joined and never a trailing number. management.md § Which animal a name means describes it.

Backward-compatible: the legacy 3-arg createOffspring(male, female, pair) overload still exists and calls the new 4-arg form with account=null, picking from the raw pool with no uniqueness check. Tests and any direct callers that don't have a ZooAccount in scope keep working.

Voice impact. Before this rule, rapid-fire breeding produced 5 baby pandas all named "Pip" — players reported confusion identifying which baby was which. After: every animal in the zoo has a distinct name, and the existing "1 of 5" render-time suffix from stays as a safety net for any name collision that slips through (e.g. premium-name pairs that bypass the picker).

Birth Announcement Parentage (queue / fb 7363)

The advance-day birth summary names which parents had which babies, so a player can tell newborns apart instead of staring at letter-string IDs. Announcement-only — the Animal model stores no mother/father fields (persisted lineage was the deferred option, decision D3 in briefs/player-backlog-20309.md). Parentage is read from the live parent animals at delivery time inside , where both parents are already in scope, so no model change or save migration is needed.

Flow. deliverOffspring records one BirthAnnouncement (mother name, father name, survivor count, voice-friendly species label) per delivering pair into a sink threaded down from -> processDailyBreeding. The records ride on DailySimulationResult.birthAnnouncements (ephemeral, not persisted, parallel to newborns). speaks one line per pair, before the existing aggregate "N new animals were born and placed in your habitats" placement line.

Voice line. {mother} and {father} had {N} new {species} {baby|babies}, {baby names}. e.g. "Bella and Max had 3 new tiger babies, Pip, Luna, and Oak." / "Luna and Leo had 1 new tiger baby, Misty." Mother is the female parent, father the male, N is the count that survived (5% mortality may drop some), and surviving baby names follow (capped at 5). The species label is used as an adjective and is never pluralized ("polar bear babies", not "polar bears"); premium pairs use the custom species name ("unicorn babies"). When a parent name is missing the line still runs as "{N} new {species} babies arrived, {names}." so unnamed parents do not drop the announcement.

Why announcement-only. Unique baby names already gave every baby a distinct name; this closes the same legibility complaint (fb 7363 — the player could not tell which babies belong to which parents) without persisting lineage. Regression coverage is JUnit (, ) rather than a scenario_test: a same-day birth depends on gestation timing, the RNG compatibility gate, 5% mortality, and random names, so an end-to-end scenario row would be inherently flaky (per test-type-decision-tree.md, deterministic service/render logic belongs in JUnit).

Surprise-Birth Mode + ISP-Gated Pick-Litter-Size Perk (queue / fb 934)

Player feedback 934 (ZOO_SIMULATOR) floated two litter affordances: a player could PICK how many offspring, OR make it a SURPRISE (do not announce how many). Both ship here. Litter-SIZE realism itself shipped earlier ( — the per-species offspring_min/offspring_max bounds above).

A. Surprise-birth mode (free, per-account toggle). ZooAccount.surpriseBirthMode — null or false is the default preview behavior; true turns on surprise mode. When on, the expected litter size is hidden on every gestation surface — startBreeding, startPremiumBreeding, viewBreedingPairs, and viewBabies all speak a "the litter size is a surprise, revealed at birth" line instead of the count. The actual count is revealed at birth by the existing advance-day birth announcement ("Bella and Max had 3 new tiger babies", see Birth Announcement Parentage above) — no new reveal path was needed. Toggle commands: "surprise births" (flips the flag), "surprise births on" / "surprise births off" (explicit). Handler , action ZOO_TOGGLE_SURPRISE_BIRTHS. Hiding the count is purely presentational — it never changes the rolled expectedOffspring value, so it does not touch breeding economics.

B. Pick-litter-size perk (ISP-gated, never free). Lets the player CHOOSE a breeding pair's litter size within the species' realistic [offspring_min, offspring_max] bounds, for a Golden Ticket cost. Owner decision (2026-06-20): picking the litter size is good ONLY as a PAID perk via ISP — a free choice would let every player max every litter and break the breeding economy. Command: "pick litter size N " (e.g. "pick litter size 4 lions"); handler , action ZOO_PICK_LITTER_SIZE. Cost PICK_LITTER_SIZE_TICKET_COST = 5 Golden Tickets per use, charged via .debit("GOLDEN_TICKET", ...) (the cache-safe spend path, same as premium animals/zones). The chosen count is clamped to species bounds in .startBreeding(account, male, female, habitat, chosenLitterSize) → clampLitterSize; an out-of-range request is rejected with the valid range and is NOT charged. The perk always speaks the chosen size regardless of surprise-birth mode, because the player chose it. If the breeding attempt soft-fails after the charge (compatibility roll miss), the Golden Tickets are refunded.

ISP nesting (where the perk lives). Golden Tickets are the Zoo Simulator ISP currency ( — GOLDEN_TICKET, ledger key golden_tickets, packs 100/315/550/1250). They are acquired only by buying ISP packs (plus the small starter grant and leaderboard rewards). Gating pick-litter-size on a Golden Ticket spend therefore makes it "a pair perk obtainable by buying ISP" without a new SMAPI product: it reuses the existing Golden Ticket entitlement, exactly like premium animals and premium zones (economy.md), which also debit Golden Tickets through . There is no separate non-consumable entitlement SKU; the perk is a per-use Golden Ticket sink. Per the ISP perks taxonomy () this is a Layer C consumable-style spend — currency the player already buys, now with a felt, named use.

Gate order in pickLitterSize (no Golden Tickets are charged until the last gate passes): resolve species (from the utterance, else lastBreedingSpecies) → resolve the chosen number (NUMBER one-shot, else a regex parse of the utterance) → require an eligible pair (else the standard diagnoseNoEligiblePairs copy) → validate the number is within the species bounds → validate Golden Ticket affordability → debit → start breeding the best eligible pair with the chosen count.

NLU / routing. Two new FreeTextDeclarationAction values (ZOO_TOGGLE_SURPRISE_BIRTHS, ZOO_PICK_LITTER_SIZE) are registered in game_process.process_action_registry, process_action_handlers (both → ), process_alternate_synonyms (tier 4), and process_one_shot_values (NUMBER for pick-litter-size, FREE_FORM for the toggle). These config rows live in the live game_process schema (prod is the source of truth; local mirrors it for the deploy gate), not in code.

Birth Mortality

Source:

Per offspring: 5% chance of not surviving (random.nextDouble() < 0.05)

Breeding Flow

ELIGIBLE -> startBreeding() -> GESTATING -> (daysUntilBirth counts down) -> deliverOffspring() -> COOLDOWN -> (cooldown expires) -> pair removed
any status -> (a parent leaves the zoo) -> pair removed on the next tick; an expected litter is reported lost

Daily processing (processDailyBreeding):

  1. For every pair: if either parent is no longer in the zoo (sold, released or dead), remove the pair. A pair is its two animals, so there is nothing left to deliver or recover. A pair still expecting (GESTATING or BREEDING) is recorded as a lost litter, which the advance-day headline and the day summary speak: "Leo's lion litter was lost, because its mother is no longer in your zoo." A litter is not delivered from one parent: offspring genetics need both.
  2. For GESTATING pairs: decrement daysUntilBirth
  3. If daysUntilBirth <= 0: deliver offspring
  4. For COOLDOWN pairs: remove the pair once both parents' breedingCooldownDays are zero. A pair's cooldown is its parents' cooldown, counted in game days by advance-day, so a pair that finished its species cooldown is gone on the next tick. No calendar date holds a pair: cooldownEndDate is no longer written, and a stored one is ignored.
  5. Decrement all animal breedingCooldownDays by 1
  6. Recalculate all animal growth stages from current age

A sale does not end the pair on the spot. The sale takes the animal; the next tick finds the pair's parent gone, removes the pair, and tells the player. deliverOffspring leaves a pair with a missing parent untouched for that tick rather than parking it in COOLDOWN.

Nursery Habitat + Auto-Routed Newborns (fb 5783/5831/5838/5840)

NURSERY is the 11th HabitatType, parallel to HATCHERY but for live-birth species. It exists to give players a buildable, named place to grow up baby animals — not just a name-substring heuristic.

Driver. Player feedback 5838 explicitly asked for "build a nursery how you build a hatchery, then place the animals there before breeding or when theyre already breeding they can go in automatically". Feedback 5840 reported confusion that the nursery "is there automatically" (the player conflated the view babies filter with a real building). Feedback 5831 asked for an alert when babies grow up and leave the nursery. Feedback 5783 requested NPC visitors/staff plus a breedable petting-zoo side feature.

Property Value
Cost 3,000 Zoo Bucks (parallel to HATCHERY)
Capacity 10
Starting qualityRating 75 (above the 60 default to reflect specialized care + slightly above HATCHERY's 70)
Newborn auto-route Live-birth newborns prefer NURSERY in the same zone over the parent's habitat when at least one NURSERY exists and has free capacity
Compatible species All live-birth species (i.e. every base species NOT in the egg-layer set used by HATCHERY)
Graduation prompt When an animal in a NURSERY transitions to ADULT, the pending-graduation-prompt queue (fb 888) surfaces a move prompt. First turn: the player is asked to move the animal. Second turn: the game auto-assigns it to the first non-nursery habitat with room and announces the move. The auto-assign was promised in voice copy from the start but only implemented by queue / fb 6406, 6434 — before that the prompt was silently dropped after two turns and the animal stranded in the nursery.

Auto-route. picks each newborn's habitat fresh (capacity fills as a litter is placed). Preference order: (1) a NURSERY in the parents' zone with free room, (2) the parents' own habitat while it still has room, (3) any other same-zone habitat with room — a species-compatible type first, (4) the parents' habitat as a last resort so newborns are never lost. Steps 2-4 (capacity-aware spill) were added by queue / fb 6400-6549: large litters used to overflow the parents' habitat far past maxCapacity while sibling habitats sat empty, leaving the player unable to tell where the animals went. Nursery detection in step 1 uses the shared isNurseryHabitat rule (NURSERY type OR a name containing "nursery").

Why parallel to HATCHERY, not merged with it. Live-birth species (LION, TIGER, BEAR, …) and egg-layers (PARROT, SNAKE, EAGLE, …) have different player intuitions. A "hatchery" houses eggs; a "nursery" houses live young. Forcing both into one HATCHERY type would conflict with the voice reskin (incubating vs gestating), and 5840 explicitly wanted them as separate buildable buildings.

Existing nursery-by-name heuristic is preserved as a fallback so saved zoos that already have habitats named "Nursery" by their players keep working. Detection rule: a habitat is treated as a nursery if EITHER habitat.type == NURSERY OR habitat.habitatName contains "nursery" (legacy).

Per-species nurseries deferred. Feedback 5838 also asked for "build a nursery or a hatchery per species since when zoos get pretty big you might have a lot of animals with the same species". Per-species buildings are scope creep for this round — players can already build multiple NURSERY habitats and assign animals using existing move animal NAME to habitat N. Tracked as a follow-up note in this doc (not queued as a separate task — fragmentation rule).

Petting-zoo breeding deferred. Feedback 5783's "you could breed and buy animals for the petting zoo as like a little side thing" introduces a new animal subset (petting-zoo animals) plus a separate breeding pipeline. Out of scope for this round; queued as design discussion only in this doc, NOT as a follow-up task.


Hatchery Habitat + Egg-Layer Voice Reskin (fb 1176)

Two-layer answer to the player request for an incubator subsystem.

Mechanical layer — Hatchery habitat. Adds HATCHERY as the 10th HabitatType. Egg-layer breeding pairs (PARROT, SNAKE, EAGLE, PENGUIN, FLAMINGO, OWL, CROCODILE, TURTLE, SHARK, DUCK) placed in a HATCHERY habitat get a 0.80x gestation multiplier (hatch 20% faster) when their pair enters GESTATING. Non-egg-layers placed in HATCHERY get the existing wrong-habitat warning ( pattern).

Property Value
Cost 3,000 Zoo Bucks
Capacity 6
Starting qualityRating 70 (above the 60 default to reflect specialized care)
Hatch bonus 0.80x gestation_days for egg-layer pairs in HATCHERY
Compatible species 10 egg-layers (PARROT, SNAKE, EAGLE, PENGUIN, FLAMINGO, OWL, CROCODILE, TURTLE, SHARK, DUCK); is the definition

species_defs.compatible_habitats JSON is updated for the 9 egg-layers to include HATCHERY — DB UPDATE only, no code change for the species list. multiplies by 0.80 when both parents are in a HATCHERY-typed habitat. view hatchery lists all egg-layer pairs across all zones with their current days-until-hatching.

Voice layer — egg-layer phrasing. Applies whether or not the pair is in a Hatchery. Driver: a blind / voice-only player should never hear a parrot described as 'pregnant' or a snake as 'gestating'.

Standard phrase Egg-layer phrase
gestating incubating
days until birth days until hatching
pregnant carrying eggs
delivered offspring hatched offspring

Driver: fb 1176 player explicitly listed parrots / eagles / snakes / sharks as egg-layers and asked for incubator + hatchery. Voice reskin alone (the earlier proposal) was a phrase swap with no strategic depth; adding the Hatchery habitat gives the player a real buy-target, a strategic choice, and a visible subsystem that matches the request.

Start-Breeding-All Batch with Two-Turn Confirmation (fb 1096/1097)

start breeding all lions / breed all eligible lions triggers a two-turn confirmation flow matching the pattern shipped in .

Turn 1 (preview). .startBreedingAll(species) scans every habitat for eligible pairs of that species, computes total cost, stores pending request on zooAccount.pendingBatchBreedSpecies + pendingBatchBreedExpiresAt = now+60s. Voice:

I found 4 lion pairs ready to breed. Total cost is 800 zoo bucks. Days until first delivery: 12. Say yes to start, or no to cancel.

Special cases at preview:

  • No eligible pairs: No lion pairs are ready to breed. Lions need to be adult, healthy, and in the same habitat.
  • Insufficient balance: Breeding 4 lion pairs would cost 800 zoo bucks but you only have 600. Buy more, sell animals, or breed fewer pairs.

Turn 2 (confirm or cancel). Within 60s:

  • Player says yes / confirm / start breeding -> handler validates the pending request still matches (species same, count same, balance still sufficient), executes startBreeding on each pair, clears pending. Voice: Started breeding 4 lion pairs. First delivery in 12 days.
  • Player says no / cancel / nevermind -> handler clears pending. Voice: Cancelled.
  • 60s elapses -> pending auto-clears, next yes is a no-op.

The command is single-species by design. Player explicitly clarified in fb 1097 that the batch should scope to one species at a time, not the whole zoo. Existing zone-scoping rules still narrow targets further. The confirmation flow exists because a player saying start breeding all lions with 12 eligible pairs could otherwise drain 2,400 zoo bucks in one breath.

Batch cap (fb 8529/8530). A single breed-all operation is capped at MAX_BATCH_BREED_PAIRS = 10 pairs (). The 200-bucks-per-pair gate is meaningless for a wealthy heavy player (the reporter had ~1M zoo bucks; another large zoo holds 134M), so without a cap breed-all was effectively unbounded — it blew the Alexa 8-second budget on the breed turn and exploded the next advance-day with hundreds of births. When more than 10 pairs are eligible, the preview breeds the strongest 10 (sorted by compatibility) and tells the player to say breed all again to continue with the rest. The premium batch path (startBreedingAllPremium) shares the same cap.


Genetics System

Source:

Rare Variant Chance

BASE_RARE_CHANCE = 0.02            (2%)
RARE_PARENT_BONUS = 0.08           (8% per rare parent)
BOTH_RARE_MULTIPLIER = 5.0         (5x base when both parents rare)
Cap: 0.50                          (50% maximum)

Formula:

chance = 0.02
if both parents rare:
  chance = 0.02 * 5.0 + 0.08 * 2 = 0.26  (26%)
else if one parent rare:
  chance += 0.08                           (10%)

// UNUSUAL_PATTERN trait bonus
for each parent with expressed UNUSUAL_PATTERN trait:
  chance += 0.02

return min(0.50, chance)

7 Rare Variants

Source: llitd.model.zoosimulator.enums.RareVariantType

Variant Appeal Multiplier Value % Bonus Description
CHIMERA 3.5x +600% Split coloring, two genetic patterns
ALBINO 3.0x +500% Pure white with red/pink eyes
GOLDEN 2.8x +450% Golden or yellow coloration
MELANISTIC 2.5x +400% All black coloration
ERYTHRISTIC 2.2x +350% Unusual red or orange coloring
LEUCISTIC 2.0x +300% Partial loss of pigmentation
PIEBALD 1.5x +200% White patches with normal coloring

Variant selection uses inverse-appeal weighting: rarer variants (higher appeal) are less likely to be selected.

weight[i] = 1.0 / variants[i].appealMultiplier
// CHIMERA: 1/3.5 = 0.286, PIEBALD: 1/1.5 = 0.667
// Piebald is ~2.3x more likely than Chimera

14 Genetic Traits

Source: llitd.model.zoosimulator.enums.GeneticTraitType

Category Trait Dominant Size Mod % Appeal Mod Description
Appearance LARGE_SIZE Yes +10% +5 Larger than average
Appearance SMALL_SIZE No -10% 0 Smaller than average
Appearance VIBRANT_COLORING Yes 0% +15 Bright vivid colors
Appearance UNUSUAL_PATTERN No 0% +20 Rare striking patterns (+2% rare variant chance)
Appearance FLUFFY_COAT No 0% +10 Extra fluffy coat
Health ROBUST_HEALTH Yes 0% +5 Strong immune (+10 base health for offspring)
Health WEAK_IMMUNE No 0% -5 Prone to illness (-15 base health for offspring)
Health LONGEVITY Yes 0% +10 Lives longer
Health FAST_HEALER Yes 0% +5 Quick illness recovery
Personality NATURALLY_CALM Yes 0% +8 Higher base happiness (+10 for offspring)
Personality HIGH_ENERGY Yes 0% +5 Faster training (0.85x training days)
Personality INTELLIGENT No 0% +15 Quick learner (0.7x training days, +10 proficiency)
Personality GENTLE_GIANT No +5% +12 Large but gentle
Personality PERFORMER No 0% +18 Natural showmanship (+15 proficiency)

Genetic Term Voice Glossary (fb 1174)

On a player's first encounter per session with any of the 14 GeneticTraitType values or 7 RareVariantType values, the voice output appends a one-sentence definition. Subsequent encounters in the same session play the term alone, no definition.

Example first encounter: "Your new baby panda Pip is a Chimera. Chimera means split coloring, two patterns." Example second encounter (same session): "Your new baby tiger Misty is a Chimera."

State: per-session Set on ZooAccount tracking which terms have been read once. Reset on session boundary (Alexa session-ended). Survives within-session multiple play turns.

Driver: fb 1174 player (also fb 1180/1181 — blind, accessibility-focused) reported not knowing what terms like "chimera" / "piebald" / "leucistic" meant, and other players likely don't either. The inline-first-mention pattern means the player never has to know to ask for help — the game self-teaches.

View-Genetics Babies Filter (fb 1173)

.viewGenetics already supports species and premium-name filters. New keyword filter babies / baby / newborn / newborns narrows the rendered list to animals where isBaby=true OR growthStage in {NEWBORN, INFANT, JUVENILE}.

Example: view genetics babies -> renders genetic traits of all baby/juvenile animals only.

Trait Inheritance Rules

Source:

For each possible trait type:

  1. Check if male parent has it, female parent has it
  2. If both parents have it: willInherit = true, pick random parent's version
  3. If one parent has it: willInherit = 50% chance
  4. If inherited:
    • Both parents had it: expressed if isDominant or random < 0.25
    • One parent had it: dominant = 75% expressed, recessive = 25% expressed
  5. Strength: parent's strength +/- random(-10, 10), clamped to [10, 100]

Mutation

MUTATION_CHANCE = 0.05  (5% per offspring)
  • Selects random GeneticTraitType
  • Strength: random(60, 100)
  • Always expressed
  • Inherited from: "mutation"

Offspring Value Calculation

Source:

baseValue = speciesPrice                            // e.g., 5000 for ELEPHANT
baseValue = baseValue * 0.6                         // offspring worth 60% of purchase price
if rareVariant:
  baseValue *= rareVariantType.appealMultiplier     // e.g., 3.5x for CHIMERA
for each expressed trait:
  baseValue += trait.appealModifier * 10            // e.g., +150 for INTELLIGENT
if generation > 1:
  baseValue += (generation - 1) * 200               // +200 per generation

Offspring Appeal Calculation

Source:

baseAppeal = 100
if isBaby:
  baseAppeal *= 1.5                                 // babies are cuter
if rareVariant:
  baseAppeal *= rareVariantType.appealMultiplier
for each expressed trait:
  baseAppeal += trait.appealModifier

Offspring Base Stats

baseHealth = 80
baseHappiness = 70
if expressed ROBUST_HEALTH:   baseHealth += 10
if expressed WEAK_IMMUNE:     baseHealth -= 15
if expressed NATURALLY_CALM:  baseHappiness += 10
baseHealth = clamp(40, 100, baseHealth + random(-10, 10))
baseHappiness = clamp(40, 100, baseHappiness + random(-10, 10))

Baby Names

Sex Possible Names
MALE Junior, Tiny, Scout, Pip, Cub, Kit, Sprout, Buddy
FEMALE Blossom, Petal, Daisy, Misty, Fawn, Pearl, Willow, Dot

Personality Inheritance

40% chance: inherit one parent's personality (random which parent)
60% chance: completely random personality from all 7 types

Training System

Source:

Training Availability

Behaviors require:

  1. Animal's trainingLevel >= behavior.requiredLevel
  2. Species compatibility (behavior.isCompatibleWith(species))
  3. Animal doesn't already know the behavior
  4. Animal is not currently in training (isTraining == false)

Training Target Resolution + Maxed Guard

Source: .trainAnimal / runTrainingForAnimal

Resolution order for who to train:

  1. Personal or custom name — customSpeciesName and animalName longest-match in the utterance, so train Bella and train white tiger hit the named animal
  2. Premium display name — ZooLookup.findFirstAnimalByPremium before base species can steal eagle from philippine eagle
  3. Base species — slot enum or freeform species parse
  4. First trainable animal — one that is not mid-session and still has an available behavior at their current level. Bare train animal never sticks on a maxed first animal.

When the resolved animal has no available behaviors at their current level:

  • Speak that they are maxed at that level
  • If higher-level compatible behaviors remain, speak the level gate: training level rises every three mastered behaviors, and name the next required level
  • Offer other trainable animals by name as options, or say none are ready
  • Always leave at least one option — never return text-only with an empty option list (voice dead-end)

Animal-management help (.showAnimalHelp) covers training, selling or trading animals for zoo bucks, and moving animals.

Train All

"Train all" (also "train all my animals", "train them all", "train everyone"; is the one phrasing test, and asks it too) starts training for every ready animal the zoo can afford in one turn and says it once: how many started, who, and the total cost. When the Zoo Bucks run out, the rest are named as waiting for funds; only the animals whose training started are counted.

Training Flow

  1. startTraining(animal, behavior, account):

    • Deducts behavior.trainingCost from zooBucks
    • Calculates training days (modified by genetic traits)
    • Sets animal.isTraining = true
  2. Daily: progressTraining(animal):

    • Increments training progress
    • Decrements trainingDaysRemaining
    • When complete, calls completeTraining()
  3. completeTraining(animal, training):

    • Sets isFullyTrained = true, trainingProgress = 100
    • Generates proficiency: 70 + random(0-30) (70-100)
    • INTELLIGENT trait: +10 proficiency
    • PERFORMER trait: +15 proficiency
    • Actual appeal = baseAppeal * (proficiency / 100.0)
    • Training level advances: min(5, 1 + completedBehaviors / 3)

Genetic Trait Effects on Training

Trait Effect
INTELLIGENT Training days * 0.7 (30% faster), +10 proficiency
HIGH_ENERGY Training days * 0.85 (15% faster)
PERFORMER +15 proficiency at completion

Enrichment System

Source:

Enrichment Items

Items have:

  • happinessBonus and healthBonus (from EnrichmentType enum)
  • currentDurability / maxDurability (in days)
  • needsReplacement flag (set when durability <= 20% of max)

Daily Enrichment Processing

Source:

For each enrichment item on each animal:

  1. Decrement currentDurability by 1
  2. If durability reaches 0: remove item
  3. If durability <= 20% of max: set needsReplacement = true
  4. Apply daily happiness gain: ceil(happinessBonus * effectiveness * 0.1) where effectiveness = currentDurability / maxDurability
  5. Apply daily health gain: ceil(healthBonus * effectiveness * 0.05)

Enrichment Bonus Calculation

enrichmentBonus = sum(all active items' happinessBonus) + sum(all active items' healthBonus)

Recalculated after any enrichment add/remove/expiry.

Enrich every animal

A command that says all or every, such as "enrich all", applies the cheapest enrichment each animal can take and the player can afford. It says how many animals were enriched and the Zoo Bucks spent. A bare "add enrichment" still asks which animal when more than one can take it, and does not spend.


Voice Readout Ergonomics (Quality of Life)

Read-only presentation helpers that make existing data legible by voice. No economy or balance impact. Implemented in / with display-only facts in .

Per-species report — "tell me about my eagles"

(action ZOO_COUNT_SPECIES) is a per-species report, not just a count. For the named species it speaks: the total and the adult-male / adult-female / young / elderly breakdown, the average health and happiness, how many are sick, a typical full-grown weight, which habitats hold them, and a breeding-readiness hint (whether the player owns adults of both sexes). NLU openings how many, count, tell me about, and tell me about my all route here; the species is extracted from the remainder of the utterance. "what species do I have", "which species", "species inventory", and "my species" force the whole-zoo roster even if a species word appears. When no species is named ("count my animals", "how many animals do I have"), countAllSpecies instead speaks a whole-zoo per-species census: the grand total, the number of distinct species, and a per-species count most numerous first (capped at 20 spoken lines to stay under the Alexa speech budget, the rest summarized as a remaining-species count), so a player can hear how many of each they own without paging the roster.

Sort the roster by an attribute

accepts a sort request — "view animals by health", "sort animals by age", "sort animals by weight" — and renders a flat roster sorted by the named attribute so a player can find the outliers when many animals share names. Supported keys: health (lowest first), happiness (least happy first), age (oldest first), weight (heaviest first), value (most valuable first), appeal (highest first), name (alphabetical). A species named in the same command scopes the sort ("view animals lions by health"). Capped at 15 entries to stay under the Alexa speech budget. NLU openings sort animals, sort animals by, sort by, sort my animals route to ZOO_VIEW_ANIMALS; the plain view animals opening already carries the "by attribute" suffix.

Animal weights in readouts

Animal readouts (the view animals roster, inspect, and the per-species report) speak a weight. Weight is cosmetic and derived, not a stored stat: each species has a typical full-grown weight (, in pounds) scaled by the animal's growth-stage size multiplier — a newborn is 20% of adult size, an elderly animal 95%. Large weights are spoken as words so Alexa does not digit-spell them. Premium animals use their base species' weight.

Zone theme in the buy listing

The buyable-species price listing (describeSpeciesPricing, surfaced by see species available and the buy-animal miss path) names the zone theme each species most naturally belongs in — "lion in africa", "polar bear in arctic" — so a player choosing their first zone can pick animals and a theme together. The single primary theme per species lives in (display-only). This is distinct from the many-to-many that drives the visitor theme-coherence morale bonus — that system is unchanged.