Hire villagers as crew, unlocking new dialogue - #136
Conversation
Crew slots used to be an anonymous headcount. Hiring is now a choice of which villager joins the outfit, and each hire opens conversations the player couldn't have before. - Add a 12-villager roster (src/npc/villagers.py), each with a backstory, a specialty, and a hire-menu blurb; the roster is at least as long as the largest boat's crew so every berth can be filled by name - Give NPC dialogue options an optional "condition" callable so questions can unlock with game state; get_dialogue_response now indexes the same filtered list the front-ends number their menus from - Add "Talk to Your Crew" at the docks, with per-crew-member dialogue that reflects the boat, the wages, and further questions that unlock on a full crew / named business / Fishing Fleet - Unlock a crew question on Sam, Gilbert, and Old Tom once anyone is hired - Persist the crew by name (player.hiredWorkers) through PlayerJsonReaderWriter and schemas/player.json; saves without the field keep their headcount as unnamed hands Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Reword the Talk to Your Crew prompt so it fits one line (the formatter had split it into adjacent string literals) - Put the off-roster hand's backstory in first person, matching every roster entry - it's read back as "<name>: <backstory>" - Drop an unused local in Gilbert's crew-customer dialogue Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dmccoystephenson
left a comment
There was a problem hiding this comment.
Self-review (no configured reviewer on this repo). Read the full diff back; the suite is green at 463 tests with the new modules fully covered. Three notes below on deliberate choices a reader might otherwise flag, plus one behaviour worth knowing about. Nothing here blocks merge.
| being fixed at NPC-construction time.""" | ||
| if 0 <= option_index < len(self.dialogue_options): | ||
| response = self.dialogue_options[option_index].get("response", "") | ||
| options = self.get_dialogue_options() |
There was a problem hiding this comment.
This is the one behavioural change to an existing contract: get_dialogue_response used to index self.dialogue_options directly and now indexes the filtered list. That's required for correctness — every front-end numbers its menu from get_dialogue_options(), so with a hidden option present the old indexing would return the wrong answer. For any NPC without conditions the two lists are identical, so existing callers are unaffected.
| self.manageBusiness() | ||
| return LocationType.DOCKS | ||
|
|
||
| elif input == "8" and self.player.hiredWorkers: |
There was a problem hiding this comment.
The and self.player.hiredWorkers guard is redundant today — all three front-ends validate the choice against the option list they were handed (console loops on a mismatch, web rejects anything outside valid, pygame bounds-checks the number keys and wraps the arrow selection), so "8" can only come back when the option was actually offered. Kept anyway so the branch can't fire against an empty crew if a future front-end is looser about it.
| # Appended (rather than slotted in next to the other "Talk to" option) | ||
| # so the numbering of everything above stays put whether or not the | ||
| # player has anyone to talk to. | ||
| if self.player.hiredWorkers: |
There was a problem hiding this comment.
Worth knowing: this gates on hiredWorkers, not workers. A save from before this change loads with a crew but no names, so those players see no "Talk to Your Crew" until they hire someone new — at which point the option appears and Sam's line mentions the unnamed hands alongside the named one. That's the intended read of an old save rather than an oversight; the alternative (backfilling names onto existing headcount) would invent history the save never had.
|
|
||
|
|
||
| def _trimCrewRoster(player): | ||
| """Drop named crew until the roster fits the headcount, and return the |
There was a problem hiding this comment.
_trimCrewRoster is the single place the roster/headcount invariant (len(hiredWorkers) <= workers) is enforced, called from dismissWorker and the unpaid-wages path in runDailyProduction. Popping from the tail gives the desired ordering for free: unnamed legacy hands are absorbed by the headcount drop before any named villager is asked to leave, and among named ones the most recent hire goes first.
Summary
Crew slots used to be an anonymous headcount — "Hire a Worker" just incremented
player.workers. Hiring is now a choice of which villager joins the outfit, and every hire unlocks conversations the player couldn't have before.src/npc/villagers.pyadds a 12-villager roster, each with a backstory, a specialty, and a one-line blurb shown next to the wage in the hire menu. The roster is deliberately at least as long as the largest boat's crew capacity, so every berth on a Fishing Fleet can be filled by name.NPCdialogue options now accept an optional"condition"— a zero-arg callable — and stay hidden while it returnsFalse.get_dialogue_responseindexes the available options, i.e. the same list the front-ends number their menus from, so a hidden option can never shift a response onto the wrong question.player.hiredWorkersround-trips throughPlayerJsonReaderWriterandschemas/player.json.workersstays the authoritative headcount the production maths runs on, so a save written before crews had names loads with its crew intact — those hands are just unnamed, and are dismissed (or quit over unpaid wages) before any named villager is.business.hireWorker/dismissWorkercentralise the roster/headcount invariant;sellBoatand the unpaid-wages quit path keep the two in sync, andrunDailyProduction's summary now reportsquitNames.Test plan
python3 -m compileall -q src testsSDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python3 -m pytest --verbose -vv --cov=src --cov-report=term-missing --cov-report=xml:cov.xml— 463 passed. New/changed lines are fully covered (src/npc/villagers.py100%,src/npc/npc.py100%,src/player/*100%,src/location/docks.py98% with only pre-existingfish()branches uncovered).black+autoflakerun over the changed files only (unrelated files the formatter also touches were left alone to keep the diff readable).showOptions,showDialogue,showInteractiveDialogue— so all three front-ends are covered: pygame and web inheritBaseUserInterface.showInteractiveDialogue, and the console overrides it. Both paths re-readget_dialogue_options()each turn, and each has its own test that a conditional option is hidden until unlocked (tests/ui/test_baseUserInterface.py,tests/ui/test_userInterface.py).Docs
README.md's Fishing Business section was rewritten to describe hiring by name and the dialogue it unlocks.PLANNING.mdand the other schemas were checked and needed no change.