eq2go/internal/sign/sign.go
2025-08-06 12:31:24 -05:00

273 lines
7.6 KiB
Go

package sign
import (
"fmt"
"strings"
)
// Copy creates a deep copy of the sign with size randomization
func (s *Sign) Copy() *Sign {
newSign := NewSign()
// Copy spawn data
if s.Spawn != nil {
// Copy basic spawn properties that exist
newSign.Spawn.SetSize(s.Spawn.GetSize())
newSign.Spawn.SetDatabaseID(s.Spawn.GetDatabaseID())
newSign.Spawn.SetName(s.Spawn.GetName())
newSign.Spawn.SetLevel(s.Spawn.GetLevel())
newSign.Spawn.SetX(s.Spawn.GetX())
newSign.Spawn.SetY(s.Spawn.GetY(), false)
newSign.Spawn.SetZ(s.Spawn.GetZ())
newSign.Spawn.SetSpawnType(s.Spawn.GetSpawnType())
newSign.Spawn.SetFactionID(s.Spawn.GetFactionID())
// TODO: Copy appearance data when spawn system is fully integrated
// TODO: Copy command lists when command system is integrated
// TODO: Copy transporter ID, sounds, loot properties, etc.
}
// Copy sign-specific properties
newSign.widgetID = s.widgetID
newSign.widgetX = s.widgetX
newSign.widgetY = s.widgetY
newSign.widgetZ = s.widgetZ
newSign.signType = s.signType
newSign.title = s.title
newSign.description = s.description
newSign.language = s.language
newSign.zoneX = s.zoneX
newSign.zoneY = s.zoneY
newSign.zoneZ = s.zoneZ
newSign.zoneHeading = s.zoneHeading
newSign.zoneID = s.zoneID
newSign.signDistance = s.signDistance
newSign.includeLocation = s.includeLocation
newSign.includeHeading = s.includeHeading
return newSign
}
// Serialize creates a packet for sending the sign to a client
func (s *Sign) Serialize(player Player, version int16) ([]byte, error) {
// TODO: Implement serialization when spawn system supports it
// Delegate to spawn serialization
// if s.Spawn != nil {
// return s.Spawn.Serialize(player, version)
// }
return nil, fmt.Errorf("sign serialization not yet implemented")
}
// HandleUse processes player interaction with the sign
func (s *Sign) HandleUse(client Client, command string) error {
if client == nil {
return fmt.Errorf("client is nil")
}
player := client.GetPlayer()
if player == nil {
return fmt.Errorf("player is nil")
}
// Check quest requirements if this is from a client (not script)
if !s.meetsQuestRequirements(client) {
return nil // Silently fail if quest requirements not met
}
// Handle transporter functionality first
// TODO: Enable when transporter system is implemented in spawn
// if s.Spawn != nil && s.Spawn.GetTransporterID() > 0 {
// return s.handleTransporter(client)
// }
// Handle zone transport signs
if s.signType == SignTypeZone && s.zoneID > 0 {
return s.handleZoneTransport(client)
}
// Handle entity commands
if len(command) > 0 {
return s.handleEntityCommand(client, command)
}
return nil
}
// meetsQuestRequirements checks if the player meets quest requirements to use the sign
func (s *Sign) meetsQuestRequirements(client Client) bool {
// This is a placeholder implementation
// In the full implementation, this would check:
// - MeetsSpawnAccessRequirements(client.GetPlayer())
// - GetQuestsRequiredOverride() flags
// - appearance.show_command_icon
// For now, assume all requirements are met
return true
}
// handleTransporter processes transporter functionality
// TODO: Enable when transporter system is implemented in spawn
func (s *Sign) handleTransporter(client Client) error {
// Placeholder implementation for transporter functionality
// This will be enabled when the spawn system supports transporters
return fmt.Errorf("transporter system not yet implemented")
}
// handleZoneTransport processes zone transport functionality
func (s *Sign) handleZoneTransport(client Client) error {
player := client.GetPlayer()
// Check distance if sign has distance requirement
if s.signDistance > 0 {
distance := player.GetDistance(s.Spawn)
if distance > s.signDistance {
client.SimpleMessage(ChannelColorYellow, "You are too far away!")
return nil
}
}
// Get zone name from database
zoneName, err := client.GetDatabase().GetZoneName(s.zoneID)
if err != nil || len(zoneName) == 0 {
client.Message(ChannelColorYellow, "Unable to find zone with ID: %d", s.zoneID)
return fmt.Errorf("zone not found: %d", s.zoneID)
}
// Check zone access
if !client.CheckZoneAccess(zoneName) {
return nil // Access denied (client handles message)
}
// Set coordinates if sign has valid zone coordinates
useZoneDefaults := !s.HasZoneCoordinates()
if !useZoneDefaults {
player.SetX(s.zoneX)
player.SetY(s.zoneY)
player.SetZ(s.zoneZ)
player.SetHeading(s.zoneHeading)
} else {
client.SimpleMessage(ChannelColorYellow, "Invalid zone in coords, taking you to a safe point.")
}
// Try instanced zone first, then regular zone
if !client.TryZoneInstance(s.zoneID, useZoneDefaults) {
return client.Zone(zoneName, useZoneDefaults)
}
return nil
}
// handleEntityCommand processes entity commands
func (s *Sign) handleEntityCommand(client Client, command string) error {
if s.Spawn == nil {
return fmt.Errorf("spawn is nil")
}
// TODO: Implement entity command finding when spawn system supports it
// entityCommand := s.Spawn.FindEntityCommand(command)
// For now, handle mark command directly
if strings.ToLower(command) == "mark" {
return s.handleMarkCommand(client)
}
// TODO: Process other entity commands when system is implemented
// zone := client.GetCurrentZone()
// if zone == nil {
// return fmt.Errorf("player not in zone")
// }
//
// player := client.GetPlayer()
// target := player.GetTarget()
//
// return zone.ProcessEntityCommand(entityCommand, player, target)
return nil // Command not handled
}
// handleMarkCommand processes the mark command for marking signs
func (s *Sign) handleMarkCommand(client Client) error {
charID := client.GetCharacterID()
charName, err := client.GetDatabase().GetCharacterName(charID)
if err != nil {
return fmt.Errorf("failed to get character name: %w", err)
}
return client.GetDatabase().SaveSignMark(charID, s.widgetID, charName, client)
}
// GetDisplayText returns the formatted display text for the sign
func (s *Sign) GetDisplayText() string {
var text strings.Builder
if s.HasTitle() {
text.WriteString(s.title)
}
if s.HasDescription() {
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(s.description)
}
// Add location information if requested
if s.includeLocation && s.HasZoneCoordinates() {
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(fmt.Sprintf("Location: %.2f, %.2f, %.2f", s.zoneX, s.zoneY, s.zoneZ))
}
// Add heading information if requested
if s.includeHeading && s.zoneHeading != 0 {
if text.Len() > 0 {
text.WriteByte('\n')
}
text.WriteString(fmt.Sprintf("Heading: %.2f", s.zoneHeading))
}
return text.String()
}
// Validate checks if the sign configuration is valid
func (s *Sign) Validate() []string {
var issues []string
if s.Spawn == nil {
issues = append(issues, "Sign has no spawn data")
return issues
}
if s.widgetID == 0 {
issues = append(issues, "Sign has no widget ID")
}
if len(s.title) > MaxSignTitleLength {
issues = append(issues, fmt.Sprintf("Sign title too long: %d > %d", len(s.title), MaxSignTitleLength))
}
if len(s.description) > MaxSignDescriptionLength {
issues = append(issues, fmt.Sprintf("Sign description too long: %d > %d", len(s.description), MaxSignDescriptionLength))
}
if s.signType == SignTypeZone {
if s.zoneID == 0 {
issues = append(issues, "Zone sign has no zone ID")
}
if s.signDistance < 0 {
issues = append(issues, "Sign distance cannot be negative")
}
}
return issues
}
// IsValid returns true if the sign configuration is valid
func (s *Sign) IsValid() bool {
issues := s.Validate()
return len(issues) == 0
}