diff options
Diffstat (limited to 'skill_plans/construction.md')
| -rw-r--r-- | skill_plans/construction.md | 1670 |
1 files changed, 58 insertions, 1612 deletions
diff --git a/skill_plans/construction.md b/skill_plans/construction.md index 85452ad..17a1533 100644 --- a/skill_plans/construction.md +++ b/skill_plans/construction.md @@ -1,1638 +1,84 @@ -# Construction Skill Implementation Plan +# Construction Skill Plan -## 1. Overview +## Overview -Construction adds player housing, a workshop for crafting planks and furniture, and a real estate broker NPC. Players buy a house from the broker for 10 credits, then visit their house (or other players' houses) through a Directory object in the Local Neighborhood room. Each house consists of two virtual rooms: a house entrance and a workshop. The workshop contains a workbench station where players slowly convert logs into planks (the `make`/`construct` command), then convert planks into sellable furniture items for Construction XP. +Construction is a production skill centered around processing logs into planks and assembling furniture. Players gain XP through repetitive crafting at a workbench, with higher-tier logs providing more XP and more valuable outputs. -**Key design decisions:** -- Player houses are virtual rooms generated in memory, NOT YAML files on disk -- The workshop has a "workbench" object that acts as a production station -- Construction products are items with credit value (not room decorations) -- The house is just 2 rooms (entrance + workshop) for now -- No Runescape-style dismantling — items are final products +## Core Loop -**Existing state:** -- Room 16 is "Construction Site" (`data/rooms/16.yaml`) with exits east:17, west:15 -- `Construction` skill already defined in `internal/player/player.go` (line 27) with abbreviation `"con"` (line 59) -- The production system in `action_production.go` is reusable for a new `"construction"` recipe type +1. **Gather logs** via Woodcutting +2. **Process logs into planks** at a workbench (with a saw) or at the sawmill (for credits) or via Plank Make (science mod) +3. **Assemble planks into furniture** at a workbench +4. **Sell furniture** for credits (furniture has higher value than input planks) -## 2. Architecture +## Architecture -### Virtual Room System +### Production System Integration -Player houses exist only in memory. The `World.LoadRoom()` function (at `internal/world/world.go:306`) currently reads exclusively from disk. We add a **virtual room registry** so `LoadRoom` checks memory first before disk. +Construction plugs into the existing unified production system. Recipes use `type: construction`, `station: [workbench]`, and optionally `tool: saw`. The `advanceProduction` function handles the full lifecycle without any construction-specific code. -Each player who buys a house gets two virtual room IDs allocated: -- **House entrance**: base ID -- **Workshop**: base ID + 1 +### Recipe Tiers -Room IDs for virtual rooms use the range **100000+** to avoid collision with YAML room files. A counter in `World` tracks the next available virtual room ID. +**Plank recipes** (4 tiers): +- Regular (Lv1): logs → planks (20 ticks, 5xp) +- Oak (Lv15): oak_logs → oak_planks (20 ticks, 10xp) +- Teak (Lv35): teak_logs → teak_planks (20 ticks, 20xp) +- Mahogany (Lv50): mahogany_logs → mahogany_planks (20 ticks, 30xp) -**Player flags used** (stored in character YAML `flags:` map): -- `has_house` (bool): Whether the player owns a house -- `house_room_id` (int): The virtual room ID of the player's house entrance -- `house_owner` (string): The player's character name (redundant but useful for Directory) +**Furniture recipes** (7 items across 4 tiers): +- Wooden shelf (Lv4), table (Lv6), chair (Lv8) +- Oak shelf (Lv18), table (Lv20) +- Teak table (Lv38) +- Mahogany table (Lv52) -### Data Flow +### Alternative Plank Sources -``` -Player buys house (talk broker) - → set player_flag has_house: true - → allocate virtual room IDs via housing.go - → store house_room_id in player flags - → save character +1. **Sawmill operator** (room 171): Pays credits per log to get planks instantly. Fees: 5/10/20/40 credits. Uses the `sawmill` node action — a custom talk node action that process all logs at once. -Player visits house (use directory) - → read target player's character file for house_room_id - → ensure virtual rooms exist in World registry - → teleport player to house entrance +2. **Plank Make mod** (Science Lv86): Converts logs to planks at 70% of sawmill cost (3/7/14/28 credits) plus junk cost. Awards both Science XP and Construction XP. -Player uses workshop - → findStation finds "workbench" object in virtual room - → production system handles make/construct recipes normally -``` +## Player Housing -## 3. Commands +Houses serve as a private space with a workbench and bank booth. Future expansions will allow adding rooms, furniture placement, and multi-city ownership. -### `make` / `construct` (Active command, production-style) +### House Purchase +- Talk to the estate broker at the Construction Site (room 16) +- Cost: 10 credits +- Sets player flag: `owns_house_local_neighborhood: "local_neighborhood"` -These are aliases for the same command. They work like `smith` — require a workbench station in the room, show a production table of available recipes, and use the unified production cycle. +### House Access +- Walk north from Construction Site to Local Neighborhood (room 170) +- Use or talk to the Estate Directory +- Enter your house via the directory -**Classification** — add to `classifyCommand()` in `internal/game/game.go:136`: -```go -case "get", "take", "grab", "pick", "drop", - "attack", "kill", - "north", "n", "south", "s", "east", "e", - "west", "w", "up", "u", "down", "d", - "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "make", "construct": - return ClassActive -``` +### House Rooms +- Room 200: Entrance Hall (charging station) +- Room 201: Workshop (workbench + bank booth) -**Dispatch** — add to `executeCommand()` in `internal/game/game.go` (after the `craft` case around line 380): -```go -case "make", "construct": - g.doMake(sess, strings.Join(args, " ")) - return -``` +Rooms are real YAML files in `data/rooms/player_housing/`. The room loader checks this directory as fallback after the main rooms directory. -**AdvanceActions** — the production system already handles any action type registered in `productionActionTypes` (via the `productionTypes` map in `action_production.go:29`). Add the entry: -```go -var productionTypes = map[string]productionTypeInfo{ - // ... existing entries ... - "construction": {"make", "construction"}, -} -``` +### Future: Multi-House Support -This causes `advanceProduction` to be called for `make` action types automatically via the `default` case in `AdvanceActions` at `action.go:237`. +Each housing area gets a unique player flag: +- `owns_house_local_neighborhood: "local_neighborhood"` → rooms 200-201 +- `owns_house_industrial_district: "industrial_district"` → rooms 210-211 (future) +- etc. -## 4. New Files to Create +The `getOwnedHouseRoom` function maps flag values to room IDs. The directory scans for area-specific flags. -### Go Files +## Files | File | Purpose | |------|---------| -| `internal/game/cmd_make.go` | `doMake()` command handler for `make`/`construct` | -| `internal/game/housing.go` | Virtual room generation, Directory logic, homeowner scanning | - -### YAML Data Files - -| File | Purpose | -|------|---------| -| `data/rooms/150.yaml` | Local Neighborhood room (north of room 16) | -| `data/objects/directory.yaml` | Directory object definition | -| `data/objects/workbench.yaml` | Workbench station object definition | -| `data/mobs/real_estate_broker.yaml` | Real estate broker mob definition | -| `data/behaviors/broker_talk.yaml` | Broker talk behavior (buy house dialog) | -| `data/items/plank.yaml` | Regular plank item | -| `data/items/oak_plank.yaml` | Oak plank item | -| `data/items/teak_plank.yaml` | Teak plank item | -| `data/items/mahogany_plank.yaml` | Mahogany plank item | -| `data/items/wooden_shelf.yaml` | Wooden shelf (furniture item) | -| `data/items/wooden_table.yaml` | Wooden table (furniture item) | -| `data/items/wooden_chair.yaml` | Wooden chair (furniture item) | -| `data/items/wooden_bench.yaml` | Wooden bench (furniture item) | -| `data/items/oak_shelf.yaml` | Oak shelf (furniture item) | -| `data/items/oak_table.yaml` | Oak table (furniture item) | -| `data/items/oak_chair.yaml` | Oak chair (furniture item) | -| `data/items/oak_bench.yaml` | Oak bench (furniture item) | -| `data/items/teak_shelf.yaml` | Teak shelf (furniture item) | -| `data/items/teak_table.yaml` | Teak table (furniture item) | -| `data/items/teak_chair.yaml` | Teak chair (furniture item) | -| `data/items/teak_bench.yaml` | Teak bench (furniture item) | -| `data/items/mahogany_shelf.yaml` | Mahogany shelf (furniture item) | -| `data/items/mahogany_table.yaml` | Mahogany table (furniture item) | -| `data/items/mahogany_chair.yaml` | Mahogany chair (furniture item) | -| `data/items/mahogany_bench.yaml` | Mahogany bench (furniture item) | -| `data/recipes/construct_plank.yaml` | Logs → plank recipe | -| `data/recipes/construct_oak_plank.yaml` | Oak logs → oak plank recipe | -| `data/recipes/construct_teak_plank.yaml` | Teak logs → teak plank recipe | -| `data/recipes/construct_mahogany_plank.yaml` | Mahogany logs → mahogany plank recipe | -| `data/recipes/construct_wooden_shelf.yaml` | Plank → wooden shelf recipe | -| `data/recipes/construct_wooden_table.yaml` | Plank → wooden table recipe | -| `data/recipes/construct_wooden_chair.yaml` | Plank → wooden chair recipe | -| `data/recipes/construct_wooden_bench.yaml` | Plank → wooden bench recipe | -| `data/recipes/construct_oak_shelf.yaml` | Oak plank → oak shelf recipe | -| `data/recipes/construct_oak_table.yaml` | Oak plank → oak table recipe | -| `data/recipes/construct_oak_chair.yaml` | Oak plank → oak chair recipe | -| `data/recipes/construct_oak_bench.yaml` | Oak plank → oak bench recipe | -| `data/recipes/construct_teak_shelf.yaml` | Teak plank → teak shelf recipe | -| `data/recipes/construct_teak_table.yaml` | Teak plank → teak table recipe | -| `data/recipes/construct_teak_chair.yaml` | Teak plank → teak chair recipe | -| `data/recipes/construct_teak_bench.yaml` | Teak plank → teak bench recipe | -| `data/recipes/construct_mahogany_shelf.yaml` | Mahogany plank → mahogany shelf recipe | -| `data/recipes/construct_mahogany_table.yaml` | Mahogany plank → mahogany table recipe | -| `data/recipes/construct_mahogany_chair.yaml` | Mahogany plank → mahogany chair recipe | -| `data/recipes/construct_mahogany_bench.yaml` | Mahogany plank → mahogany bench recipe | -| `data/help/make.yaml` | Help topic for make/construct command | -| `data/help/construction.yaml` | Help topic for construction skill | - -## 5. Code Changes to Existing Files - -### `internal/game/game.go` - -**1. `classifyCommand()` (line 136)** — Add `"make"` and `"construct"` to the Active command case: - -```go -// Change this line (around line 150): -"quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft": -// To: -"quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "make", "construct": -``` - -**2. `executeCommand()` (around line 379)** — Add dispatch case after `craft`: - -```go -case "make", "construct": - g.doMake(sess, strings.Join(args, " ")) - return -``` - -### `internal/game/action_production.go` - -**3. `productionTypes` map (line 29)** — Add construction entry: - -```go -var productionTypes = map[string]productionTypeInfo{ - "cooking": {"cook", "cooking"}, - "smelting": {"smelt", "smelting"}, - "smithing": {"smith", "smithing"}, - "crafting": {"craft", "crafting"}, - "combine": {"combine", "combining"}, - "fletching": {"fletch", "fletching"}, - "construction": {"make", "construction"}, -} -``` - -This single addition causes the entire production cycle (`startProduction`, `advanceProduction`, `canContinueProduction`) to work automatically for `type: "construction"` recipes. The `init()` function at line 40 will register `"make"` in `productionActionTypes`, and the `default` case in `AdvanceActions` at `action.go:237` will route to `advanceProduction`. - -### `internal/world/world.go` - -**4. Add virtual room registry** — Add a `virtualRooms` map and a `nextVirtualID` counter to the `World` struct (line 39): - -```go -type World struct { - dataDir string - mu sync.Mutex - groundItems map[int][]*groundEntry - seeded map[int]bool - objStates map[string]*ObjState - objMoves []ObjMove - virtualRooms map[int]*Room // NEW: in-memory rooms - nextVirtualID int // NEW: next available virtual room ID -} -``` - -**5. Initialize in `New()` (line 297)**: - -```go -func New(dataDir string) *World { - return &World{ - dataDir: dataDir, - groundItems: make(map[int][]*groundEntry), - seeded: make(map[int]bool), - objStates: make(map[string]*ObjState), - virtualRooms: make(map[int]*Room), - nextVirtualID: 100000, - } -} -``` - -**6. Modify `LoadRoom()` (line 306)** — Check virtual rooms first: - -```go -func (w *World) LoadRoom(id int) (*Room, error) { - w.mu.Lock() - if vr, ok := w.virtualRooms[id]; ok { - w.mu.Unlock() - return vr, nil - } - w.mu.Unlock() - - // existing disk-loading code follows... - path := filepath.Join(w.dataDir, "rooms", fmt.Sprintf("%d.yaml", id)) - // ... -} -``` - -**7. Add virtual room management methods**: - -```go -func (w *World) RegisterVirtualRoom(room *Room) { - w.mu.Lock() - defer w.mu.Unlock() - w.virtualRooms[room.ID] = room -} - -func (w *World) AllocateVirtualID() int { - w.mu.Lock() - defer w.mu.Unlock() - id := w.nextVirtualID - w.nextVirtualID += 2 // reserve 2 IDs per house (entrance + workshop) - return id -} - -func (w *World) HasVirtualRoom(id int) bool { - w.mu.Lock() - defer w.mu.Unlock() - _, ok := w.virtualRooms[id] - return ok -} -``` - -### `internal/player/store.go` - -**8. Add `ListHomeowners()` method** — Scan all character files for `has_house` flag: - -```go -func (s *AccountStore) ListHomeowners() []string { - dir := filepath.Join(s.dataDir, "players", "characters") - entries, err := os.ReadDir(dir) - if err != nil { - return nil - } - var names []string - for _, e := range entries { - if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { - continue - } - charName := strings.TrimSuffix(e.Name(), ".yaml") - p, err := s.LoadCharacter(charName) - if err != nil { - continue - } - if p.Flags != nil { - if hasHouse, ok := p.Flags["has_house"].(bool); ok && hasHouse { - names = append(names, p.Name) - } - } - } - sort.Strings(names) - return names -} -``` - -This requires adding `"sort"` to the imports in `store.go`. - -### `internal/player/player.go` - -**9. Add `make_all` option** — Add to `OptionDefs` slice (around line 134, after the existing `*_all` options): - -```go -{"make_all", OptBool, false, nil, "Auto-start construction when only one product is possible"}, -``` - -### `internal/game/game.go` (HandleSession) - -**10. Add `StateVisitHouse` handling** — In `HandleSession()` (line 92), add a case for the new session state that the Directory object will use: - -```go -case net.StateVisitHouse: - g.handleVisitHouseInput(sess, input) -``` - -### `internal/net/server.go` - -**11. Add `StateVisitHouse` constant** — Add to the session state constants (around line 39): - -```go -StateVisitHouse -``` - -## 6. Real Estate Broker - -The broker is a mob NPC placed in the Construction Site room (room 16). Uses the existing talk behavior system. - -### Mob Definition: `data/mobs/real_estate_broker.yaml` - -```yaml -id: real_estate_broker -name: Real Estate Broker -description: "A smartly dressed broker clutching a clipboard and a ring of keys. She looks eager to make a sale." -behavior: broker_talk -unique: true -protected: true -hp: 50 -attack: 1 -strength: 1 -defense: 1 -speed: 5 -aggressive: false -respawn_ticks: 30 -idle_descriptions: - - "flips through property listings on a clipboard" - - "jingles a ring of keys" - - "adjusts her collar and checks her watch" - - "polishes a small 'SOLD' stamp" -``` - -### Talk Behavior: `data/behaviors/broker_talk.yaml` - -```yaml -id: broker_talk -type: talk -nodes: - start: - message: "\"Welcome! I'm the local real estate broker. Looking for a place to call home?\"" - options: - - text: "\"I'd like to buy a house.\"" - goto: buy_offer - condition: - all_of: - - player_flag: has_house - not: true - - min_credits: 10 - - text: "\"I'd like to buy a house.\"" - goto: no_credits - condition: - all_of: - - player_flag: has_house - not: true - - min_credits: 10 - not: true - - text: "\"I already own a house.\"" - goto: already_own - condition: - player_flag: has_house - value: true - - text: "\"Just looking around.\"" - end: true - buy_offer: - message: "\"Excellent! I have a lovely starter home available in the Local Neighborhood — just north of here. It comes with a workshop, perfect for construction projects. The price is 10 credits. Shall I draw up the paperwork?\"" - options: - - text: "\"Yes, I'll take it!\"" - goto: purchase_complete - - text: "\"Let me think about it.\"" - end: true - purchase_complete: - message: "\"Congratulations! Here are your keys. Your new home is in the Local Neighborhood, just north of here. Use the Directory there to find your house. Happy building!\"" - action: - cost: 10 - set_player_flags: - has_house: true - options: - - text: "\"Thanks!\"" - end: true - no_credits: - message: "\"I'm afraid the starter home costs 10 credits. Come back when you've saved up!\"" - options: - - text: "\"I'll be back.\"" - end: true - already_own: - message: "\"You already own a home! Head to the Local Neighborhood north of here and use the Directory to visit it.\"" - options: - - text: "\"Thanks for the reminder.\"" - end: true -``` - -**Note on house room allocation:** The talk behavior sets `has_house: true` via `set_player_flags`. The actual virtual room ID allocation happens lazily the **first time** the player visits their house via the Directory (in `housing.go`). This avoids needing to extend `applyNodeAction` — the existing `set_player_flags` and `cost` handling in `action_talk.go:165-218` handles everything. - -### Update Room 16: `data/rooms/16.yaml` - -```yaml -id: 16 -name: "Construction Site" -description: "A half-built structure surrounded by planks, nails, and blueprints. A {130}real estate broker{/} stands near a model home display, eager to chat." -exits: - north: 150 - east: 17 - west: 15 -mobs: - - id: real_estate_broker -``` - -Changes from current: -- Added `north: 150` exit to Local Neighborhood -- Updated description to mention the broker -- Added broker mob - -## 7. Local Neighborhood Room - -### `data/rooms/150.yaml` - -```yaml -id: 150 -name: "Local Neighborhood" -description: "A quiet residential street lined with small houses. A large {45}directory board{/} stands at the entrance, listing all the homeowners in the area. The construction site lies to the south." -exits: - south: 16 -objects: - - id: directory -``` - -## 8. Directory Object - -### `data/objects/directory.yaml` - -```yaml -id: directory -name: directory -color: "45" -description: "A large board listing all homeowners in the neighborhood. Use it to visit someone's house." -inroom_description: "A large directory board lists the local homeowners." -``` - -The Directory has two interactions: -1. **`look directory`** — Shows a list of all homeowners (handled by custom logic in `cmd_look.go`) -2. **`use directory`** — Prompts "Visit whose house (enter for yours):" and teleports (handled by custom logic in `housing.go`) - -### Implementation in `internal/game/housing.go` - -This file contains all housing-related logic. - -```go -package game - -import ( - "fmt" - "sort" - "strings" - - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/player" - "thehouseoficarus/internal/world" -) - -const neighborhoodRoomID = 150 - -func (g *Game) isDirectoryObject(defID string) bool { - return defID == "directory" -} - -func (g *Game) listHomeowners() []string { - return g.AccountStore.ListHomeowners() -} - -func (g *Game) lookDirectory(sess *net.Session) { - owners := g.listHomeowners() - if len(owners) == 0 { - sess.WriteLine("The directory is empty — no one has bought a house yet.") - return - } - sess.WriteLine("The directory lists the following homeowners:") - for _, name := range owners { - sess.WriteLine(fmt.Sprintf(" - %s", name)) - } -} - -func (g *Game) useDirectory(sess *net.Session) { - p := sess.Player.(*player.Player) - _ = p - sess.State = net.StateVisitHouse - sess.Write("Visit whose house (enter for yours): ") -} - -func (g *Game) handleVisitHouseInput(sess *net.Session, input string) { - sess.State = net.StateGame - p := sess.Player.(*player.Player) - input = strings.TrimSpace(input) - - var targetName string - if input == "" { - if p.Flags == nil { - sess.WriteLine("You don't own a house.") - g.reprompt(sess) - return - } - hasHouse, _ := p.Flags["has_house"].(bool) - if !hasHouse { - sess.WriteLine("You don't own a house. Talk to the Real Estate Broker at the Construction Site.") - g.reprompt(sess) - return - } - targetName = p.Name - } else { - found := false - for _, name := range g.listHomeowners() { - if strings.EqualFold(name, input) || strings.HasPrefix(strings.ToLower(name), strings.ToLower(input)) { - targetName = name - found = true - break - } - } - if !found { - sess.WriteLine(fmt.Sprintf("%s doesn't own a house here.", input)) - g.reprompt(sess) - return - } - } - - roomID := g.ensurePlayerHouse(targetName) - if roomID == 0 { - sess.WriteLine("Something went wrong finding that house.") - g.reprompt(sess) - return - } - - p.RoomID = roomID - g.AccountStore.SaveCharacter(p) - g.World.SeedGroundItems(p.RoomID) - g.seedRoomObjects(p.RoomID) - if g.Hub != nil { - g.Hub.EnterRoom(sess, p.RoomID) - } - g.doLook(sess) - g.writePrompt(sess) -} - -func (g *Game) ensurePlayerHouse(charName string) int { - target, err := g.AccountStore.LoadCharacter(charName) - if err != nil { - return 0 - } - if target.Flags == nil { - return 0 - } - - // Check if the character already has a room ID allocated - if rid, ok := target.Flags["house_room_id"]; ok { - var roomID int - switch v := rid.(type) { - case int: - roomID = v - case float64: - roomID = int(v) - } - if roomID > 0 && g.World.HasVirtualRoom(roomID) { - return roomID - } - // Room ID stored but virtual rooms not registered (server restart) — regenerate - if roomID > 0 { - g.registerHouseRooms(roomID, charName) - return roomID - } - } - - // Allocate new virtual room IDs - roomID := g.World.AllocateVirtualID() - target.Flags["house_room_id"] = roomID - g.AccountStore.SaveCharacter(target) - - // If this is the current player, also update their in-memory flags - // (the caller may have a different Player pointer) - - g.registerHouseRooms(roomID, charName) - return roomID -} - -func (g *Game) registerHouseRooms(baseID int, ownerName string) { - workshopID := baseID + 1 - - entrance := &world.Room{ - ID: baseID, - Name: fmt.Sprintf("%s's House", ownerName), - Description: fmt.Sprintf("A cozy starter home belonging to %s. The walls are bare but full of potential. A doorway to the west leads to a workshop.", ownerName), - Exits: map[world.ExitDir]world.ExitDef{ - world.South: {Room: neighborhoodRoomID}, - world.West: {Room: workshopID}, - }, - } - - workshop := &world.Room{ - ID: workshopID, - Name: "Workshop", - Description: "A dusty workshop with a sturdy workbench in the center. Sawdust covers the floor. Tools hang neatly on the walls.", - Exits: map[world.ExitDir]world.ExitDef{ - world.East: {Room: baseID}, - }, - Objects: []world.RoomObject{ - {ID: "workbench"}, - }, - } - - g.World.RegisterVirtualRoom(entrance) - g.World.RegisterVirtualRoom(workshop) -} -``` - -### Hooking `look directory` into `cmd_look.go` - -In `doLookTarget()` in `internal/game/cmd_look.go`, add a check: if the target matches a directory object in the current room, call `g.lookDirectory(sess)` instead of (or in addition to) showing the object description. - -Find the section in `doLookTarget` that handles looking at objects. After the object is found and its description is displayed, add: - -```go -// After displaying the object description for "directory" -if obj.ID == "directory" { - g.lookDirectory(sess) -} -``` - -This is an addition to the normal look flow — the object's `description` field text is shown first ("A large board listing all homeowners..."), then the dynamic homeowner list is appended. - -### Hooking `use directory` into `cmd_use.go` - -In `doUse()` in `internal/game/cmd_use.go`, the flow already routes through `StartAction` for object interactions. However, the Directory doesn't have a behavior YAML — it needs a custom handler. - -The cleanest approach: add a `use_interactions` entry on the directory object that triggers a custom action, OR handle it as a special case in `doUse`. Since the directory needs a prompt state (StateVisitHouse), handle it as a special case: - -In `cmd_use.go`, when `use directory` is entered and the directory object is found in the room, call `g.useDirectory(sess)` instead of routing through the behavior system. The simplest hook point is in `doUse` — after identifying the target is the `directory` object, short-circuit: - -```go -if target object's defID == "directory" { - g.useDirectory(sess) - return -} -``` - -Alternatively, add a check in `StartAction` (`action.go:45`) that intercepts `directory` before behavior lookup. The `doUse` approach is cleaner since `use` on its own already has custom item-on-object logic. - -## 9. Player House Rooms - -### Dynamic Room Generation - -Virtual rooms are `*world.Room` structs stored in `World.virtualRooms`. They are created on-demand by `ensurePlayerHouse()` in `housing.go`. - -**House Entrance Room:** -- Name: `"<PlayerName>'s House"` -- Description: `"A cozy starter home belonging to <PlayerName>. The walls are bare but full of potential. A doorway to the west leads to a workshop."` -- Exits: `south → 150` (Local Neighborhood), `west → workshopID` -- No objects, no mobs, no spawns - -**Workshop Room:** -- Name: `"Workshop"` -- Description: `"A dusty workshop with a sturdy workbench in the center. Sawdust covers the floor. Tools hang neatly on the walls."` -- Exits: `east → entranceID` -- Objects: `[{id: "workbench"}]` - -### Server Restart Handling - -Virtual rooms are lost on server restart (they live in memory). When a player visits a house after restart, `ensurePlayerHouse()` checks if the virtual rooms exist. If not, it re-registers them using the stored `house_room_id` from the player's flags. This is seamless — the first visit after restart recreates the rooms. - -The `AllocateVirtualID()` counter also resets on restart. To prevent ID collisions, on startup the system should scan all character files for existing `house_room_id` values and set `nextVirtualID` above the maximum found. Add this to `Game.New()` or a separate init function: - -```go -func (g *Game) initHousingIDs() { - dir := filepath.Join(g.dataDir, "players", "characters") - entries, _ := os.ReadDir(dir) - maxID := 100000 - for _, e := range entries { - if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { - continue - } - name := strings.TrimSuffix(e.Name(), ".yaml") - p, err := g.AccountStore.LoadCharacter(name) - if err != nil { - continue - } - if p.Flags != nil { - if rid, ok := p.Flags["house_room_id"]; ok { - var id int - switch v := rid.(type) { - case int: - id = v - case float64: - id = int(v) - } - if id+2 > maxID { - maxID = id + 2 - } - } - } - } - g.World.SetNextVirtualID(maxID) -} -``` - -Add `SetNextVirtualID` to `World`: -```go -func (w *World) SetNextVirtualID(id int) { - w.mu.Lock() - defer w.mu.Unlock() - if id > w.nextVirtualID { - w.nextVirtualID = id - } -} -``` - -Call `g.initHousingIDs()` after `Game.New()` in `cmd/mud/main.go`, before the server starts accepting connections. - -### Map Integration - -The BFS map builder in `map.go` calls `World.LoadRoom()` to traverse exits. Virtual rooms will automatically be included since `LoadRoom` checks the virtual registry. Player house rooms will appear on the map when the player is inside them. - -## 10. Workshop - -### Workbench Object: `data/objects/workbench.yaml` - -```yaml -id: workbench -name: workbench -color: "137" -description: "A sturdy wooden workbench with a built-in vice, saw guides, and tool racks. Perfect for construction projects." -inroom_description: "A sturdy workbench dominates the center of the room." -``` - -The workbench has no behavior — it serves purely as a station object. `findStation()` in `stations.go:3` searches for objects in the room by `defID`. Construction recipes list `station: [workbench]`, and `findStation` will match the workbench's `defID` of `"workbench"`. - -### How the Workshop Works - -1. Player enters workshop (east exit from house entrance) -2. `seedRoomObjects` is called, which calls `World.LoadRoom(workshopID)` — returns the virtual room — and then calls `EnsureObjectStates` for the `workbench` object -3. Player types `make` or `construct` -4. `doMake()` calls `findStation(p.RoomID, []string{"workbench"})` — finds the workbench -5. Recipes of `type: "construction"` with `station: [workbench]` are loaded and filtered -6. Player picks a recipe from the production table -7. Unified production cycle runs: consume materials, wait ticks, produce output, award XP - -## 11. Items - -### Plank Items - -#### `data/items/plank.yaml` -```yaml -id: plank -name: plank -color: "137" -description: "A carefully shaped wooden plank, ready for construction." -value: 5 -stackable: false -``` - -#### `data/items/oak_plank.yaml` -```yaml -id: oak_plank -name: oak plank -color: "143" -description: "A sturdy oak plank, suitable for quality furniture." -value: 15 -stackable: false -``` - -#### `data/items/teak_plank.yaml` -```yaml -id: teak_plank -name: teak plank -color: "179" -description: "A fine teak plank with a beautiful grain." -value: 40 -stackable: false -``` - -#### `data/items/mahogany_plank.yaml` -```yaml -id: mahogany_plank -name: mahogany plank -color: "124" -description: "A rich mahogany plank, the finest building material." -value: 100 -stackable: false -``` - -### Furniture Items (Wooden) - -#### `data/items/wooden_shelf.yaml` -```yaml -id: wooden_shelf -name: wooden shelf -color: "137" -description: "A simple wooden shelf. Could hold a few things." -value: 15 -stackable: false -``` - -#### `data/items/wooden_table.yaml` -```yaml -id: wooden_table -name: wooden table -color: "137" -description: "A basic wooden table with four legs." -value: 25 -stackable: false -``` - -#### `data/items/wooden_chair.yaml` -```yaml -id: wooden_chair -name: wooden chair -color: "137" -description: "A simple wooden chair. Not comfortable, but functional." -value: 20 -stackable: false -``` - -#### `data/items/wooden_bench.yaml` -```yaml -id: wooden_bench -name: wooden bench -color: "137" -description: "A long wooden bench for sitting." -value: 30 -stackable: false -``` - -### Furniture Items (Oak) - -#### `data/items/oak_shelf.yaml` -```yaml -id: oak_shelf -name: oak shelf -color: "143" -description: "A sturdy oak shelf with dovetail joints." -value: 50 -stackable: false -``` - -#### `data/items/oak_table.yaml` -```yaml -id: oak_table -name: oak table -color: "143" -description: "A solid oak table with a polished surface." -value: 80 -stackable: false -``` - -#### `data/items/oak_chair.yaml` -```yaml -id: oak_chair -name: oak chair -color: "143" -description: "A well-crafted oak chair with carved armrests." -value: 65 -stackable: false -``` - -#### `data/items/oak_bench.yaml` -```yaml -id: oak_bench -name: oak bench -color: "143" -description: "A heavy oak bench with smooth edges." -value: 95 -stackable: false -``` - -### Furniture Items (Teak) - -#### `data/items/teak_shelf.yaml` -```yaml -id: teak_shelf -name: teak shelf -color: "179" -description: "An elegant teak shelf with a warm finish." -value: 130 -stackable: false -``` - -#### `data/items/teak_table.yaml` -```yaml -id: teak_table -name: teak table -color: "179" -description: "A beautiful teak table with intricate leg carvings." -value: 200 -stackable: false -``` - -#### `data/items/teak_chair.yaml` -```yaml -id: teak_chair -name: teak chair -color: "179" -description: "A refined teak chair with a contoured seat." -value: 165 -stackable: false -``` - -#### `data/items/teak_bench.yaml` -```yaml -id: teak_bench -name: teak bench -color: "179" -description: "A gorgeous teak bench with a curved backrest." -value: 240 -stackable: false -``` - -### Furniture Items (Mahogany) - -#### `data/items/mahogany_shelf.yaml` -```yaml -id: mahogany_shelf -name: mahogany shelf -color: "124" -description: "A luxurious mahogany shelf with beveled edges." -value: 320 -stackable: false -``` - -#### `data/items/mahogany_table.yaml` -```yaml -id: mahogany_table -name: mahogany table -color: "124" -description: "A magnificent mahogany table with brass inlays." -value: 500 -stackable: false -``` - -#### `data/items/mahogany_chair.yaml` -```yaml -id: mahogany_chair -name: mahogany chair -color: "124" -description: "A grand mahogany chair fit for a captain." -value: 400 -stackable: false -``` - -#### `data/items/mahogany_bench.yaml` -```yaml -id: mahogany_bench -name: mahogany bench -color: "124" -description: "A stately mahogany bench with ornate scrollwork." -value: 600 -stackable: false -``` - -## 12. Recipes - -All construction recipes use `type: "construction"`, `station: [workbench]`, and have very slow wait times (20+ ticks for planks, 15+ ticks for furniture). - -### Plank Recipes - -#### `data/recipes/construct_plank.yaml` -```yaml -id: construct_plank -type: construction -level: 1 -xp: 30 -wait: 20 -station: [workbench] -consume: - - items: [logs] - qty: 1 -output: plank -message: "You carefully saw the logs into a plank." -``` - -#### `data/recipes/construct_oak_plank.yaml` -```yaml -id: construct_oak_plank -type: construction -level: 15 -xp: 60 -wait: 24 -station: [workbench] -consume: - - items: [oak_logs] - qty: 1 -output: oak_plank -message: "You saw the oak logs into a fine plank." -``` - -#### `data/recipes/construct_teak_plank.yaml` -```yaml -id: construct_teak_plank -type: construction -level: 35 -xp: 100 -wait: 28 -station: [workbench] -consume: - - items: [teak_logs] - qty: 1 -output: teak_plank -message: "You carefully work the teak logs into a smooth plank." -``` - -#### `data/recipes/construct_mahogany_plank.yaml` -```yaml -id: construct_mahogany_plank -type: construction -level: 50 -xp: 150 -wait: 32 -station: [workbench] -consume: - - items: [mahogany_logs] - qty: 1 -output: mahogany_plank -message: "You painstakingly shape the mahogany logs into a perfect plank." -``` - -### Wooden Furniture Recipes - -#### `data/recipes/construct_wooden_shelf.yaml` -```yaml -id: construct_wooden_shelf -type: construction -level: 1 -xp: 50 -wait: 15 -station: [workbench] -consume: - - items: [plank] - qty: 2 -output: wooden_shelf -message: "You assemble a simple wooden shelf." -``` - -#### `data/recipes/construct_wooden_table.yaml` -```yaml -id: construct_wooden_table -type: construction -level: 5 -xp: 80 -wait: 18 -station: [workbench] -consume: - - items: [plank] - qty: 3 -output: wooden_table -message: "You build a sturdy wooden table." -``` - -#### `data/recipes/construct_wooden_chair.yaml` -```yaml -id: construct_wooden_chair -type: construction -level: 3 -xp: 65 -wait: 16 -station: [workbench] -consume: - - items: [plank] - qty: 2 -output: wooden_chair -message: "You craft a simple wooden chair." -``` - -#### `data/recipes/construct_wooden_bench.yaml` -```yaml -id: construct_wooden_bench -type: construction -level: 8 -xp: 100 -wait: 20 -station: [workbench] -consume: - - items: [plank] - qty: 4 -output: wooden_bench -message: "You construct a long wooden bench." -``` - -### Oak Furniture Recipes - -#### `data/recipes/construct_oak_shelf.yaml` -```yaml -id: construct_oak_shelf -type: construction -level: 20 -xp: 120 -wait: 18 -station: [workbench] -consume: - - items: [oak_plank] - qty: 2 -output: oak_shelf -message: "You assemble a sturdy oak shelf." -``` - -#### `data/recipes/construct_oak_table.yaml` -```yaml -id: construct_oak_table -type: construction -level: 25 -xp: 180 -wait: 22 -station: [workbench] -consume: - - items: [oak_plank] - qty: 3 -output: oak_table -message: "You build a solid oak table." -``` - -#### `data/recipes/construct_oak_chair.yaml` -```yaml -id: construct_oak_chair -type: construction -level: 22 -xp: 150 -wait: 20 -station: [workbench] -consume: - - items: [oak_plank] - qty: 2 -output: oak_chair -message: "You craft a well-made oak chair." -``` - -#### `data/recipes/construct_oak_bench.yaml` -```yaml -id: construct_oak_bench -type: construction -level: 28 -xp: 220 -wait: 24 -station: [workbench] -consume: - - items: [oak_plank] - qty: 4 -output: oak_bench -message: "You construct a heavy oak bench." -``` - -### Teak Furniture Recipes - -#### `data/recipes/construct_teak_shelf.yaml` -```yaml -id: construct_teak_shelf -type: construction -level: 40 -xp: 200 -wait: 20 -station: [workbench] -consume: - - items: [teak_plank] - qty: 2 -output: teak_shelf -message: "You assemble an elegant teak shelf." -``` - -#### `data/recipes/construct_teak_table.yaml` -```yaml -id: construct_teak_table -type: construction -level: 45 -xp: 300 -wait: 25 -station: [workbench] -consume: - - items: [teak_plank] - qty: 3 -output: teak_table -message: "You build a beautiful teak table." -``` - -#### `data/recipes/construct_teak_chair.yaml` -```yaml -id: construct_teak_chair -type: construction -level: 42 -xp: 250 -wait: 22 -station: [workbench] -consume: - - items: [teak_plank] - qty: 2 -output: teak_chair -message: "You craft a refined teak chair." -``` - -#### `data/recipes/construct_teak_bench.yaml` -```yaml -id: construct_teak_bench -type: construction -level: 48 -xp: 360 -wait: 28 -station: [workbench] -consume: - - items: [teak_plank] - qty: 4 -output: teak_bench -message: "You construct a gorgeous teak bench." -``` - -### Mahogany Furniture Recipes - -#### `data/recipes/construct_mahogany_shelf.yaml` -```yaml -id: construct_mahogany_shelf -type: construction -level: 55 -xp: 350 -wait: 24 -station: [workbench] -consume: - - items: [mahogany_plank] - qty: 2 -output: mahogany_shelf -message: "You assemble a luxurious mahogany shelf." -``` - -#### `data/recipes/construct_mahogany_table.yaml` -```yaml -id: construct_mahogany_table -type: construction -level: 60 -xp: 500 -wait: 30 -station: [workbench] -consume: - - items: [mahogany_plank] - qty: 3 -output: mahogany_table -message: "You build a magnificent mahogany table." -``` - -#### `data/recipes/construct_mahogany_chair.yaml` -```yaml -id: construct_mahogany_chair -type: construction -level: 58 -xp: 420 -wait: 26 -station: [workbench] -consume: - - items: [mahogany_plank] - qty: 2 -output: mahogany_chair -message: "You craft a grand mahogany chair." -``` - -#### `data/recipes/construct_mahogany_bench.yaml` -```yaml -id: construct_mahogany_bench -type: construction -level: 65 -xp: 600 -wait: 32 -station: [workbench] -consume: - - items: [mahogany_plank] - qty: 4 -output: mahogany_bench -message: "You construct a stately mahogany bench." -``` - -## 13. Plank Making - -Plank making is the core "slow grind" of Construction. Converting logs to planks is intentionally very slow (20-32 ticks per plank, meaning 12-19 seconds at default 600ms ticks) to make it feel like real labor. - -| Log Type | Plank Output | Level | XP | Wait (ticks) | Wait (seconds at 600ms) | -|----------|-------------|-------|----|-------------|------------------------| -| logs | plank | 1 | 30 | 20 | 12.0 | -| oak_logs | oak_plank | 15 | 60 | 24 | 14.4 | -| teak_logs | teak_plank | 35 | 100 | 28 | 16.8 | -| mahogany_logs | mahogany_plank | 50 | 150 | 32 | 19.2 | - -Planks are non-stackable (like logs), so inventory management is part of the challenge — you can only carry 28 items, so a full inventory of logs becomes a full inventory of planks. - -No success/fail rolls — plank making always succeeds. This keeps it simple and avoids frustrating material loss on an already-slow process. - -## 14. XP Table - -### Plank Making XP - -| Recipe | Level | XP | Ticks | -|--------|-------|----|-------| -| Plank | 1 | 30 | 20 | -| Oak Plank | 15 | 60 | 24 | -| Teak Plank | 35 | 100 | 28 | -| Mahogany Plank | 50 | 150 | 32 | - -### Furniture XP - -| Recipe | Level | XP | Planks | Ticks | -|--------|-------|----|--------|-------| -| Wooden Shelf | 1 | 50 | 2 | 15 | -| Wooden Chair | 3 | 65 | 2 | 16 | -| Wooden Table | 5 | 80 | 3 | 18 | -| Wooden Bench | 8 | 100 | 4 | 20 | -| Oak Shelf | 20 | 120 | 2 | 18 | -| Oak Chair | 22 | 150 | 2 | 20 | -| Oak Table | 25 | 180 | 3 | 22 | -| Oak Bench | 28 | 220 | 4 | 24 | -| Teak Shelf | 40 | 200 | 2 | 20 | -| Teak Chair | 42 | 250 | 2 | 22 | -| Teak Table | 45 | 300 | 3 | 25 | -| Teak Bench | 48 | 360 | 4 | 28 | -| Mahogany Shelf | 55 | 350 | 2 | 24 | -| Mahogany Chair | 58 | 420 | 2 | 26 | -| Mahogany Table | 60 | 500 | 3 | 30 | -| Mahogany Bench | 65 | 600 | 4 | 32 | - -### XP/Hour Efficiency (approximate, at 600ms ticks) - -| Method | XP/tick | XP/hour | -|--------|---------|---------| -| Regular planks | 1.5 | 9,000 | -| Regular furniture (shelf) | 3.3 | 12,000 | -| Oak planks | 2.5 | 15,000 | -| Oak furniture (table) | 8.2 | 29,500 | -| Teak planks | 3.6 | 21,400 | -| Teak furniture (table) | 12.0 | 43,200 | -| Mahogany planks | 4.7 | 28,100 | -| Mahogany furniture (table) | 16.7 | 60,000 | - -## 15. Help Files - -### `data/help/make.yaml` - -```yaml -name: "make" -category: "Skills" -description: | - Construct items at a workbench using the Construction skill. - - Usage: make <item> Build a specific item - make Show all items you can make right now - construct Same as make - - Construction requires a workbench, found in your house's - workshop. Buy a house from the Real Estate Broker at the - Construction Site, then visit it via the Directory in the - Local Neighborhood. - - Plank making (logs to planks) is slow but reliable — no chance - of failure. Furniture crafting uses planks to produce sellable - items for credits. - - At the prompt, type a product number or name to start making. - Partial names work if unambiguous. You can prefix with a count - to limit quantity (e.g., "3 shelf"). Press enter to repeat - the last product. - - See also: construction -``` - -### `data/help/construction.yaml` - -```yaml -name: "construction" -category: "Skills" -description: | - Construction is a production skill for building furniture and - other items from planks at a workbench. - - Getting started: - 1. Visit the Construction Site (south of the skill halls) - 2. Talk to the Real Estate Broker to buy a house (10 credits) - 3. Go north to the Local Neighborhood - 4. Use the Directory to visit your house - 5. Go west to the Workshop - 6. Use "make" or "construct" to start building - - Plank making: Convert logs into planks at the workbench. This - is very slow work. Different log types require different levels - and yield different plank types. - - Furniture: Use planks to build furniture items (shelves, tables, - chairs, benches). These items have credit value and can be sold. - - Log types: logs (level 1), oak (15), teak (35), mahogany (50) - - Use "make" at a workbench to see available recipes. Items you - can't yet make are shown dimmed. - - See also: make -``` - -## Implementation Order - -For an AI agent implementing this, the recommended order is: - -1. **YAML data files first** (no code changes needed to test creation): - - `data/objects/workbench.yaml` - - `data/objects/directory.yaml` - - `data/mobs/real_estate_broker.yaml` - - `data/behaviors/broker_talk.yaml` - - All `data/items/*.yaml` (planks + furniture) - - All `data/recipes/construct_*.yaml` - - `data/help/make.yaml` and `data/help/construction.yaml` - -2. **Room YAML**: - - `data/rooms/150.yaml` (Local Neighborhood) - - Update `data/rooms/16.yaml` (Construction Site — add north exit, broker mob, update description) - -3. **World virtual room system** (`internal/world/world.go`): - - Add `virtualRooms` map and `nextVirtualID` to `World` struct - - Update `New()` to initialize them - - Modify `LoadRoom()` to check virtual rooms first - - Add `RegisterVirtualRoom()`, `AllocateVirtualID()`, `HasVirtualRoom()`, `SetNextVirtualID()` - -4. **Player store** (`internal/player/store.go`): - - Add `ListHomeowners()` method - -5. **Housing logic** (`internal/game/housing.go` — new file): - - `isDirectoryObject()`, `listHomeowners()`, `lookDirectory()` - - `useDirectory()`, `handleVisitHouseInput()` - - `ensurePlayerHouse()`, `registerHouseRooms()` - - `initHousingIDs()` - -6. **Session state** (`internal/net/server.go`): - - Add `StateVisitHouse` constant - -7. **Production type** (`internal/game/action_production.go`): - - Add `"construction"` entry to `productionTypes` map - -8. **Make command** (`internal/game/cmd_make.go` — new file): - - `doMake()` handler following `cmd_cook.go` / `cmd_craft.go` pattern - -9. **Command dispatch** (`internal/game/game.go`): - - Add `"make"`, `"construct"` to `classifyCommand()` Active case - - Add `"make"`, `"construct"` case to `executeCommand()` - - Add `StateVisitHouse` case to `HandleSession()` - -10. **Option** (`internal/player/player.go`): - - Add `make_all` option - -11. **Look/Use hooks**: - - Hook `lookDirectory()` into object look flow in `cmd_look.go` - - Hook `useDirectory()` into `cmd_use.go` or `StartAction` for the directory object - -12. **Startup init** (`cmd/mud/main.go`): - - Call `g.initHousingIDs()` after game creation - -13. **Build and test**: - - `make vet` — verify no compilation errors - - `make test` — run test suite - - `make run` — manual testing - -## `cmd_make.go` Full Implementation - -```go -package game - -import ( - "fmt" - "strings" - - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/player" - "thehouseoficarus/internal/world" -) - -func (g *Game) doMake(sess *net.Session, input string) { - p := sess.Player.(*player.Player) - g.CancelAction(p) - - stationDefID, _ := g.findStation(p.RoomID, []string{"workbench"}) - if stationDefID == "" { - sess.WriteLine("You need a workbench to make things. Visit your house's workshop.") - return - } - - allRecipes, err := g.RecipeStore.LoadAll() - if err != nil { - sess.WriteLine("Error loading recipes.") - return - } - - var makeRecipes []action.RecipeDef - for _, r := range allRecipes { - if r.Type != "construction" { - continue - } - if !stationMatch(stationDefID, r.Station) { - continue - } - makeRecipes = append(makeRecipes, r) - } - - if len(makeRecipes) == 0 { - sess.WriteLine("There's nothing to make here.") - return - } - - if input != "" { - g.doMakeWithInput(sess, p, makeRecipes, input) - return - } - - lastRecipeID, _ := p.Flags["last_make"].(string) - if lastRecipeID != "" { - for i := range makeRecipes { - if makeRecipes[i].ID == lastRecipeID { - r := &makeRecipes[i] - if g.canDoMakeRecipe(p, r) { - g.startMakeRecipe(sess, p, r, 0) - return - } - break - } - } - } - - available := g.availableMakeRecipes(p, makeRecipes) - if len(available) == 0 { - sess.WriteLine("You don't have any materials to make anything.") - return - } - - if p.OptionBool("make_all") && len(available) == 1 { - g.startMakeRecipe(sess, p, &available[0], 0) - return - } - - g.showProductionTable(sess, p, makeRecipes, "Construction", "construction", "last_make", "Make", false) -} - -func (g *Game) doMakeWithInput(sess *net.Session, p *player.Player, makeRecipes []action.RecipeDef, input string) { - qty, productName := parseQty(input) - productName = strings.TrimSpace(productName) - - var matched []action.RecipeDef - for _, r := range makeRecipes { - outDef, _ := g.ItemStore.Load(r.Output) - name := r.Output - if outDef != nil { - name = outDef.Name - } - if world.WordPrefixMatch(productName, name) { - matched = append(matched, r) - } - } - - if len(matched) == 0 { - sess.WriteLine("You can't make that.") - return - } - - var available []action.RecipeDef - for _, r := range matched { - if g.canDoMakeRecipe(p, &r) { - available = append(available, r) - } - } - - if len(available) == 0 { - sess.WriteLine("You don't have the materials or level for that.") - return - } - - if len(available) == 1 { - g.startMakeRecipe(sess, p, &available[0], qty) - return - } - - g.showProductionTable(sess, p, available, "Construction", "construction", "last_make", "Make", false) -} - -func (g *Game) canDoMakeRecipe(p *player.Player, r *action.RecipeDef) bool { - if p.Level(player.Construction) < r.Level { - return false - } - if !r.HasAllItemsQty(p.CountItem) { - return false - } - return true -} - -func (g *Game) availableMakeRecipes(p *player.Player, allMake []action.RecipeDef) []action.RecipeDef { - var result []action.RecipeDef - for _, r := range allMake { - if r.HasAllItemsQty(p.CountItem) { - result = append(result, r) - } - } - return result -} - -func (g *Game) startMakeRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { - if p.Flags == nil { - p.Flags = make(map[string]any) - } - p.Flags["last_make"] = recipe.ID - g.AccountStore.SaveCharacter(p) - g.startProductionFromRecipe(sess, p, recipe, count) -} -``` - -## Edge Cases to Handle - -1. **Player visits someone else's house** — The Directory allows visiting any homeowner's house. `ensurePlayerHouse(targetName)` loads the target's character file, not the visiting player's. - -2. **Server restart** — Virtual rooms are gone. `ensurePlayerHouse()` detects the missing virtual room and re-registers it. `initHousingIDs()` ensures the ID counter doesn't re-allocate existing IDs. - -3. **Player dies in their house** — Death teleports to room 1 (Town Square). Standard behavior, no special handling needed. - -4. **Player quits inside house** — `room_id` is saved to character YAML. On next login, `LoadRoom()` is called — if virtual rooms aren't registered yet, the first `look` or `move` will fail gracefully. The `completeMove` / `doLook` code calls `LoadRoom`; if it returns an error, the player gets "You can't move from here." To handle this: on login/character connection, if the player's `room_id` >= 100000, call `ensurePlayerHouse` to register the virtual rooms before showing the room. Add this check to the character connection flow in `login_char.go`. - -5. **Multiple players in the same house** — Works naturally. The Hub tracks sessions by room ID. Multiple players can be in the same virtual room. Chat (`say`) works normally. - -6. **YAML flag type mismatch** — When `house_room_id` is loaded from YAML, integers may deserialize as `float64` or `int` depending on the YAML parser. The `ensurePlayerHouse` function handles both via type switch. - -7. **Player doesn't have `has_house` but has `house_room_id`** — Shouldn't happen in normal flow, but `ensurePlayerHouse` checks both flags defensively. - -8. **Inventory full during construction** — The standard production system already handles this: "Your inventory is too full!" and cancels the action (`action_production.go:362`). - -9. **No logs/planks** — The `doMake` handler shows "You don't have any materials to make anything." Same pattern as `doCook`. - -10. **Using directory outside neighborhood room** — The directory object only exists in room 150. `use` on a non-existent object in other rooms won't match. No special guard needed. +| `internal/game/action_production.go` | Added `"construction"` to productionTypes | +| `internal/game/cmd_construct.go` | `doConstruct` command handler | +| `internal/game/cmd_estate_directory.go` | Directory handling + teleport | +| `internal/game/action_talk.go` | Added sawmill node action + `processSawmill` | +| `internal/game/action.go` | Added directory interception in StartAction | +| `internal/game/cmd_look.go` | Added directory handling in doLookTarget | +| `internal/game/science.go` | Added Plank Make mod definition | +| `internal/game/cmd_trigger.go` | Added `triggerPlankMake` handler | +| `internal/game/game.go` | Added `construct`/`make` to command dispatch | +| `internal/action/behavior.go` | Added `Sawmill` field to NodeAction | +| `internal/world/world.go` | Extended LoadRoom to support player_housing dir | +| `internal/player/player.go` | Added `construct_all` option | |
