eq2go/internal/sign/sign.go

297 lines
7.7 KiB
Go

package sign
import (
"fmt"
"math/rand"
"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 {
// Handle size randomization like the C++ version
if s.Spawn.GetSizeOffset() > 0 {
offset := s.Spawn.GetSizeOffset() + 1
tmpSize := int32(s.Spawn.GetSize()) + (rand.Int31n(int32(offset)) - rand.Int31n(int32(offset)))
if tmpSize < 0 {
tmpSize = 1
} else if tmpSize >= 0xFFFF {
tmpSize = 0xFFFF
}
newSign.Spawn.SetSize(int16(tmpSize))
} else {
newSign.Spawn.SetSize(s.Spawn.GetSize())
}
// Copy other spawn properties
newSign.Spawn.SetDatabaseID(s.Spawn.GetDatabaseID())
newSign.Spawn.SetMerchantID(s.Spawn.GetMerchantID())
newSign.Spawn.SetMerchantType(s.Spawn.GetMerchantType())
// 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) {
// Delegate to spawn serialization
if s.Spawn != nil {
return s.Spawn.Serialize(player, version)
}
return nil, fmt.Errorf("spawn is nil")
}
// 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
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
func (s *Sign) handleTransporter(client Client) error {
zone := client.GetPlayer().GetZone()
if zone == nil {
return fmt.Errorf("player not in zone")
}
transporterID := s.Spawn.GetTransporterID()
// Get transport destinations
destinations, err := zone.GetTransporters(client, transporterID)
if err != nil {
return fmt.Errorf("failed to get transporters: %w", err)
}
if len(destinations) > 0 {
client.SetTemporaryTransportID(0)
return client.ProcessTeleport(s, destinations, transporterID)
}
return nil
}
// 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")
}
entityCommand := s.Spawn.FindEntityCommand(command)
if entityCommand == nil {
return nil // Command not found
}
// Handle mark command specially
if strings.ToLower(entityCommand.Command) == "mark" {
return s.handleMarkCommand(client)
}
// Process the entity command
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)
}
// 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
}