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 }