297 lines
7.7 KiB
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
|
|
}
|