Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c3cb286
Fix missing functionality for chant and learn scroll
csanburn1966 May 1, 2026
f4db0cd
Removed compiled lofp.exe binary from PR
csanburn1966 May 1, 2026
6860bad
Allow smelted ores and purchased steel to be worked for weaponsmithing.
csanburn1966 May 1, 2026
8bbd53f
Allow examine scroll to see what spell it contains
csanburn1966 May 12, 2026
d407590
Allow learning general magic spells from scrolls
csanburn1966 May 15, 2026
ca7dd8b
Implement timed effects and combat changes
Jul 27, 2026
36f00a3
Merge branch 'pr-6' into integration
Jul 27, 2026
ce833f0
Merge branch 'timed-effects' into integration
Jul 27, 2026
e6bbdc0
Use EffectiveStat for gameplay calculations
Jul 27, 2026
6b58fab
Add effective stats and gameplay updates
Jul 27, 2026
ed3e9ab
implement babich root healing
Jul 29, 2026
cb42a56
Fix creature skinning persistence
Jul 29, 2026
7062004
Add ordinal targeting to skin command
Jul 29, 2026
25cea44
Fix skinning state and ordinal targeting
Aug 3, 2026
03d49c6
Merge main into fix-creature-skinning
Aug 3, 2026
5fedeef
Merge pull request #1 from Plateaus/fix-creature-skinning
Plateaus Aug 3, 2026
f17622f
fix ifcarry to check all ineventory
Aug 3, 2026
a4185e4
Run item preverb scripts for GET and DROP
Aug 4, 2026
a59c999
Merge pull request #2 from Plateaus/item-preverb-get-drop
Plateaus Aug 4, 2026
b6ea131
Run item preverb scripts for equipment commands
Aug 5, 2026
f27893c
Merge pull request #3 from Plateaus/item-preverbs
Plateaus Aug 5, 2026
f54ee9f
Add spell durations and progression catch-up
Aug 6, 2026
4db8802
Prevent fleeing through invalid upward exits
Aug 6, 2026
6c96750
Prevent monster upward fleeing
Aug 7, 2026
be9a41d
Merge branch 'monster-flee-fix'
Aug 7, 2026
35c66d8
Clear player combat when monsters flee
Aug 7, 2026
8a7d478
Merge branch 'monster-flee-fix'
Aug 7, 2026
4c92d2c
Add room container put and get support
Aug 7, 2026
8ef22f6
Add portable container support
Aug 7, 2026
36bc855
Add generated treasure chest loot
Aug 8, 2026
0911ee7
Add generated treasure chest loot
Aug 8, 2026
bab0970
Add lockpicking support
Aug 9, 2026
1ea32e5
Support lockpicking locked doors
Aug 9, 2026
6d4c95a
Fix equipment handling and wolf form restrictions
Aug 9, 2026
ed35697
Merge branch 'lockpicking'
Aug 9, 2026
a6bfedc
Add offhand equipment and shield support
Aug 10, 2026
0a14a08
Add shared experience based on combat contribution
Aug 11, 2026
3fc7e71
Add poison and disease status effects
Aug 12, 2026
59f1425
Add stat reroll and character limit
Aug 13, 2026
6fb8ed0
Add bank item deposits and withdrawals
Aug 14, 2026
3f8eba8
Add GM player reports command
Aug 14, 2026
cf394fe
Fix build point progression and add report completion
Aug 15, 2026
b55d03f
Add container volume limits and encumbrance
Aug 15, 2026
9f75afd
Expand script engine support for preverbs, room verbs, spell actions,…
Aug 17, 2026
0ba233c
Add room lighting, light spell, and lightable item support
Aug 18, 2026
6ba720e
Add night vision and directional lighting
Aug 18, 2026
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
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ node_modules/
frontend/dist/
frontend/node_modules/
*.log
engine/lofp
VIBECTL.md
engine/lofp
VIBECTL.md
.vscode/
__debug_bin*
start.batstart.bat
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ FROM alpine:3.21
RUN apk add --no-cache ca-certificates
WORKDIR /app

# Copy original GM HTML pages
COPY ["original/GM Pages/", "gm-pages/"]

# Copy binary
COPY --from=backend /lofp .

Expand Down
38 changes: 28 additions & 10 deletions engine/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ type ClientConn interface {
type Session struct {
Player *engine.Player
Conn ClientConn
CaptureID string // active capture session ID, empty if not recording
lastCmdTime time.Time // rate limiting: last command timestamp
cmdCount int // rate limiting: commands in current window
CaptureID string // active capture session ID, empty if not recording
lastCmdTime time.Time // rate limiting: last command timestamp
cmdCount int // rate limiting: commands in current window
chatTimes []time.Time // chat flood: timestamps of recent broadcasts
cmdTimes []time.Time // command rate: sliding window for 10/10s limit
authFailures int // auth attempt failures (disconnect after 3)
lastActivity time.Time // idle timeout tracking
quitSent bool // QUIT already broadcast departure
authFailures int // auth attempt failures (disconnect after 3)
lastActivity time.Time // idle timeout tracking
quitSent bool // QUIT already broadcast departure
}

// wsConn wraps a gorilla WebSocket connection to implement ClientConn.
Expand Down Expand Up @@ -103,7 +103,6 @@ func (w *wsConn) RemoteAddr() string {
return w.conn.RemoteAddr().String()
}


// getClientIP extracts the real client IP from the request, preferring
// Fly-Client-IP (set by Fly.io proxy), then X-Forwarded-For, then RemoteAddr.
func getClientIP(r *http.Request) string {
Expand Down Expand Up @@ -348,6 +347,18 @@ func (s *Server) setupRoutes() {
api.HandleFunc("/admin/characters/deleted", s.handleAdminListDeletedCharacters).Methods("GET")
api.HandleFunc("/admin/characters/{firstName}/recover", s.handleAdminRecoverCharacter).Methods("PUT")

gmPagesDir := "/app/gm-pages"

if info, err := os.Stat(gmPagesDir); err == nil && info.IsDir() {
s.router.PathPrefix("/gm-pages/").Handler(
http.StripPrefix(
"/gm-pages/",
http.FileServer(http.Dir(gmPagesDir)),
),
)

log.Printf("Serving GM pages from %s", gmPagesDir)
}
// Serve static frontend files in production (if /app/static exists)
staticDir := os.Getenv("LOFP_STATIC_DIR")
if staticDir == "" {
Expand Down Expand Up @@ -490,7 +501,9 @@ func (s *Server) handleGameWS(w http.ResponseWriter, r *http.Request) {
defer func() {
s.connMu.Lock()
s.connsByIP[ip]--
if s.connsByIP[ip] <= 0 { delete(s.connsByIP, ip) }
if s.connsByIP[ip] <= 0 {
delete(s.connsByIP, ip)
}
s.connMu.Unlock()
}()

Expand Down Expand Up @@ -773,7 +786,9 @@ func (s *Server) handleGameWS(w http.ResponseWriter, r *http.Request) {
cutoff := now.Add(-10 * time.Second)
var recentCmds []time.Time
for _, t := range session.cmdTimes {
if t.After(cutoff) { recentCmds = append(recentCmds, t) }
if t.After(cutoff) {
recentCmds = append(recentCmds, t)
}
}
session.cmdTimes = append(recentCmds, now)
if len(session.cmdTimes) > 10 {
Expand All @@ -787,6 +802,7 @@ func (s *Server) handleGameWS(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
playerRoom := session.Player.RoomNumber
result := s.engine.ProcessCommand(ctx, session.Player, cmd.Input)

result.PlayerState = session.Player
result.PromptIndicators = session.Player.PromptIndicators()
s.sendResult(session, result)
Expand All @@ -812,7 +828,9 @@ func (s *Server) handleGameWS(w http.ResponseWriter, r *http.Request) {
cutoff := now.Add(-10 * time.Second)
var recent []time.Time
for _, t := range session.chatTimes {
if t.After(cutoff) { recent = append(recent, t) }
if t.After(cutoff) {
recent = append(recent, t)
}
}
session.chatTimes = recent
if len(session.chatTimes) >= 5 {
Expand Down
9 changes: 7 additions & 2 deletions engine/internal/api/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,8 +665,8 @@ func (s *Server) sshCharacterSelect(sc *sshConn, ctx context.Context, account *a

func (s *Server) sshCreateCharacter(sc *sshConn, ctx context.Context, accountID string) *engine.Player {
existing, _ := s.engine.ListPlayersByAccount(ctx, accountID)
if len(existing) >= 8 {
sc.writeLine(ansiRed + "You can have at most 8 characters." + ansiReset)
if len(existing) >= 3 {
sc.writeLine(ansiRed + "You can have at most 3 characters." + ansiReset)
return nil
}

Expand Down Expand Up @@ -766,6 +766,11 @@ func (s *Server) sshCommandLoop(ctx context.Context, session *Session, sc *sshCo
}

result := s.engine.ProcessCommand(ctx, session.Player, input)

// Expire timed effects
timerMessages := s.engine.UpdatePlayerTimers(session.Player)
result.Messages = append(result.Messages, timerMessages...)

result.PlayerState = session.Player
result.PromptIndicators = session.Player.PromptIndicators()
s.sendResult(session, result)
Expand Down
39 changes: 22 additions & 17 deletions engine/internal/api/telnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ const (
sbByte = 250 // sub-negotiation begin
seByte = 240 // sub-negotiation end

optEcho = 1 // Echo
optSGA = 3 // Suppress Go-Ahead
optTType = 24 // Terminal Type (MTTS)
optNAWS = 31 // Negotiate About Window Size
optMSDP = 69 // MUD Server Data Protocol
optMSSP = 70 // MUD Server Status Protocol
optMCCP2 = 86 // MUD Client Compression Protocol v2
optMXP = 91 // MUD eXtension Protocol
optEcho = 1 // Echo
optSGA = 3 // Suppress Go-Ahead
optTType = 24 // Terminal Type (MTTS)
optNAWS = 31 // Negotiate About Window Size
optMSDP = 69 // MUD Server Data Protocol
optMSSP = 70 // MUD Server Status Protocol
optMCCP2 = 86 // MUD Client Compression Protocol v2
optMXP = 91 // MUD eXtension Protocol
optGMCP = 201 // Generic MUD Communication Protocol

// MSSP sub-negotiation
Expand Down Expand Up @@ -95,7 +95,7 @@ type telnetConn struct {
passwordMode bool

// MCCP2: compressed writer (nil until activated)
compWriter *zlib.Writer
compWriter *zlib.Writer
compActivated bool

// Server reference for MSSP player count
Expand Down Expand Up @@ -382,13 +382,13 @@ func (t *telnetConn) negotiate() {
// We switch to WILL ECHO only for password prompts
t.conn.Write([]byte{
iacByte, wontByte, optEcho, // Client handles echo (normal)
iacByte, willByte, optSGA, // Suppress go-ahead
iacByte, doByte, optNAWS, // Request terminal size
iacByte, willByte, optGMCP, // Offer GMCP
iacByte, willByte, optSGA, // Suppress go-ahead
iacByte, doByte, optNAWS, // Request terminal size
iacByte, willByte, optGMCP, // Offer GMCP
iacByte, willByte, optMCCP2, // Offer MCCP2
iacByte, willByte, optMSSP, // Offer MSSP
iacByte, willByte, optMSDP, // Offer MSDP
iacByte, doByte, optMXP, // Request MXP
iacByte, willByte, optMSSP, // Offer MSSP
iacByte, willByte, optMSDP, // Offer MSDP
iacByte, doByte, optMXP, // Request MXP
})

// Read and respond to client negotiation for up to 2 seconds
Expand Down Expand Up @@ -1341,8 +1341,8 @@ func (s *Server) telnetCharacterSelect(tc *telnetConn, ctx context.Context, acco

func (s *Server) telnetCreateCharacter(tc *telnetConn, ctx context.Context, accountID string) *engine.Player {
existing, _ := s.engine.ListPlayersByAccount(ctx, accountID)
if len(existing) >= 8 {
tc.writeLine(ansiRed + "You can have at most 8 characters." + ansiReset)
if len(existing) >= 3 {
tc.writeLine(ansiRed + "You can have at most 3 characters." + ansiReset)
return nil
}

Expand Down Expand Up @@ -1491,6 +1491,11 @@ func (s *Server) telnetCommandLoop(ctx context.Context, session *Session, tc *te
}

result := s.engine.ProcessCommand(ctx, session.Player, input)

// Expire timed effects
//timerMessages := s.engine.UpdatePlayerTimers(session.Player)
//result.Messages = append(result.Messages, timerMessages...)

result.PlayerState = session.Player
result.PromptIndicators = session.Player.PromptIndicators()
s.sendResult(session, result)
Expand Down
Loading