diff options
| -rw-r--r-- | config.yaml | 8 | ||||
| -rw-r--r-- | data/help/color.yaml | 7 | ||||
| -rw-r--r-- | data/help/prompt.yaml | 2 | ||||
| -rw-r--r-- | internal/config/config.go | 10 | ||||
| -rw-r--r-- | internal/game/action_agility.go | 4 | ||||
| -rw-r--r-- | internal/game/action_finishing_blow.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_attack.go | 47 | ||||
| -rw-r--r-- | internal/game/cmd_color.go | 10 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 14 | ||||
| -rw-r--r-- | internal/game/cmd_score.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_trigger_combat.go | 12 | ||||
| -rw-r--r-- | internal/game/core_production.go | 2 | ||||
| -rw-r--r-- | internal/game/sys_combat.go | 2 | ||||
| -rw-r--r-- | internal/game/tick.go | 8 | ||||
| -rw-r--r-- | internal/game/ui_combat.go | 59 | ||||
| -rw-r--r-- | internal/game/ui_help.go | 2 | ||||
| -rw-r--r-- | worldbuilding_guide/README.md | 26 | ||||
| -rw-r--r-- | worldbuilding_guide/behaviors.md | 3 | ||||
| -rw-r--r-- | worldbuilding_guide/construction.md | 2 | ||||
| -rw-r--r-- | worldbuilding_guide/courses.md | 119 | ||||
| -rw-r--r-- | worldbuilding_guide/hacking.md | 22 | ||||
| -rw-r--r-- | worldbuilding_guide/index.md | 2 |
22 files changed, 250 insertions, 115 deletions
diff --git a/config.yaml b/config.yaml index 0bde479..ffe9d3b 100644 --- a/config.yaml +++ b/config.yaml @@ -8,12 +8,10 @@ colors: direction: "75" exit_direction: "75" exit_name: "114" - mob_name: "203" - friendly_npc: "120" - hostile_npc: "203" + protected_mob: "off" + mob: "off" damage: "196" - enemy_hp: "167" - character_hp: "84" + xp: "222" level_up: "226 bold" item: "223" diff --git a/data/help/color.yaml b/data/help/color.yaml index 6082183..12fd4a5 100644 --- a/data/help/color.yaml +++ b/data/help/color.yaml @@ -55,12 +55,9 @@ description: | direction Movement direction text exit_direction Exit direction labels exit_name Exit destination room names - mob_name Mob names in combat - friendly_npc Friendly NPC names - hostile_npc Hostile mob names in room listings + protected_mob Protected mob names + mob Mob names (combat, room listings, etc.) damage Damage numbers - enemy_hp Enemy HP in combat - character_hp Your HP in combat xp XP gain messages level_up Level-up announcements item Item names (default) diff --git a/data/help/prompt.yaml b/data/help/prompt.yaml index f3c23f4..2b35f41 100644 --- a/data/help/prompt.yaml +++ b/data/help/prompt.yaml @@ -14,6 +14,8 @@ description: | Variables: %h Current HP %H Maximum HP + %b Current battery level + %B Maximum battery level %c Credits %m Current mob HP (blank unless in combat) %M Mob max HP (blank unless in combat) diff --git a/internal/config/config.go b/internal/config/config.go index cbf15c5..874d6a3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,12 +25,12 @@ func DefaultColors() ColorsConfig { "direction": "75", "exit_direction": "75", "exit_name": "114", - "mob_name": "203", - "friendly_npc": "120", - "hostile_npc": "203", + "protected_mob": "off", + "mob": "off", + "damage_dealt": "33", + "damage_taken": "196", "damage": "196", - "enemy_hp": "167", - "character_hp": "84", + "xp": "222", "level_up": "226 bold", "item": "223", diff --git a/internal/game/action_agility.go b/internal/game/action_agility.go index 43c9cfd..f916a8d 100644 --- a/internal/game/action_agility.go +++ b/internal/game/action_agility.go @@ -140,13 +140,13 @@ func (g *Game) obstacleFail(sess *net.Session, p *player.Player, data map[string damage = failMin + rand.Intn(failMax-failMin+1) } - sess.WriteLine(g.colorize(sess, "damage", "You slip and fall!")) + sess.WriteLine(g.colorize(sess, "damage_taken", "You slip and fall!")) p.HP -= damage if p.HP < 1 { p.HP = 1 } - sess.WriteLine(g.colorize(sess, "damage", + sess.WriteLine(g.colorize(sess, "damage_taken", fmt.Sprintf("You take %d damage. HP: %d/%d", damage, p.HP, p.MaxHP()))) g.AccountStore.SaveCharacter(p) diff --git a/internal/game/action_finishing_blow.go b/internal/game/action_finishing_blow.go index 674c2ef..070559d 100644 --- a/internal/game/action_finishing_blow.go +++ b/internal/game/action_finishing_blow.go @@ -72,7 +72,7 @@ func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.M if consumed { p.RemoveItem(mob.FinishingBlow, 1) } - sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob_name", mobDisplayName(mob, true)))) + sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob", mobDisplayName(mob, true)))) mob.HP = 0 g.endCombat(sess, p, mob) } diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 89cde46..1ede21f 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -7,6 +7,7 @@ import ( "strings" "thehouseoficarus/internal/action" + "thehouseoficarus/internal/color" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" @@ -149,7 +150,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn if len(styleParts) > 0 { styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" } - sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", g.colorize(sess, "mob_name", mobDisplayName(mob, true)), styleStr)) + sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) } else { mod := GetMod(p.AutotriggerMod) modName := p.AutotriggerMod @@ -157,7 +158,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn modName = mod.Name } sess.WriteLine(fmt.Sprintf("\nYou attack %s with %s!", - g.colorize(sess, "mob_name", mobDisplayName(mob, true)), + g.colorize(sess, "mob", mobDisplayName(mob, true)), g.colorize(sess, "science_mod", modName))) } p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name} @@ -352,7 +353,7 @@ func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.Mo fbName = fbDef.Name } sess.WriteLine(g.colorize(sess, "warning", - fmt.Sprintf(" %s resists death! Use %s on it to finish it off.", + fmt.Sprintf("%s resists death! Use %s on it to finish it off.", mobDisplayName(mob, false), fbName))) } @@ -373,13 +374,17 @@ func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.Mo if !mob.Unique { attacker = "The " + mob.Name } - w := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)) - if w2 := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)); w2 > w { + w := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)) + if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w { w = w2 } - prefix := fmt.Sprintf(" You hit %s for %s damage.", g.colorize(sess, "mob_name", mobName), g.colorize(sess, "damage", fmt.Sprint(dmg))) - hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "enemy_hp", fmt.Sprint(mob.HP)), mob.MaxHP) - line := fmt.Sprintf("%-*s %s", w, prefix, hpPart) + prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) + visLen := color.VisibleLen(prefix) + if visLen < w { + prefix += strings.Repeat(" ", w-visLen+1) + } + hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) + line := prefix + hpSuffix if p.OptionBool("xp_drops") && len(gains) > 0 { var parts []string for _, gain := range gains { @@ -396,7 +401,7 @@ func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.Mo } func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.MobInstance, weaponType object.WeaponType) { - sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true)))) + sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("You miss %s.", mobDisplayName(mob, true)))) if weaponType == object.WeaponRanged { p.AmmoQty-- @@ -487,13 +492,17 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn attacker = "The " + mob.Name } mobName := mobDisplayName(mob, true) - w := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)) - if w2 := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)); w2 > w { + w := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)) + if w2 := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)); w2 > w { w = w2 } - prefix := fmt.Sprintf(" %s hits you for %s damage.", g.colorize(sess, "mob_name", attacker), g.colorize(sess, "damage", fmt.Sprint(dmg))) - hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "character_hp", fmt.Sprint(p.HP)), p.MaxHP()) - sess.WriteLine(fmt.Sprintf("%-*s %s", w, prefix, hpPart)) + prefix := fmt.Sprintf("%s hits you for %s damage.", g.colorize(sess, "mob", attacker), g.colorize(sess, "damage_taken", fmt.Sprint(dmg))) + visLen := color.VisibleLen(prefix) + if visLen < w { + prefix += strings.Repeat(" ", w-visLen+1) + } + hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, p.HP, p.MaxHP()), p.HP, p.MaxHP()) + sess.WriteLine(prefix + hpSuffix) if p.MoveTicks > 0 { p.ClearMoveState() @@ -507,7 +516,7 @@ func (g *Game) applyMobMiss(sess *net.Session, mob *world.MobInstance) { if !mob.Unique { attacker = "The " + mob.Name } - sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" %s misses you.", attacker))) + sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s misses you.", attacker))) } func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { @@ -572,7 +581,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst dropper = "The " + mob.Name } coloredName := g.itemColorize(sess, def, name) - sess.WriteLine(fmt.Sprintf(" %s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) + sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) } if len(mob.Drops.Loot) > 0 { @@ -594,9 +603,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst } coloredName := g.itemColorize(sess, def, name) if qty > 1 { - sess.WriteLine(fmt.Sprintf(" %s %d x %s", g.colorize(sess, "drop_message", dropper+" drops:"), qty, coloredName)) + sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropper+" drops:"), qty, coloredName)) } else { - sess.WriteLine(fmt.Sprintf(" %s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) + sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName)) } } } @@ -684,7 +693,7 @@ func (g *Game) respawnMob(instanceID string) { if p := sess.Player; p != nil && p.OptionBool("mob_spawn") { mobLvl := mobCombatLevel(inst) levelStr := g.levelColorize(sess, p.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl)) - sess.WriteLine(fmt.Sprintf("\n%s %s spawns in the area.", g.colorize(sess, "mob_name", mobDisplayName(inst, false)), levelStr)) + sess.WriteLine(fmt.Sprintf("\n%s %s spawns in the area.", g.colorize(sess, "mob", mobDisplayName(inst, false)), levelStr)) } } } diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 257c965..fe2fe62 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -139,12 +139,12 @@ var colorCategoryOrder = []string{ "direction", "exit_direction", "exit_name", - "mob_name", - "friendly_npc", - "hostile_npc", + "protected_mob", + "mob", + "damage_dealt", + "damage_taken", "damage", - "enemy_hp", - "character_hp", + "xp", "level_up", "item", diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 505f7a6..936a6b0 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -81,7 +81,7 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo for _, m := range mobs { hp := "" if m.HP < m.MaxHP { - hp = fmt.Sprintf(" [%s/%dhp]", g.colorize(sess, "enemy_hp", fmt.Sprint(m.HP)), m.MaxHP) + hp = fmt.Sprintf(" [%s/%dhp]", color.Render(g.colorMode(sess), color.Parse("167"), fmt.Sprint(m.HP)), m.MaxHP) } var desc string if combat.IsMobInCombat(m.InstanceID) { @@ -98,13 +98,13 @@ func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Roo if !m.Unique { displayName = "A " + m.Name } - npcColor := "hostile_npc" + mobColor := "mob" if m.Protected { - npcColor = "friendly_npc" + mobColor = "protected_mob" } mobLevel := mobCombatLevel(m) levelStr := g.levelColorize(sess, playerLevel, mobLevel, fmt.Sprintf("(level %d)", mobLevel)) - sess.WriteLine(fmt.Sprintf(" %s %s%s%s", g.colorize(sess, npcColor, displayName), levelStr, hp, desc)) + sess.WriteLine(fmt.Sprintf(" %s %s%s%s", g.colorize(sess, mobColor, displayName), levelStr, hp, desc)) } } } @@ -466,15 +466,15 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { } } if best != nil { - npcColor := "hostile_npc" + mobColor := "mob" if best.Protected { - npcColor = "friendly_npc" + mobColor = "protected_mob" } mobLevel := mobCombatLevel(best) levelStr := g.levelColorize(sess, p.CombatLevel(), mobLevel, fmt.Sprintf("(level %d)", mobLevel)) sess.WriteLines( "", - fmt.Sprintf("%s %s", g.colorize(sess, npcColor, best.Name), levelStr), + fmt.Sprintf("%s %s", g.colorize(sess, mobColor, best.Name), levelStr), ) if best.IdleDescription != "" { sess.WriteLine(fmt.Sprintf(" %s", best.IdleDescription)) diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index b49fd7f..523dcb9 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -20,7 +20,7 @@ func (g *Game) doScore(sess *net.Session) { "", fmt.Sprintf("Name: %s", g.colorize(sess, "player_name", p.Name)), fmt.Sprintf("Combat Level: %s", color.Render(mode, color.Parse("230"), fmt.Sprint(p.CombatLevel()))), - fmt.Sprintf("HP: %s/%s", g.colorize(sess, "character_hp", fmt.Sprint(p.HP)), color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))), + fmt.Sprintf("HP: %s/%s", color.Render(mode, color.Parse("84"), fmt.Sprint(p.HP)), color.Render(mode, color.Parse("230"), fmt.Sprint(p.MaxHP()))), fmt.Sprintf("Battery: %s/%s", g.colorize(sess, "battery", fmt.Sprintf("%.1f", p.Battery)), color.Render(mode, color.Parse("230"), fmt.Sprintf("%.0f", p.MaxBattery()))), fmt.Sprintf("Credits: %s", g.colorize(sess, "credits_pickup", fmt.Sprint(p.Credits))), ) diff --git a/internal/game/cmd_trigger_combat.go b/internal/game/cmd_trigger_combat.go index 444f80d..2086235 100644 --- a/internal/game/cmd_trigger_combat.go +++ b/internal/game/cmd_trigger_combat.go @@ -62,12 +62,12 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob g.AccountStore.SaveCharacter(p) mobName := mobDisplayName(mob, true) - prefix := fmt.Sprintf(" %s hits %s for %s damage.", + prefix := fmt.Sprintf("%s hits %s for %s damage.", g.colorize(sess, "science_mod", mod.Name), - g.colorize(sess, "mob_name", mobName), - g.colorize(sess, "damage", fmt.Sprint(dmg))) - hpPart := fmt.Sprintf("[%s/%dhp]", g.colorize(sess, "enemy_hp", fmt.Sprint(mob.HP)), mob.MaxHP) - line := prefix + " " + hpPart + g.colorize(sess, "mob", mobName), + g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) + hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) + line := prefix + " " + hpSuffix if p.OptionBool("xp_drops") && len(gains) > 0 { var parts []string for _, gain := range gains { @@ -77,7 +77,7 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob } sess.WriteLine(line) } else { - sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" %s fails to connect.", mod.Name))) + sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s fails to connect.", mod.Name))) } return true } diff --git a/internal/game/core_production.go b/internal/game/core_production.go index 47a164c..b4101a9 100644 --- a/internal/game/core_production.go +++ b/internal/game/core_production.go @@ -286,7 +286,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem) if msg != "" { - sess.WriteLine(g.colorize(sess, "damage", msg)) + sess.WriteLine(g.colorize(sess, "damage_taken", msg)) } } diff --git a/internal/game/sys_combat.go b/internal/game/sys_combat.go index 702871c..81b2c83 100644 --- a/internal/game/sys_combat.go +++ b/internal/game/sys_combat.go @@ -105,7 +105,7 @@ func (g *Game) checkAggro(sess *net.Session) { if !aggMob.Unique { attacker = "The " + aggMob.Name } - sess.WriteLine(fmt.Sprintf("\n%s attacks you!", g.colorize(sess, "mob_name", attacker))) + sess.WriteLine(fmt.Sprintf("\n%s attacks you!", g.colorize(sess, "mob", attacker))) g.startCombat(sess, p, aggMob) return false }) diff --git a/internal/game/tick.go b/internal/game/tick.go index ec7eda9..1402b06 100644 --- a/internal/game/tick.go +++ b/internal/game/tick.go @@ -142,17 +142,17 @@ func (g *Game) WanderTick() { if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { if m.level > 0 { levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) - sess.WriteLine(fmt.Sprintf("\n%s %s leaves.", g.colorize(sess, "mob_name", m.name), levelStr)) + sess.WriteLine(fmt.Sprintf("\n%s %s leaves.", g.colorize(sess, "mob", m.name), levelStr)) } else { - sess.WriteLine(fmt.Sprintf("\n%s moves away.", g.colorize(sess, "mob_name", m.name))) + sess.WriteLine(fmt.Sprintf("\n%s moves away.", g.colorize(sess, "mob", m.name))) } } if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { if m.level > 0 { levelStr := g.levelColorize(sess, p.CombatLevel(), m.level, fmt.Sprintf("(level %d)", m.level)) - sess.WriteLine(fmt.Sprintf("\n%s %s enters.", g.colorize(sess, "mob_name", m.name), levelStr)) + sess.WriteLine(fmt.Sprintf("\n%s %s enters.", g.colorize(sess, "mob", m.name), levelStr)) } else { - sess.WriteLine(fmt.Sprintf("\n%s drifts in.", g.colorize(sess, "mob_name", m.name))) + sess.WriteLine(fmt.Sprintf("\n%s drifts in.", g.colorize(sess, "mob", m.name))) } } } diff --git a/internal/game/ui_combat.go b/internal/game/ui_combat.go new file mode 100644 index 0000000..56de7e8 --- /dev/null +++ b/internal/game/ui_combat.go @@ -0,0 +1,59 @@ +package game + +import ( + "math" + "strings" + + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" +) + +func (g *Game) hpBar(sess *net.Session, current, max int) string { + if max <= 0 { + max = 1 + } + if current < 0 { + current = 0 + } + if current > max { + current = max + } + + pct := float64(current) / float64(max) * 100.0 + filled := int(math.Round(pct / 10.0)) + if filled < 0 { + filled = 0 + } + if filled > 10 { + filled = 10 + } + + mode := g.colorMode(sess) + + var fgColor int + switch { + case pct >= 70: + fgColor = 82 + case pct >= 40: + fgColor = 220 + default: + fgColor = 160 + } + + fillSpec := color.ColorSpec{Fg: fgColor} + emptySpec := color.ColorSpec{Fg: 238, Dim: true} + + fillChar := "█" + emptyChar := "░" + if sess.Player != nil && !sess.Player.OptionBool("unicode") { + fillChar = "#" + emptyChar = "." + } + + var sb strings.Builder + sb.WriteString("[") + sb.WriteString(color.Render(mode, fillSpec, strings.Repeat(fillChar, filled))) + sb.WriteString(color.Render(mode, emptySpec, strings.Repeat(emptyChar, 10-filled))) + sb.WriteString("]") + return sb.String() +} diff --git a/internal/game/ui_help.go b/internal/game/ui_help.go index 151d1ba..ee8ad97 100644 --- a/internal/game/ui_help.go +++ b/internal/game/ui_help.go @@ -70,7 +70,7 @@ var commandList = []cmdEntry{ {"steal", "Active", "Steal from mobs (Thieving)"}, {"stoke", "Active", "Add logs to a fire"}, {"style", "Instant", "Change combat style"}, - {"talk / speak / ask", "Active", "Talk to NPCs"}, + {"talk / speak / ask", "Active", "Talk to mobs"}, {"tech", "Instant", "Toggle technology abilities"}, {"trigger", "Active", "Trigger a science module"}, {"unalias", "Instant", "Remove command shortcuts"}, diff --git a/worldbuilding_guide/README.md b/worldbuilding_guide/README.md deleted file mode 100644 index 0bea82c..0000000 --- a/worldbuilding_guide/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# World Building Guide - -The House of Icarus is data-driven. Everything — rooms, items, mobs, objects, behaviors, drop tables, and recipes — is defined in YAML files under `data/`. No code changes needed to build a world. - -## Files - -| Document | Covers | -|----------|--------| -| [Rooms](rooms.md) | Room definitions, exits (simple and conditional), spawns, placed objects and mobs, on_enter scripts | -| [Items](items.md) | Item definitions, stats, equip slots, weapon types, tool types, firemaking, food/healing | -| [Mobs](mobs.md) | Mob definitions, combat stats, drops, behavior, idle descriptions | -| [Objects](objects.md) | Interactive objects, behaviors, hidden objects, in-room descriptions | -| [Behaviors](behaviors.md) | Gather, use, talk behaviors with full YAML reference | -| [Recipes](recipes.md) | Recipe system for cooking, crafting, smithing — station and item-on-item | -| [Conditions](conditions.md) | All condition types: flag, player_flag, has_item, all_of, any_of, not | -| [Drop Tables](drops.md) | Shared weighted drop tables for mob loot and search tables | -| [State](state.md) | World flags vs player flags — global vs per-character state | -| [Doors](doors.md) | Door examples with world flags and player flags | -| [Quests](quests.md) | Complete multi-room quest example with talk behaviors | -| [Wandering Objects](wandering_objects.md) | Objects that teleport between rooms on a timer | -| [Wandering Mobs](wandering_mobs.md) | Mobs that wander through legal exits | -| [Search](search.md) | The search command and searchable items | -| [Hidden Objects](hidden_objects.md) | Hidden interactive objects | -| [Bank](bank.md) | Bank system — deposit, withdraw, browse, bank booth placement | -| [Construction](construction.md) | Construction skill — planks, furniture, houses, sawmill, Plank Make | -| [Tips](tips.md) | Practical tips for building a world | diff --git a/worldbuilding_guide/behaviors.md b/worldbuilding_guide/behaviors.md index 1934f5b..08dc2d1 100644 --- a/worldbuilding_guide/behaviors.md +++ b/worldbuilding_guide/behaviors.md @@ -1,8 +1,7 @@ ## Behaviors Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`, -`use:`) or mob YAML (`talk:`). There is no separate `data/behaviors/` -directory. Each section below shows the keys you can use under each behavior type. +`use:`) or mob YAML (`talk:`). Each section below shows the keys you can use under each behavior type. ### Gather (mining, fishing, woodcutting) diff --git a/worldbuilding_guide/construction.md b/worldbuilding_guide/construction.md index 63249b3..2c4b7ce 100644 --- a/worldbuilding_guide/construction.md +++ b/worldbuilding_guide/construction.md @@ -212,7 +212,7 @@ This enables automatic support through the unified production system (`advancePr | `data/items/saw.yaml` | Saw tool | | `data/items/planks.yaml` - `mahogany_planks.yaml` | Plank items (4 tiers) | | `data/items/wooden_shelf.yaml` - `mahogany_table.yaml` | Furniture items (7 items) | -| `data/recipes/construct_planks.yaml` - `construct_mahogany_table.yaml` | Construction recipes (11 recipes) | +| Item YAMLs with `craft:` blocks (planks, furniture) | Construction recipes — inline on output item YAML | | `data/mobs/estate_broker.yaml` | Estate broker mob (talk inline) | | `data/mobs/sawmill_operator.yaml` | Sawmill operator mob (talk inline) | | `data/rooms/16.yaml` | Construction Site (updated) | diff --git a/worldbuilding_guide/courses.md b/worldbuilding_guide/courses.md new file mode 100644 index 0000000..491e2bd --- /dev/null +++ b/worldbuilding_guide/courses.md @@ -0,0 +1,119 @@ +## Agility Courses + +Agility courses are defined in `data/courses/<id>.yaml`. Each course defines a sequence of obstacle rooms, each with a specific verb the player must type to advance. Obstacle rooms themselves are standard room YAML files (typically in `data/rooms/agility/`). + +### Course YAML + +```yaml +id: "vent_shaft" +name: "Ventilation Shaft Course" +required_level: 1 +start_room: 200 +completion_xp: 40 +obstacles: + - room_id: 201 + verb: scramble + ticks_per_phase: 2 + xp: 8 + fail_damage: [1, 2] + messages: + - "You approach the corroded ventilation wall..." + - "You find footholds in the rusted panels and begin to climb..." + - "You scramble up the wall and haul yourself onto the ledge!" + - room_id: 202 + verb: balance + ticks_per_phase: 2 + xp: 8 + fail_damage: [1, 2] + messages: + - "You step onto the narrow coolant pipe..." + - "Arms outstretched, you carefully place one foot in front of the other..." + - "You reach the other side of the pipe and step onto solid ground!" +``` + +### CourseConfig Fields + +| Field | Type | Description | +| ---------------- | ------------- | ---------------------------------------------------- | +| `id` | string | Unique course identifier | +| `name` | string | Display name shown to players | +| `required_level` | int | Minimum Agility level to attempt the course | +| `start_room` | int | Hub room — teleported here on fail or lap completion | +| `completion_xp` | int | Bonus XP awarded when a full lap is completed | +| `obstacles` | []ObstacleDef | Ordered list of obstacles | + +### ObstacleDef Fields + +| Field | Type | Description | +| ----------------- | -------- | --------------------------------------------------------------------------- | +| `room_id` | int | Room ID for this obstacle | +| `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) | +| `ticks_per_phase` | float64 | Ticks between each phase message (supports fractional via `engine.ToTicks`) | +| `xp` | int | XP awarded for successfully completing this single obstacle | +| `fail_damage` | [2]int | `[min, max]` damage on failure (HP clamped to minimum 1 — cannot kill) | +| `messages` | []string | Exactly 3 strings for the 3-phase advancement system | + +### 3-Phase Message System + +Each obstacle advances through 3 phases, printing one message per phase: + +``` +Phase 0 (start): messages[0] printed immediately +Phase 1 (middle): messages[1] printed — FAILURE CHECK happens here +Phase 2 (end): messages[2] printed — XP awarded, teleport to next room +``` + +Failure only occurs at phase 1. Success chance is based on Agility level: +- At required level: 70% success (30% fail) +- Each level above reduces fail chance by 1% (down to minimum 5%) +- Fail chance capped at 60% + +On failure, the player takes random damage in `[fail_damage[0], fail_damage[1]]`, is teleported back to `start_room`, and must restart the course. + +### Lap Counting + +Completing all obstacles in a course increments a lap counter stored as a player flag (`agility_laps_<course_id>`). The `completion_xp` bonus is awarded on the final obstacle only. + +### Obstacle Room YAML + +Each obstacle room should have an `on_enter` message telling the player which verb to use, and a `down` exit back to the course hub: + +```yaml +id: 201 +name: "Ventilation Shaft - Corroded Wall" +description: "A towering wall of corroded ventilation panels..." +on_enter: + - message: "Type 'scramble' to climb the wall." +exits: + down: + room: 200 + blocked_message: "" +``` + +The hub room (e.g. room 200) links to the first obstacle of each course: + +```yaml +id: 200 +name: "Agility Training Grounds" +exits: + south: 17 + north: 201 # Vent Shaft (level 1) + east: 210 # Rooftop (level 20) + west: 220 # Reactor (level 50) +``` + +### Supported Obstacle Verbs + +| Verb | Gerund (display) | +| ---------- | ---------------- | +| `scramble` | scrambling | +| `jump` | jumping | +| `swing` | swinging | +| `balance` | balancing | +| `climb` | climbing | +| `crawl` | crawling | +| `vault` | vaulting | +| `leap` | leaping | +| `slide` | sliding | + +Obstacle verbs are auto-registered at startup from all course YAML files. No changes to `cmd_registry.go` needed when adding new courses — any verb in a `data/courses/` file will work. diff --git a/worldbuilding_guide/hacking.md b/worldbuilding_guide/hacking.md index 6f9bf9a..8927b3e 100644 --- a/worldbuilding_guide/hacking.md +++ b/worldbuilding_guide/hacking.md @@ -46,26 +46,4 @@ type XPBonuser interface { } ``` -### Agility Courses -Agility courses are defined in `data/courses/<id>.yaml`. Each course defines a sequence of obstacle rooms with specific verbs. - -```yaml -id: "vent_shaft" -name: "Ventilation Shaft Course" -required_level: 1 -start_room: 200 -completion_xp: 40 -obstacles: - - room_id: 201 - verb: scramble - ticks_per_phase: 2 - xp: 8 - fail_damage: [1, 2] - messages: - - "You approach the corroded ventilation wall..." - - "You find footholds in the rusted panels and begin to climb..." - - "You scramble up the wall and haul yourself onto the ledge!" -``` - -Each obstacle room should have an `on_enter` message telling the player which verb to use, and a `down` exit back to the course hub. diff --git a/worldbuilding_guide/index.md b/worldbuilding_guide/index.md index 83b3782..e79ee1b 100644 --- a/worldbuilding_guide/index.md +++ b/worldbuilding_guide/index.md @@ -7,7 +7,7 @@ | A mob (NPC, monster) | `data/mobs/<id>.yaml` | | An interactive object (rock, lever, door) | `data/objects/<id>.yaml` | | A shared drop table | `data/drops/<id>.yaml` | -| A crafting recipe | `data/recipes/<id>.yaml` | +| A crafting recipe | In the output item's YAML under a `craft:` block | | A help topic | `data/help/<id>.yaml` | | A science module | `data/modules/<id>.yaml` | | An agility course | `data/courses/<id>.yaml` | |
