package control import ( "fmt" "dk/internal/database" "dk/internal/helpers/scanner" "zombiezen.com/go/sqlite" ) // Control represents the game control settings in the database type Control struct { ID int `db:"id" json:"id"` WorldSize int `db:"world_size" json:"world_size"` Open int `db:"open" json:"open"` AdminEmail string `db:"admin_email" json:"admin_email"` Class1Name string `db:"class_1_name" json:"class_1_name"` Class2Name string `db:"class_2_name" json:"class_2_name"` Class3Name string `db:"class_3_name" json:"class_3_name"` } // New creates a new Control with sensible defaults func New() *Control { return &Control{ WorldSize: 200, // Default world size Open: 1, // Default open for registration AdminEmail: "", // No admin email by default Class1Name: "Mage", // Default class names Class2Name: "Warrior", Class3Name: "Paladin", } } var controlScanner = scanner.New[Control]() // controlColumns returns the column list for control queries func controlColumns() string { return controlScanner.Columns() } // scanControl populates a Control struct using the fast scanner func scanControl(stmt *sqlite.Stmt) *Control { control := &Control{} controlScanner.Scan(stmt, control) return control } // Find retrieves the control record by ID (typically only ID 1 exists) func Find(id int) (*Control, error) { var control *Control query := `SELECT ` + controlColumns() + ` FROM control WHERE id = ?` err := database.Query(query, func(stmt *sqlite.Stmt) error { control = scanControl(stmt) return nil }, id) if err != nil { return nil, fmt.Errorf("failed to find control: %w", err) } if control == nil { return nil, fmt.Errorf("control with ID %d not found", id) } return control, nil } // Get retrieves the main control record (ID 1) func Get() (*Control, error) { return Find(1) } // Save updates the control record in the database func (c *Control) Save() error { if c.ID == 0 { return fmt.Errorf("cannot save control without ID") } query := `UPDATE control SET world_size = ?, open = ?, admin_email = ?, class_1_name = ?, class_2_name = ?, class_3_name = ? WHERE id = ?` return database.Exec(query, c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name, c.ID) } // Insert saves a new control to the database and sets the ID func (c *Control) Insert() error { if c.ID != 0 { return fmt.Errorf("control already has ID %d, use Save() to update", c.ID) } // Use a transaction to ensure we can get the ID err := database.Transaction(func(tx *database.Tx) error { query := `INSERT INTO control (world_size, open, admin_email, class_1_name, class_2_name, class_3_name) VALUES (?, ?, ?, ?, ?, ?)` if err := tx.Exec(query, c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name); err != nil { return fmt.Errorf("failed to insert control: %w", err) } // Get the last insert ID var id int err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { id = stmt.ColumnInt(0) return nil }) if err != nil { return fmt.Errorf("failed to get insert ID: %w", err) } c.ID = id return nil }) return err } // Delete removes the control record from the database func (c *Control) Delete() error { if c.ID == 0 { return fmt.Errorf("cannot delete control without ID") } return database.Exec("DELETE FROM control WHERE id = ?", c.ID) } // IsOpen returns true if the game world is open for new players func (c *Control) IsOpen() bool { return c.Open == 1 } // SetOpen sets whether the game world is open for new players func (c *Control) SetOpen(open bool) { if open { c.Open = 1 } else { c.Open = 0 } } // Close closes the game world to new players func (c *Control) Close() { c.Open = 0 } // OpenWorld opens the game world to new players func (c *Control) OpenWorld() { c.Open = 1 } // GetClassNames returns all class names as a slice func (c *Control) GetClassNames() []string { classes := make([]string, 0, 3) if c.Class1Name != "" { classes = append(classes, c.Class1Name) } if c.Class2Name != "" { classes = append(classes, c.Class2Name) } if c.Class3Name != "" { classes = append(classes, c.Class3Name) } return classes } // SetClassNames sets all class names from a slice func (c *Control) SetClassNames(classes []string) { // Reset all class names c.Class1Name = "" c.Class2Name = "" c.Class3Name = "" // Set provided class names if len(classes) > 0 { c.Class1Name = classes[0] } if len(classes) > 1 { c.Class2Name = classes[1] } if len(classes) > 2 { c.Class3Name = classes[2] } } // GetClassName returns the name of a specific class (1-3) func (c *Control) GetClassName(classNum int) string { switch classNum { case 1: return c.Class1Name case 2: return c.Class2Name case 3: return c.Class3Name default: return "" } } // SetClassName sets the name of a specific class (1-3) func (c *Control) SetClassName(classNum int, name string) bool { switch classNum { case 1: c.Class1Name = name return true case 2: c.Class2Name = name return true case 3: c.Class3Name = name return true default: return false } } // IsValidClassName returns true if the given name matches one of the configured classes func (c *Control) IsValidClassName(name string) bool { if name == "" { return false } return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name } // GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found func (c *Control) GetClassNumber(name string) int { if name == c.Class1Name && name != "" { return 1 } if name == c.Class2Name && name != "" { return 2 } if name == c.Class3Name && name != "" { return 3 } return 0 } // HasAdminEmail returns true if an admin email is configured func (c *Control) HasAdminEmail() bool { return c.AdminEmail != "" } // IsWorldSizeValid returns true if the world size is within reasonable bounds func (c *Control) IsWorldSizeValid() bool { return c.WorldSize > 0 && c.WorldSize <= 10000 } // GetWorldRadius returns the world radius (half the world size) func (c *Control) GetWorldRadius() int { return c.WorldSize / 2 } // IsWithinWorldBounds returns true if the given coordinates are within world bounds func (c *Control) IsWithinWorldBounds(x, y int) bool { radius := c.GetWorldRadius() return x >= -radius && x <= radius && y >= -radius && y <= radius } // GetWorldBounds returns the minimum and maximum coordinates for the world func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) { radius := c.GetWorldRadius() return -radius, -radius, radius, radius } // ToMap converts the control to a map for efficient template rendering func (c *Control) ToMap() map[string]any { return map[string]any{ "ID": c.ID, "WorldSize": c.WorldSize, "Open": c.Open, "AdminEmail": c.AdminEmail, "Class1Name": c.Class1Name, "Class2Name": c.Class2Name, "Class3Name": c.Class3Name, // Computed values "IsOpen": c.IsOpen(), "ClassNames": c.GetClassNames(), "HasAdminEmail": c.HasAdminEmail(), "IsWorldSizeValid": c.IsWorldSizeValid(), "WorldRadius": c.GetWorldRadius(), "WorldBounds": map[string]int{ "MinX": -c.GetWorldRadius(), "MinY": -c.GetWorldRadius(), "MaxX": c.GetWorldRadius(), "MaxY": c.GetWorldRadius(), }, } }