Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions _datafiles/world/default/rooms/frostfang/1067.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
roomid: 1067
zone: Frostfang
title: Magic Academy Back Room
description: The dusty back room of the Magic Academy is seldom visited, its air thick
with the quiet weight of forgotten knowledge and the faint scent of old parchment.
Shelves sag beneath the burden of neglected tomes and curious relics, their secrets
long undisturbed. At the heart of the chamber, a glowing dais hums with a steady,
almost breathing rhythm, its light spilling softly across the worn stone floor in
rippling patterns. Arcane sigils flicker and coil along its surface like living
things, whispering of distant places and unseen pathways. The very air around it
shimmers, charged with restrained energy.
biome: city
exits:
southeast:
roomid: 879
tags:
- fast travel
mapx: -3
mapy: -6
mapz: 0
hascoordinates: true
6 changes: 2 additions & 4 deletions _datafiles/world/default/rooms/frostfang/879.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ maplegend: Trainer
exits:
east:
roomid: 5
northwest:
roomid: 1067
spawninfo:
- mobid: 50
message: A mage enters the room.
respawnrate: 2 real minutes
skilltraining:
cast:
min: 1
max: 4
mapx: -1
mapy: -4
mapz: 0
Expand Down
3 changes: 3 additions & 0 deletions _datafiles/world/default/rooms/mystarion/844.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ description: At the very end of Bloodroot Way, the shadows converge, and the air
Shelves are lined with ancient, leather-bound tomes etched with glowing runes, alongside
vials of mysterious, shimmering elixirs. Enchanted relics float in eerie stillness,
their soft glow casting an otherworldly light.
biome: city
exits:
south:
roomid: 843
tags:
- fast travel
mapx: 71
mapy: -4
mapz: 0
Expand Down
14 changes: 7 additions & 7 deletions internal/mapper/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ func TestArrowForDelta(t *testing.T) {
dx, dy, dz int
want rune
}{
{0, -1, 0, '\u2502'}, // north: vertical bar
{0, 1, 0, '\u2502'}, // south: vertical bar
{-1, 0, 0, '\u2500'}, // west: horizontal bar
{1, 0, 0, '\u2500'}, // east: horizontal bar
{1, -1, 0, '\u2571'}, // northeast: slash
{-1, 1, 0, '\u2571'}, // southwest: slash
{1, 1, 0, '\u2572'}, // southeast: backslash
{0, -1, 0, '\u2502'}, // north: vertical bar
{0, 1, 0, '\u2502'}, // south: vertical bar
{-1, 0, 0, '\u2500'}, // west: horizontal bar
{1, 0, 0, '\u2500'}, // east: horizontal bar
{1, -1, 0, '\u2571'}, // northeast: slash
{-1, 1, 0, '\u2571'}, // southwest: slash
{1, 1, 0, '\u2572'}, // southeast: backslash
{-1, -1, 0, '\u2572'}, // northwest: backslash
{0, 0, 1, '^'}, // up
{0, 0, -1, 'v'}, // down
Expand Down
1 change: 1 addition & 0 deletions modules/all-modules.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

290 changes: 290 additions & 0 deletions modules/fasttravel/fasttravel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package fasttravel

import (
"embed"
"fmt"
"sort"
"strings"

"github.com/GoMudEngine/GoMud/internal/events"
"github.com/GoMudEngine/GoMud/internal/items"
"github.com/GoMudEngine/GoMud/internal/plugins"
"github.com/GoMudEngine/GoMud/internal/rooms"
"github.com/GoMudEngine/GoMud/internal/term"
"github.com/GoMudEngine/GoMud/internal/users"
"github.com/GoMudEngine/GoMud/internal/util"
)

var (
//go:embed files/*
files embed.FS
)

const (
fastTravelTag = "fast travel"
dataKeyFmt = "fasttravel-user-%d"
)

func init() {
m := &FastTravelModule{
plug: plugins.New(`fasttravel`, `1.0`),
players: make(map[int]FastTravelData),
}

if err := m.plug.AttachFileSystem(files); err != nil {
panic(err)
}

m.plug.AddUserCommand(`fasttravel`, m.fastTravelCommand, false, false)

m.plug.Web.AdminPage("Config", "fasttravel-config", "html/admin/fasttravel-config.html", true, "Modules", "Fast Travel", nil)

m.plug.ReserveTags(fastTravelTag)

m.plug.Callbacks.SetOnSave(m.onSave)

events.RegisterListener(events.PlayerSpawn{}, m.onPlayerSpawn)
events.RegisterListener(events.PlayerDespawn{}, m.onPlayerDespawn)

rooms.OnRoomLook.Register(m.onRoomLook)
}

// FastTravelData holds the unlocked fast travel room IDs for a single user.
type FastTravelData struct {
UnlockedRoomIds []int `yaml:"unlockedroomids,omitempty"`
}

// FastTravelModule owns all fast travel state.
type FastTravelModule struct {
plug *plugins.Plugin
players map[int]FastTravelData // keyed by userId; loaded on PlayerSpawn
}

func dataKey(userId int) string {
return fmt.Sprintf(dataKeyFmt, userId)
}

func (m *FastTravelModule) load(userId int) FastTravelData {
var data FastTravelData
m.plug.ReadIntoStruct(dataKey(userId), &data)
return data
}

func (m *FastTravelModule) save(userId int, data FastTravelData) {
m.plug.WriteStruct(dataKey(userId), data)
}

func (m *FastTravelModule) onSave() {
for userId, data := range m.players {
m.save(userId, data)
}
}

func (m *FastTravelModule) onPlayerSpawn(e events.Event) events.ListenerReturn {
evt, ok := e.(events.PlayerSpawn)
if !ok {
return events.Continue
}
m.players[evt.UserId] = m.load(evt.UserId)
return events.Continue
}

func (m *FastTravelModule) onPlayerDespawn(e events.Event) events.ListenerReturn {
evt, ok := e.(events.PlayerDespawn)
if !ok {
return events.Continue
}
if data, exists := m.players[evt.UserId]; exists {
m.save(evt.UserId, data)
delete(m.players, evt.UserId)
}
return events.Continue
}

// onRoomLook injects a fast travel alert when the room has the fast travel tag.
func (m *FastTravelModule) onRoomLook(d rooms.RoomTemplateDetails) rooms.RoomTemplateDetails {
for _, t := range d.Tags {
if strings.EqualFold(t, fastTravelTag) {
d.RoomAlerts = append(d.RoomAlerts,
` <ansi fg="yellow-bold">This is a fast travel station!</ansi> <ansi fg="command">fasttravel</ansi> lists destinations.`,
)
return d
}
}
return d
}

// roomIsFastTravel returns true if the room has the fast travel tag.
func roomIsFastTravel(room *rooms.Room) bool {
return room.HasTag(fastTravelTag)
}

// hasUnlocked returns true if the user has unlocked the given room.
func (m *FastTravelModule) hasUnlocked(userId, roomId int) bool {
data := m.players[userId]
for _, id := range data.UnlockedRoomIds {
if id == roomId {
return true
}
}
return false
}

// unlock adds the roomId to the user's unlocked list if not already present.
// Returns true if the room was newly unlocked, false if it was already known.
func (m *FastTravelModule) unlock(userId, roomId int) bool {
if m.hasUnlocked(userId, roomId) {
return false
}
data := m.players[userId]
data.UnlockedRoomIds = append(data.UnlockedRoomIds, roomId)
m.players[userId] = data
return true
}

// travelCost reads the configured gold cost and required item ID from the module config.
// A cost of 0 means no gold is required. An itemId of 0 means no item is required.
func (m *FastTravelModule) travelCost() (goldCost int, itemId int) {
if v, ok := m.plug.Config.Get(`GoldCost`).(int); ok && v > 0 {
goldCost = v
}
if v, ok := m.plug.Config.Get(`RequiredItemId`).(int); ok && v > 0 {
itemId = v
}
return goldCost, itemId
}

// destEntry holds a resolved fast travel destination for display and matching.
type destEntry struct {
roomId int
title string
}

func (m *FastTravelModule) fastTravelCommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) {
if !roomIsFastTravel(room) {
user.SendText(`You must be at a fast travel station to use fast travel.` + term.CRLFStr)
return true, nil
}

// Always unlock the current room when the player uses the command here.
if m.unlock(user.UserId, room.RoomId) {
user.SendText(`<ansi fg="yellow-bold">You have discovered a new fast travel station: </ansi><ansi fg="room-title">` + room.Title + `</ansi><ansi fg="yellow-bold">!</ansi>` + term.CRLFStr)
}

// Build the list of unlocked destinations, excluding the current room.
data := m.players[user.UserId]
destinations := make([]destEntry, 0, len(data.UnlockedRoomIds))
for _, id := range data.UnlockedRoomIds {
if id == room.RoomId {
continue
}
r := rooms.LoadRoom(id)
if r == nil {
continue
}
destinations = append(destinations, destEntry{roomId: id, title: r.Title})
}
sort.Slice(destinations, func(i, j int) bool {
return destinations[i].title < destinations[j].title
})

// List mode.
if rest == `` {
if len(destinations) == 0 {
user.SendText(`<ansi fg="yellow">You have not discovered any other fast travel stations yet.</ansi>` + term.CRLFStr)
return true, nil
}

var sb strings.Builder
sb.WriteString(`<ansi fg="yellow-bold">Fast Travel Destinations:</ansi>` + "\n")
for i, dest := range destinations {
sb.WriteString(fmt.Sprintf(` <ansi fg="cyan">%d)</ansi> <ansi fg="room-title">%s</ansi>`+"\n", i+1, dest.title))
}
user.SendText(sb.String())
return true, nil
}

// Travel mode: match the argument against destination titles.
titles := make([]string, len(destinations))
for i, dest := range destinations {
titles[i] = dest.title
}

closeMatch, exactMatch := util.FindMatchIn(rest, titles...)

matchTitle := exactMatch
if matchTitle == `` {
matchTitle = closeMatch
}
if matchTitle == `` {
user.SendText(fmt.Sprintf(`No fast travel destination matching "<ansi fg="cyan">%s</ansi>" found.`+term.CRLFStr, rest))
return true, nil
}

var destRoomId int
for _, dest := range destinations {
if strings.EqualFold(dest.title, matchTitle) {
destRoomId = dest.roomId
break
}
}
if destRoomId == 0 {
user.SendText(fmt.Sprintf(`No fast travel destination matching "<ansi fg="cyan">%s</ansi>" found.`+term.CRLFStr, rest))
return true, nil
}

destRoom := rooms.LoadRoom(destRoomId)
if destRoom == nil {
user.SendText(`That fast travel destination is no longer available.` + term.CRLFStr)
return true, nil
}

// Enforce travel cost before moving.
goldCost, requiredItemId := m.travelCost()

if goldCost > 0 {
if user.Character.Gold < goldCost {
user.SendText(fmt.Sprintf(`You need at least <ansi fg="gold">%d gold</ansi> to use the fast travel network.`+term.CRLFStr, goldCost))
return true, nil
}
}

var requiredItem items.Item
if requiredItemId > 0 {
itm, found := user.Character.FindInBackpack(fmt.Sprintf(`!%d`, requiredItemId))
if !found {
iSpec := items.GetItemSpec(requiredItemId)
itemName := fmt.Sprintf(`item #%d`, requiredItemId)
if iSpec != nil {
itemName = iSpec.Name
}
user.SendText(fmt.Sprintf(`You need a <ansi fg="itemname">%s</ansi> to use the fast travel network.`+term.CRLFStr, itemName))
return true, nil
}
requiredItem = itm
}

// Deduct gold cost.
if goldCost > 0 {
user.Character.Gold -= goldCost
events.AddToQueue(events.EquipmentChange{
UserId: user.UserId,
GoldChange: -goldCost,
})
}

// Consume required item.
if requiredItem.ItemId > 0 {
user.Character.RemoveItem(requiredItem)
events.AddToQueue(events.ItemOwnership{
UserId: user.UserId,
Item: requiredItem,
Gained: false,
})
}

user.SendText(fmt.Sprintf(`You step through the fast travel network and arrive at <ansi fg="room-title">%s</ansi>.`+term.CRLFStr, destRoom.Title))
rooms.MoveToRoom(user.UserId, destRoomId)

return true, nil
}
15 changes: 15 additions & 0 deletions modules/fasttravel/files/data-overlays/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
################################################################################
# If modified, these settings will be copied into your config overrides
# folder under `Modules.fasttravel`:
#
# Modules:
# fasttravel:
# GoldCost: 0
# RequiredItemId: 0
################################################################################
# - GoldCost -
# Gold required each time a player uses fast travel. Set to 0 to disable.
GoldCost: 0
# - RequiredItemId -
# Item ID that is consumed each time a player uses fast travel. Set to 0 to disable.
RequiredItemId: 0
6 changes: 6 additions & 0 deletions modules/fasttravel/files/data-overlays/keywords.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
help:
command:
travel:
- fasttravel
command-aliases:
'fasttravel': ['ft']
Loading
Loading