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:
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)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):
animal.age += 1(1 day = 1 month)- If
age >= 126: apply elderly senescence decline (health -= round(4 * (age-126)/54), clamped at 0) - Non-sick, non-elderly animals get +2 natural recovery; elderly animals do not
- Elderly animals have 1.5x illness chance multiplier
- 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
maxTreatmentsanimals - 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:
- Growth stage has
canBreed == true(ADULT only, ages 24-125) breedingCooldownDaysis null or <= 0health >= 30isSick != 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:
- Owns none -> buy an adult male and female at the market.
- 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_MONTHSup toZooLifeStage.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 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):
- 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.
- For GESTATING pairs: decrement
daysUntilBirth - If
daysUntilBirth <= 0: deliver offspring - For COOLDOWN pairs: remove the pair once both parents'
breedingCooldownDaysare 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:cooldownEndDateis no longer written, and a stored one is ignored. - Decrement all animal
breedingCooldownDaysby 1 - 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
yesis 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
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:
- Check if male parent has it, female parent has it
- If both parents have it:
willInherit = true, pick random parent's version - If one parent has it:
willInherit = 50%chance - If inherited:
- Both parents had it: expressed if
isDominantorrandom < 0.25 - One parent had it: dominant =
75%expressed, recessive =25%expressed
- Both parents had it: expressed if
- 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:
- Animal's
trainingLevel >= behavior.requiredLevel - Species compatibility (
behavior.isCompatibleWith(species)) - Animal doesn't already know the behavior
- Animal is not currently in training (
isTraining == false)
Training Target Resolution + Maxed Guard
Source: .trainAnimal / runTrainingForAnimal
Resolution order for who to train:
- Personal or custom name — customSpeciesName and animalName longest-match in the utterance, so train Bella and train white tiger hit the named animal
- Premium display name — ZooLookup.findFirstAnimalByPremium before base species can steal eagle from philippine eagle
- Base species — slot enum or freeform species parse
- 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
startTraining(animal, behavior, account):- Deducts
behavior.trainingCostfrom zooBucks - Calculates training days (modified by genetic traits)
- Sets
animal.isTraining = true
- Deducts
Daily:
progressTraining(animal):- Increments training progress
- Decrements
trainingDaysRemaining - When complete, calls
completeTraining()
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)
- Sets
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:
happinessBonusandhealthBonus(fromEnrichmentTypeenum)currentDurability/maxDurability(in days)needsReplacementflag (set when durability <= 20% of max)
Daily Enrichment Processing
Source:
For each enrichment item on each animal:
- Decrement
currentDurabilityby 1 - If durability reaches 0: remove item
- If durability <= 20% of max: set
needsReplacement = true - Apply daily happiness gain:
ceil(happinessBonus * effectiveness * 0.1)whereeffectiveness = currentDurability / maxDurability - 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.