package sign import ( "fmt" "math/rand" "strings" "eq2emu/internal/spawn" ) // 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 }