fix heroic_ops
This commit is contained in:
parent
5948eac67e
commit
ecadf002e2
@ -1,836 +0,0 @@
|
|||||||
package heroic_ops
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"zombiezen.com/go/sqlite"
|
|
||||||
"zombiezen.com/go/sqlite/sqlitex"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DatabaseHeroicOPManager implements HeroicOPDatabase interface using sqlitex.Pool
|
|
||||||
type DatabaseHeroicOPManager struct {
|
|
||||||
pool *sqlitex.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabaseHeroicOPManager creates a new database heroic OP manager
|
|
||||||
func NewDatabaseHeroicOPManager(pool *sqlitex.Pool) *DatabaseHeroicOPManager {
|
|
||||||
return &DatabaseHeroicOPManager{
|
|
||||||
pool: pool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadStarters retrieves all starters from database
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadStarters(ctx context.Context) ([]HeroicOPData, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, ho_type, starter_class, starter_icon, 0 as starter_link_id,
|
|
||||||
0 as chain_order, 0 as shift_icon, 0 as spell_id, 0.0 as chance,
|
|
||||||
ability1, ability2, ability3, ability4, ability5, ability6,
|
|
||||||
name, description FROM heroic_ops WHERE ho_type = ?`
|
|
||||||
|
|
||||||
var starters []HeroicOPData
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{HOTypeStarter},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
var starter HeroicOPData
|
|
||||||
starter.ID = int32(stmt.ColumnInt64(0))
|
|
||||||
starter.HOType = stmt.ColumnText(1)
|
|
||||||
starter.StarterClass = int8(stmt.ColumnInt64(2))
|
|
||||||
starter.StarterIcon = int16(stmt.ColumnInt64(3))
|
|
||||||
starter.StarterLinkID = int32(stmt.ColumnInt64(4))
|
|
||||||
starter.ChainOrder = int8(stmt.ColumnInt64(5))
|
|
||||||
starter.ShiftIcon = int16(stmt.ColumnInt64(6))
|
|
||||||
starter.SpellID = int32(stmt.ColumnInt64(7))
|
|
||||||
starter.Chance = float32(stmt.ColumnFloat(8))
|
|
||||||
starter.Ability1 = int16(stmt.ColumnInt64(9))
|
|
||||||
starter.Ability2 = int16(stmt.ColumnInt64(10))
|
|
||||||
starter.Ability3 = int16(stmt.ColumnInt64(11))
|
|
||||||
starter.Ability4 = int16(stmt.ColumnInt64(12))
|
|
||||||
starter.Ability5 = int16(stmt.ColumnInt64(13))
|
|
||||||
starter.Ability6 = int16(stmt.ColumnInt64(14))
|
|
||||||
if stmt.ColumnType(15) != sqlite.TypeNull {
|
|
||||||
starter.Name = stmt.ColumnText(15)
|
|
||||||
}
|
|
||||||
if stmt.ColumnType(16) != sqlite.TypeNull {
|
|
||||||
starter.Description = stmt.ColumnText(16)
|
|
||||||
}
|
|
||||||
starters = append(starters, starter)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query heroic op starters: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return starters, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadStarter retrieves a specific starter from database
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadStarter(ctx context.Context, starterID int32) (*HeroicOPData, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, ho_type, starter_class, starter_icon, 0 as starter_link_id,
|
|
||||||
0 as chain_order, 0 as shift_icon, 0 as spell_id, 0.0 as chance,
|
|
||||||
ability1, ability2, ability3, ability4, ability5, ability6,
|
|
||||||
name, description FROM heroic_ops WHERE id = ? AND ho_type = ?`
|
|
||||||
|
|
||||||
var starter HeroicOPData
|
|
||||||
var found bool
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{starterID, HOTypeStarter},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
found = true
|
|
||||||
starter.ID = int32(stmt.ColumnInt64(0))
|
|
||||||
starter.HOType = stmt.ColumnText(1)
|
|
||||||
starter.StarterClass = int8(stmt.ColumnInt64(2))
|
|
||||||
starter.StarterIcon = int16(stmt.ColumnInt64(3))
|
|
||||||
starter.StarterLinkID = int32(stmt.ColumnInt64(4))
|
|
||||||
starter.ChainOrder = int8(stmt.ColumnInt64(5))
|
|
||||||
starter.ShiftIcon = int16(stmt.ColumnInt64(6))
|
|
||||||
starter.SpellID = int32(stmt.ColumnInt64(7))
|
|
||||||
starter.Chance = float32(stmt.ColumnFloat(8))
|
|
||||||
starter.Ability1 = int16(stmt.ColumnInt64(9))
|
|
||||||
starter.Ability2 = int16(stmt.ColumnInt64(10))
|
|
||||||
starter.Ability3 = int16(stmt.ColumnInt64(11))
|
|
||||||
starter.Ability4 = int16(stmt.ColumnInt64(12))
|
|
||||||
starter.Ability5 = int16(stmt.ColumnInt64(13))
|
|
||||||
starter.Ability6 = int16(stmt.ColumnInt64(14))
|
|
||||||
if stmt.ColumnType(15) != sqlite.TypeNull {
|
|
||||||
starter.Name = stmt.ColumnText(15)
|
|
||||||
}
|
|
||||||
if stmt.ColumnType(16) != sqlite.TypeNull {
|
|
||||||
starter.Description = stmt.ColumnText(16)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load heroic op starter %d: %w", starterID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("heroic op starter %d not found", starterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &starter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadWheels retrieves all wheels from database
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadWheels(ctx context.Context) ([]HeroicOPData, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, ho_type, 0 as starter_class, 0 as starter_icon, starter_link_id,
|
|
||||||
chain_order, shift_icon, spell_id, chance,
|
|
||||||
ability1, ability2, ability3, ability4, ability5, ability6,
|
|
||||||
name, description FROM heroic_ops WHERE ho_type = ?`
|
|
||||||
|
|
||||||
var wheels []HeroicOPData
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{HOTypeWheel},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
var wheel HeroicOPData
|
|
||||||
wheel.ID = int32(stmt.ColumnInt64(0))
|
|
||||||
wheel.HOType = stmt.ColumnText(1)
|
|
||||||
wheel.StarterClass = int8(stmt.ColumnInt64(2))
|
|
||||||
wheel.StarterIcon = int16(stmt.ColumnInt64(3))
|
|
||||||
wheel.StarterLinkID = int32(stmt.ColumnInt64(4))
|
|
||||||
wheel.ChainOrder = int8(stmt.ColumnInt64(5))
|
|
||||||
wheel.ShiftIcon = int16(stmt.ColumnInt64(6))
|
|
||||||
wheel.SpellID = int32(stmt.ColumnInt64(7))
|
|
||||||
wheel.Chance = float32(stmt.ColumnFloat(8))
|
|
||||||
wheel.Ability1 = int16(stmt.ColumnInt64(9))
|
|
||||||
wheel.Ability2 = int16(stmt.ColumnInt64(10))
|
|
||||||
wheel.Ability3 = int16(stmt.ColumnInt64(11))
|
|
||||||
wheel.Ability4 = int16(stmt.ColumnInt64(12))
|
|
||||||
wheel.Ability5 = int16(stmt.ColumnInt64(13))
|
|
||||||
wheel.Ability6 = int16(stmt.ColumnInt64(14))
|
|
||||||
if stmt.ColumnType(15) != sqlite.TypeNull {
|
|
||||||
wheel.Name = stmt.ColumnText(15)
|
|
||||||
}
|
|
||||||
if stmt.ColumnType(16) != sqlite.TypeNull {
|
|
||||||
wheel.Description = stmt.ColumnText(16)
|
|
||||||
}
|
|
||||||
wheels = append(wheels, wheel)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query heroic op wheels: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return wheels, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadWheelsForStarter retrieves wheels for a specific starter
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadWheelsForStarter(ctx context.Context, starterID int32) ([]HeroicOPData, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, ho_type, 0 as starter_class, 0 as starter_icon, starter_link_id,
|
|
||||||
chain_order, shift_icon, spell_id, chance,
|
|
||||||
ability1, ability2, ability3, ability4, ability5, ability6,
|
|
||||||
name, description FROM heroic_ops WHERE starter_link_id = ? AND ho_type = ?`
|
|
||||||
|
|
||||||
var wheels []HeroicOPData
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{starterID, HOTypeWheel},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
var wheel HeroicOPData
|
|
||||||
wheel.ID = int32(stmt.ColumnInt64(0))
|
|
||||||
wheel.HOType = stmt.ColumnText(1)
|
|
||||||
wheel.StarterClass = int8(stmt.ColumnInt64(2))
|
|
||||||
wheel.StarterIcon = int16(stmt.ColumnInt64(3))
|
|
||||||
wheel.StarterLinkID = int32(stmt.ColumnInt64(4))
|
|
||||||
wheel.ChainOrder = int8(stmt.ColumnInt64(5))
|
|
||||||
wheel.ShiftIcon = int16(stmt.ColumnInt64(6))
|
|
||||||
wheel.SpellID = int32(stmt.ColumnInt64(7))
|
|
||||||
wheel.Chance = float32(stmt.ColumnFloat(8))
|
|
||||||
wheel.Ability1 = int16(stmt.ColumnInt64(9))
|
|
||||||
wheel.Ability2 = int16(stmt.ColumnInt64(10))
|
|
||||||
wheel.Ability3 = int16(stmt.ColumnInt64(11))
|
|
||||||
wheel.Ability4 = int16(stmt.ColumnInt64(12))
|
|
||||||
wheel.Ability5 = int16(stmt.ColumnInt64(13))
|
|
||||||
wheel.Ability6 = int16(stmt.ColumnInt64(14))
|
|
||||||
if stmt.ColumnType(15) != sqlite.TypeNull {
|
|
||||||
wheel.Name = stmt.ColumnText(15)
|
|
||||||
}
|
|
||||||
if stmt.ColumnType(16) != sqlite.TypeNull {
|
|
||||||
wheel.Description = stmt.ColumnText(16)
|
|
||||||
}
|
|
||||||
wheels = append(wheels, wheel)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query wheels for starter %d: %w", starterID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return wheels, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadWheel retrieves a specific wheel from database
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadWheel(ctx context.Context, wheelID int32) (*HeroicOPData, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, ho_type, 0 as starter_class, 0 as starter_icon, starter_link_id,
|
|
||||||
chain_order, shift_icon, spell_id, chance,
|
|
||||||
ability1, ability2, ability3, ability4, ability5, ability6,
|
|
||||||
name, description FROM heroic_ops WHERE id = ? AND ho_type = ?`
|
|
||||||
|
|
||||||
var wheel HeroicOPData
|
|
||||||
var found bool
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{wheelID, HOTypeWheel},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
found = true
|
|
||||||
wheel.ID = int32(stmt.ColumnInt64(0))
|
|
||||||
wheel.HOType = stmt.ColumnText(1)
|
|
||||||
wheel.StarterClass = int8(stmt.ColumnInt64(2))
|
|
||||||
wheel.StarterIcon = int16(stmt.ColumnInt64(3))
|
|
||||||
wheel.StarterLinkID = int32(stmt.ColumnInt64(4))
|
|
||||||
wheel.ChainOrder = int8(stmt.ColumnInt64(5))
|
|
||||||
wheel.ShiftIcon = int16(stmt.ColumnInt64(6))
|
|
||||||
wheel.SpellID = int32(stmt.ColumnInt64(7))
|
|
||||||
wheel.Chance = float32(stmt.ColumnFloat(8))
|
|
||||||
wheel.Ability1 = int16(stmt.ColumnInt64(9))
|
|
||||||
wheel.Ability2 = int16(stmt.ColumnInt64(10))
|
|
||||||
wheel.Ability3 = int16(stmt.ColumnInt64(11))
|
|
||||||
wheel.Ability4 = int16(stmt.ColumnInt64(12))
|
|
||||||
wheel.Ability5 = int16(stmt.ColumnInt64(13))
|
|
||||||
wheel.Ability6 = int16(stmt.ColumnInt64(14))
|
|
||||||
if stmt.ColumnType(15) != sqlite.TypeNull {
|
|
||||||
wheel.Name = stmt.ColumnText(15)
|
|
||||||
}
|
|
||||||
if stmt.ColumnType(16) != sqlite.TypeNull {
|
|
||||||
wheel.Description = stmt.ColumnText(16)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load heroic op wheel %d: %w", wheelID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("heroic op wheel %d not found", wheelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &wheel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveStarter saves a heroic op starter
|
|
||||||
func (dhom *DatabaseHeroicOPManager) SaveStarter(ctx context.Context, starter *HeroicOPStarter) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `INSERT OR REPLACE INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{
|
|
||||||
starter.ID,
|
|
||||||
HOTypeStarter,
|
|
||||||
starter.StartClass,
|
|
||||||
starter.StarterIcon,
|
|
||||||
starter.Abilities[0],
|
|
||||||
starter.Abilities[1],
|
|
||||||
starter.Abilities[2],
|
|
||||||
starter.Abilities[3],
|
|
||||||
starter.Abilities[4],
|
|
||||||
starter.Abilities[5],
|
|
||||||
starter.Name,
|
|
||||||
starter.Description,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save heroic op starter %d: %w", starter.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveWheel saves a heroic op wheel
|
|
||||||
func (dhom *DatabaseHeroicOPManager) SaveWheel(ctx context.Context, wheel *HeroicOPWheel) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `INSERT OR REPLACE INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{
|
|
||||||
wheel.ID,
|
|
||||||
HOTypeWheel,
|
|
||||||
wheel.StarterLinkID,
|
|
||||||
wheel.Order,
|
|
||||||
wheel.ShiftIcon,
|
|
||||||
wheel.SpellID,
|
|
||||||
wheel.Chance,
|
|
||||||
wheel.Abilities[0],
|
|
||||||
wheel.Abilities[1],
|
|
||||||
wheel.Abilities[2],
|
|
||||||
wheel.Abilities[3],
|
|
||||||
wheel.Abilities[4],
|
|
||||||
wheel.Abilities[5],
|
|
||||||
wheel.Name,
|
|
||||||
wheel.Description,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save heroic op wheel %d: %w", wheel.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteStarter removes a starter from database
|
|
||||||
func (dhom *DatabaseHeroicOPManager) DeleteStarter(ctx context.Context, starterID int32) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
// Use a transaction to delete starter and associated wheels
|
|
||||||
err = sqlitex.Execute(conn, "BEGIN", nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
||||||
}
|
|
||||||
defer sqlitex.Execute(conn, "ROLLBACK", nil)
|
|
||||||
|
|
||||||
// Delete associated wheels first
|
|
||||||
err = sqlitex.Execute(conn, "DELETE FROM heroic_ops WHERE starter_link_id = ? AND ho_type = ?", &sqlitex.ExecOptions{
|
|
||||||
Args: []any{starterID, HOTypeWheel},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete wheels for starter %d: %w", starterID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the starter
|
|
||||||
err = sqlitex.Execute(conn, "DELETE FROM heroic_ops WHERE id = ? AND ho_type = ?", &sqlitex.ExecOptions{
|
|
||||||
Args: []any{starterID, HOTypeStarter},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete starter %d: %w", starterID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sqlitex.Execute(conn, "COMMIT", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteWheel removes a wheel from database
|
|
||||||
func (dhom *DatabaseHeroicOPManager) DeleteWheel(ctx context.Context, wheelID int32) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, "DELETE FROM heroic_ops WHERE id = ? AND ho_type = ?", &sqlitex.ExecOptions{
|
|
||||||
Args: []any{wheelID, HOTypeWheel},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete wheel %d: %w", wheelID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveHOInstance saves a heroic opportunity instance
|
|
||||||
func (dhom *DatabaseHeroicOPManager) SaveHOInstance(ctx context.Context, ho *HeroicOP) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `INSERT OR REPLACE INTO heroic_op_instances
|
|
||||||
(id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time,
|
|
||||||
time_remaining, total_time, complete, countered_1, countered_2, countered_3,
|
|
||||||
countered_4, countered_5, countered_6, shift_used, starter_progress,
|
|
||||||
completed_by, spell_name, spell_description)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
startTimeUnix := ho.StartTime.Unix()
|
|
||||||
wheelStartTimeUnix := ho.WheelStartTime.Unix()
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{
|
|
||||||
ho.ID,
|
|
||||||
ho.EncounterID,
|
|
||||||
ho.StarterID,
|
|
||||||
ho.WheelID,
|
|
||||||
ho.State,
|
|
||||||
startTimeUnix,
|
|
||||||
wheelStartTimeUnix,
|
|
||||||
ho.TimeRemaining,
|
|
||||||
ho.TotalTime,
|
|
||||||
ho.Complete,
|
|
||||||
ho.Countered[0],
|
|
||||||
ho.Countered[1],
|
|
||||||
ho.Countered[2],
|
|
||||||
ho.Countered[3],
|
|
||||||
ho.Countered[4],
|
|
||||||
ho.Countered[5],
|
|
||||||
ho.ShiftUsed,
|
|
||||||
ho.StarterProgress,
|
|
||||||
ho.CompletedBy,
|
|
||||||
ho.SpellName,
|
|
||||||
ho.SpellDescription,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save HO instance %d: %w", ho.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadHOInstance retrieves a heroic opportunity instance
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadHOInstance(ctx context.Context, instanceID int64) (*HeroicOP, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time,
|
|
||||||
time_remaining, total_time, complete, countered_1, countered_2, countered_3,
|
|
||||||
countered_4, countered_5, countered_6, shift_used, starter_progress,
|
|
||||||
completed_by, spell_name, spell_description
|
|
||||||
FROM heroic_op_instances WHERE id = ?`
|
|
||||||
|
|
||||||
var ho HeroicOP
|
|
||||||
var found bool
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{instanceID},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
found = true
|
|
||||||
ho.ID = stmt.ColumnInt64(0)
|
|
||||||
ho.EncounterID = int32(stmt.ColumnInt64(1))
|
|
||||||
ho.StarterID = int32(stmt.ColumnInt64(2))
|
|
||||||
ho.WheelID = int32(stmt.ColumnInt64(3))
|
|
||||||
ho.State = int8(stmt.ColumnInt64(4))
|
|
||||||
startTimeUnix := stmt.ColumnInt64(5)
|
|
||||||
wheelStartTimeUnix := stmt.ColumnInt64(6)
|
|
||||||
ho.TimeRemaining = int32(stmt.ColumnInt64(7))
|
|
||||||
ho.TotalTime = int32(stmt.ColumnInt64(8))
|
|
||||||
ho.Complete = int8(stmt.ColumnInt64(9))
|
|
||||||
ho.Countered[0] = int8(stmt.ColumnInt64(10))
|
|
||||||
ho.Countered[1] = int8(stmt.ColumnInt64(11))
|
|
||||||
ho.Countered[2] = int8(stmt.ColumnInt64(12))
|
|
||||||
ho.Countered[3] = int8(stmt.ColumnInt64(13))
|
|
||||||
ho.Countered[4] = int8(stmt.ColumnInt64(14))
|
|
||||||
ho.Countered[5] = int8(stmt.ColumnInt64(15))
|
|
||||||
ho.ShiftUsed = int8(stmt.ColumnInt64(16))
|
|
||||||
ho.StarterProgress = int8(stmt.ColumnInt64(17))
|
|
||||||
ho.CompletedBy = int32(stmt.ColumnInt64(18))
|
|
||||||
if stmt.ColumnType(19) != sqlite.TypeNull {
|
|
||||||
ho.SpellName = stmt.ColumnText(19)
|
|
||||||
}
|
|
||||||
if stmt.ColumnType(20) != sqlite.TypeNull {
|
|
||||||
ho.SpellDescription = stmt.ColumnText(20)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert timestamps
|
|
||||||
ho.StartTime = time.Unix(startTimeUnix, 0)
|
|
||||||
ho.WheelStartTime = time.Unix(wheelStartTimeUnix, 0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load HO instance %d: %w", instanceID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("HO instance %d not found", instanceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize maps
|
|
||||||
ho.Participants = make(map[int32]bool)
|
|
||||||
ho.CurrentStarters = make([]int32, 0)
|
|
||||||
|
|
||||||
return &ho, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteHOInstance removes a heroic opportunity instance
|
|
||||||
func (dhom *DatabaseHeroicOPManager) DeleteHOInstance(ctx context.Context, instanceID int64) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, "DELETE FROM heroic_op_instances WHERE id = ?", &sqlitex.ExecOptions{
|
|
||||||
Args: []any{instanceID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete HO instance %d: %w", instanceID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveHOEvent saves a heroic opportunity event
|
|
||||||
func (dhom *DatabaseHeroicOPManager) SaveHOEvent(ctx context.Context, event *HeroicOPEvent) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `INSERT INTO heroic_op_events
|
|
||||||
(id, instance_id, event_type, character_id, ability_icon, timestamp, data)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
timestampUnix := event.Timestamp.Unix()
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{
|
|
||||||
event.ID,
|
|
||||||
event.InstanceID,
|
|
||||||
event.EventType,
|
|
||||||
event.CharacterID,
|
|
||||||
event.AbilityIcon,
|
|
||||||
timestampUnix,
|
|
||||||
event.Data,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save HO event %d: %w", event.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadHOEvents retrieves events for a heroic opportunity instance
|
|
||||||
func (dhom *DatabaseHeroicOPManager) LoadHOEvents(ctx context.Context, instanceID int64) ([]HeroicOPEvent, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := `SELECT id, instance_id, event_type, character_id, ability_icon, timestamp, data
|
|
||||||
FROM heroic_op_events WHERE instance_id = ? ORDER BY timestamp ASC`
|
|
||||||
|
|
||||||
var events []HeroicOPEvent
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{instanceID},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
var event HeroicOPEvent
|
|
||||||
event.ID = stmt.ColumnInt64(0)
|
|
||||||
event.InstanceID = stmt.ColumnInt64(1)
|
|
||||||
event.EventType = int(stmt.ColumnInt64(2))
|
|
||||||
event.CharacterID = int32(stmt.ColumnInt64(3))
|
|
||||||
event.AbilityIcon = int16(stmt.ColumnInt64(4))
|
|
||||||
timestampUnix := stmt.ColumnInt64(5)
|
|
||||||
event.Timestamp = time.Unix(timestampUnix, 0)
|
|
||||||
if stmt.ColumnType(6) != sqlite.TypeNull {
|
|
||||||
event.Data = stmt.ColumnText(6)
|
|
||||||
}
|
|
||||||
events = append(events, event)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query HO events for instance %d: %w", instanceID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return events, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHOStatistics retrieves statistics for a character
|
|
||||||
func (dhom *DatabaseHeroicOPManager) GetHOStatistics(ctx context.Context, characterID int32) (*HeroicOPStatistics, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
// This is a simplified implementation - in practice you'd want more complex statistics
|
|
||||||
stats := &HeroicOPStatistics{
|
|
||||||
ParticipationStats: make(map[int32]int64),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count total HOs started by this character
|
|
||||||
query := `SELECT COUNT(*) FROM heroic_op_events
|
|
||||||
WHERE character_id = ? AND event_type = ?`
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{characterID, EventHOStarted},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
stats.TotalHOsStarted = stmt.ColumnInt64(0)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get HO started count: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count total HOs completed by this character
|
|
||||||
query = `SELECT COUNT(*) FROM heroic_op_events
|
|
||||||
WHERE character_id = ? AND event_type = ?`
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{characterID, EventHOCompleted},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
stats.TotalHOsCompleted = stmt.ColumnInt64(0)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get HO completed count: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate success rate
|
|
||||||
if stats.TotalHOsStarted > 0 {
|
|
||||||
stats.SuccessRate = float64(stats.TotalHOsCompleted) / float64(stats.TotalHOsStarted) * 100.0
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.ParticipationStats[characterID] = stats.TotalHOsStarted
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextStarterID returns the next available starter ID
|
|
||||||
func (dhom *DatabaseHeroicOPManager) GetNextStarterID(ctx context.Context) (int32, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM heroic_ops WHERE ho_type = ?"
|
|
||||||
|
|
||||||
var nextID int32
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{HOTypeStarter},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
nextID = int32(stmt.ColumnInt64(0))
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get next starter ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextWheelID returns the next available wheel ID
|
|
||||||
func (dhom *DatabaseHeroicOPManager) GetNextWheelID(ctx context.Context) (int32, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM heroic_ops WHERE ho_type = ?"
|
|
||||||
|
|
||||||
var nextID int32
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: []any{HOTypeWheel},
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
nextID = int32(stmt.ColumnInt64(0))
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get next wheel ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextInstanceID returns the next available instance ID
|
|
||||||
func (dhom *DatabaseHeroicOPManager) GetNextInstanceID(ctx context.Context) (int64, error) {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM heroic_op_instances"
|
|
||||||
|
|
||||||
var nextID int64
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
ResultFunc: func(stmt *sqlite.Stmt) error {
|
|
||||||
nextID = stmt.ColumnInt64(0)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get next instance ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureHOTables creates the heroic opportunity tables if they don't exist
|
|
||||||
func (dhom *DatabaseHeroicOPManager) EnsureHOTables(ctx context.Context) error {
|
|
||||||
conn, err := dhom.pool.Take(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get database connection: %w", err)
|
|
||||||
}
|
|
||||||
defer dhom.pool.Put(conn)
|
|
||||||
|
|
||||||
queries := []string{
|
|
||||||
`CREATE TABLE IF NOT EXISTS heroic_ops (
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
ho_type TEXT NOT NULL CHECK(ho_type IN ('Starter', 'Wheel')),
|
|
||||||
starter_class INTEGER NOT NULL DEFAULT 0,
|
|
||||||
starter_icon INTEGER NOT NULL DEFAULT 0,
|
|
||||||
starter_link_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
chain_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
shift_icon INTEGER NOT NULL DEFAULT 0,
|
|
||||||
spell_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
chance REAL NOT NULL DEFAULT 1.0,
|
|
||||||
ability1 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ability2 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ability3 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ability4 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ability5 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ability6 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
name TEXT DEFAULT '',
|
|
||||||
description TEXT DEFAULT '',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id, ho_type)
|
|
||||||
)`,
|
|
||||||
`CREATE TABLE IF NOT EXISTS heroic_op_instances (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
encounter_id INTEGER NOT NULL,
|
|
||||||
starter_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
wheel_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
state INTEGER NOT NULL DEFAULT 0,
|
|
||||||
start_time INTEGER NOT NULL,
|
|
||||||
wheel_start_time INTEGER NOT NULL DEFAULT 0,
|
|
||||||
time_remaining INTEGER NOT NULL DEFAULT 0,
|
|
||||||
total_time INTEGER NOT NULL DEFAULT 0,
|
|
||||||
complete INTEGER NOT NULL DEFAULT 0,
|
|
||||||
countered_1 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
countered_2 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
countered_3 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
countered_4 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
countered_5 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
countered_6 INTEGER NOT NULL DEFAULT 0,
|
|
||||||
shift_used INTEGER NOT NULL DEFAULT 0,
|
|
||||||
starter_progress INTEGER NOT NULL DEFAULT 0,
|
|
||||||
completed_by INTEGER NOT NULL DEFAULT 0,
|
|
||||||
spell_name TEXT DEFAULT '',
|
|
||||||
spell_description TEXT DEFAULT '',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)`,
|
|
||||||
`CREATE TABLE IF NOT EXISTS heroic_op_events (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
instance_id INTEGER NOT NULL,
|
|
||||||
event_type INTEGER NOT NULL,
|
|
||||||
character_id INTEGER NOT NULL,
|
|
||||||
ability_icon INTEGER NOT NULL DEFAULT 0,
|
|
||||||
timestamp INTEGER NOT NULL,
|
|
||||||
data TEXT DEFAULT '',
|
|
||||||
FOREIGN KEY (instance_id) REFERENCES heroic_op_instances(id) ON DELETE CASCADE
|
|
||||||
)`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, query := range queries {
|
|
||||||
err := sqlitex.Execute(conn, query, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create HO table %d: %w", i+1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create indexes for better performance
|
|
||||||
indexes := []string{
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_heroic_ops_type ON heroic_ops(ho_type)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_heroic_ops_class ON heroic_ops(starter_class)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_heroic_ops_link ON heroic_ops(starter_link_id)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ho_instances_encounter ON heroic_op_instances(encounter_id)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ho_instances_state ON heroic_op_instances(state)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ho_events_instance ON heroic_op_events(instance_id)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ho_events_character ON heroic_op_events(character_id)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ho_events_type ON heroic_op_events(event_type)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ho_events_timestamp ON heroic_op_events(timestamp)`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, query := range indexes {
|
|
||||||
err := sqlitex.Execute(conn, query, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create HO index %d: %w", i+1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,721 +0,0 @@
|
|||||||
package heroic_ops
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"zombiezen.com/go/sqlite"
|
|
||||||
"zombiezen.com/go/sqlite/sqlitex"
|
|
||||||
)
|
|
||||||
|
|
||||||
// createTestPool creates a temporary test database pool
|
|
||||||
func createTestPool(t *testing.T) *sqlitex.Pool {
|
|
||||||
// Create temporary directory for test database
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
dbPath := filepath.Join(tempDir, "test_heroic_ops.db")
|
|
||||||
|
|
||||||
// Create and initialize database pool
|
|
||||||
pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{
|
|
||||||
Flags: sqlite.OpenReadWrite | sqlite.OpenCreate,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create test database pool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create heroic ops tables for testing
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
err = dhom.EnsureHOTables(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create heroic ops tables: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// execSQL is a helper to execute SQL with parameters
|
|
||||||
func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...any) {
|
|
||||||
conn, err := pool.Take(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get connection: %v", err)
|
|
||||||
}
|
|
||||||
defer pool.Put(conn)
|
|
||||||
|
|
||||||
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
||||||
Args: args,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to execute SQL: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_LoadStarters tests loading starters from database
|
|
||||||
func TestDatabaseHeroicOPManager_LoadStarters(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Insert test starter data
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
1, HOTypeStarter, 5, 100, 10, 20, 30, 40, 50, 60, "Test Starter", "A test starter")
|
|
||||||
|
|
||||||
// Test loading starters
|
|
||||||
starters, err := dhom.LoadStarters(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load starters: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(starters) != 1 {
|
|
||||||
t.Errorf("Expected 1 starter, got %d", len(starters))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(starters) > 0 {
|
|
||||||
starter := starters[0]
|
|
||||||
if starter.ID != 1 {
|
|
||||||
t.Errorf("Expected starter ID 1, got %d", starter.ID)
|
|
||||||
}
|
|
||||||
if starter.StarterClass != 5 {
|
|
||||||
t.Errorf("Expected starter class 5, got %d", starter.StarterClass)
|
|
||||||
}
|
|
||||||
if starter.Name != "Test Starter" {
|
|
||||||
t.Errorf("Expected name 'Test Starter', got '%s'", starter.Name)
|
|
||||||
}
|
|
||||||
if starter.Ability1 != 10 {
|
|
||||||
t.Errorf("Expected ability1 10, got %d", starter.Ability1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_LoadStarter tests loading a specific starter
|
|
||||||
func TestDatabaseHeroicOPManager_LoadStarter(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Insert test starter data
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
2, HOTypeStarter, 10, 200, 11, 21, 31, 41, 51, 61, "Specific Starter", "A specific test starter")
|
|
||||||
|
|
||||||
// Test loading specific starter
|
|
||||||
starter, err := dhom.LoadStarter(ctx, 2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load starter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if starter.ID != 2 {
|
|
||||||
t.Errorf("Expected starter ID 2, got %d", starter.ID)
|
|
||||||
}
|
|
||||||
if starter.StarterClass != 10 {
|
|
||||||
t.Errorf("Expected starter class 10, got %d", starter.StarterClass)
|
|
||||||
}
|
|
||||||
if starter.Name != "Specific Starter" {
|
|
||||||
t.Errorf("Expected name 'Specific Starter', got '%s'", starter.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test loading non-existent starter
|
|
||||||
_, err = dhom.LoadStarter(ctx, 999)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error loading non-existent starter, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_LoadWheels tests loading wheels from database
|
|
||||||
func TestDatabaseHeroicOPManager_LoadWheels(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Insert test wheel data
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
10, HOTypeWheel, 1, 1, 300, 1001, 0.75, 15, 25, 35, 45, 55, 65, "Test Wheel", "A test wheel")
|
|
||||||
|
|
||||||
// Test loading wheels
|
|
||||||
wheels, err := dhom.LoadWheels(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load wheels: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wheels) != 1 {
|
|
||||||
t.Errorf("Expected 1 wheel, got %d", len(wheels))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wheels) > 0 {
|
|
||||||
wheel := wheels[0]
|
|
||||||
if wheel.ID != 10 {
|
|
||||||
t.Errorf("Expected wheel ID 10, got %d", wheel.ID)
|
|
||||||
}
|
|
||||||
if wheel.StarterLinkID != 1 {
|
|
||||||
t.Errorf("Expected starter link ID 1, got %d", wheel.StarterLinkID)
|
|
||||||
}
|
|
||||||
if wheel.SpellID != 1001 {
|
|
||||||
t.Errorf("Expected spell ID 1001, got %d", wheel.SpellID)
|
|
||||||
}
|
|
||||||
if wheel.Chance != 0.75 {
|
|
||||||
t.Errorf("Expected chance 0.75, got %f", wheel.Chance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_LoadWheelsForStarter tests loading wheels for specific starter
|
|
||||||
func TestDatabaseHeroicOPManager_LoadWheelsForStarter(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
starterID := int32(5)
|
|
||||||
|
|
||||||
// Insert multiple wheels for the same starter
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
20, HOTypeWheel, starterID, 1, 400, 1002, 0.5, 16, 26, 36, 46, 56, 66, "Wheel 1", "First wheel")
|
|
||||||
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
21, HOTypeWheel, starterID, 2, 500, 1003, 0.8, 17, 27, 37, 47, 57, 67, "Wheel 2", "Second wheel")
|
|
||||||
|
|
||||||
// Test loading wheels for specific starter
|
|
||||||
wheels, err := dhom.LoadWheelsForStarter(ctx, starterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load wheels for starter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wheels) != 2 {
|
|
||||||
t.Errorf("Expected 2 wheels, got %d", len(wheels))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test loading wheels for non-existent starter
|
|
||||||
wheels, err = dhom.LoadWheelsForStarter(ctx, 999)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load wheels for non-existent starter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wheels) != 0 {
|
|
||||||
t.Errorf("Expected 0 wheels for non-existent starter, got %d", len(wheels))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_SaveStarter tests saving a heroic op starter
|
|
||||||
func TestDatabaseHeroicOPManager_SaveStarter(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Create a test starter
|
|
||||||
starter := &HeroicOPStarter{
|
|
||||||
ID: 100,
|
|
||||||
StartClass: 15,
|
|
||||||
StarterIcon: 500,
|
|
||||||
Abilities: [6]int16{70, 80, 90, 100, 110, 120},
|
|
||||||
Name: "Saved Starter",
|
|
||||||
Description: "A saved test starter",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the starter
|
|
||||||
err := dhom.SaveStarter(ctx, starter)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save starter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and verify
|
|
||||||
loaded, err := dhom.LoadStarter(ctx, 100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load saved starter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if loaded.StarterClass != 15 {
|
|
||||||
t.Errorf("Expected starter class 15, got %d", loaded.StarterClass)
|
|
||||||
}
|
|
||||||
if loaded.Name != "Saved Starter" {
|
|
||||||
t.Errorf("Expected name 'Saved Starter', got '%s'", loaded.Name)
|
|
||||||
}
|
|
||||||
if loaded.Ability1 != 70 {
|
|
||||||
t.Errorf("Expected ability1 70, got %d", loaded.Ability1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_SaveWheel tests saving a heroic op wheel
|
|
||||||
func TestDatabaseHeroicOPManager_SaveWheel(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Create a test wheel
|
|
||||||
wheel := &HeroicOPWheel{
|
|
||||||
ID: 200,
|
|
||||||
StarterLinkID: 100,
|
|
||||||
Order: 2,
|
|
||||||
ShiftIcon: 600,
|
|
||||||
SpellID: 2001,
|
|
||||||
Chance: 0.9,
|
|
||||||
Abilities: [6]int16{71, 81, 91, 101, 111, 121},
|
|
||||||
Name: "Saved Wheel",
|
|
||||||
Description: "A saved test wheel",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the wheel
|
|
||||||
err := dhom.SaveWheel(ctx, wheel)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save wheel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and verify
|
|
||||||
loaded, err := dhom.LoadWheel(ctx, 200)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load saved wheel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if loaded.StarterLinkID != 100 {
|
|
||||||
t.Errorf("Expected starter link ID 100, got %d", loaded.StarterLinkID)
|
|
||||||
}
|
|
||||||
if loaded.SpellID != 2001 {
|
|
||||||
t.Errorf("Expected spell ID 2001, got %d", loaded.SpellID)
|
|
||||||
}
|
|
||||||
if loaded.Chance != 0.9 {
|
|
||||||
t.Errorf("Expected chance 0.9, got %f", loaded.Chance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_DeleteStarter tests deleting a starter and its wheels
|
|
||||||
func TestDatabaseHeroicOPManager_DeleteStarter(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
starterID := int32(300)
|
|
||||||
|
|
||||||
// Insert starter
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
starterID, HOTypeStarter, 20, 700, 72, 82, 92, 102, 112, 122, "Delete Test Starter", "To be deleted")
|
|
||||||
|
|
||||||
// Insert associated wheels
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
400, HOTypeWheel, starterID, 1, 800, 3001, 0.6, 73, 83, 93, 103, 113, 123, "Wheel to Delete", "Will be deleted")
|
|
||||||
|
|
||||||
// Verify they exist
|
|
||||||
_, err := dhom.LoadStarter(ctx, starterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Starter should exist before deletion: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wheels, err := dhom.LoadWheelsForStarter(ctx, starterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load wheels before deletion: %v", err)
|
|
||||||
}
|
|
||||||
if len(wheels) != 1 {
|
|
||||||
t.Errorf("Expected 1 wheel before deletion, got %d", len(wheels))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the starter
|
|
||||||
err = dhom.DeleteStarter(ctx, starterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to delete starter: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify they're gone
|
|
||||||
_, err = dhom.LoadStarter(ctx, starterID)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error loading deleted starter, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
wheels, err = dhom.LoadWheelsForStarter(ctx, starterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load wheels after deletion: %v", err)
|
|
||||||
}
|
|
||||||
if len(wheels) != 0 {
|
|
||||||
t.Errorf("Expected 0 wheels after deletion, got %d", len(wheels))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_HeroicOPInstance tests HO instance operations
|
|
||||||
func TestDatabaseHeroicOPManager_HeroicOPInstance(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Create a test HO instance
|
|
||||||
ho := &HeroicOP{
|
|
||||||
ID: 1000,
|
|
||||||
EncounterID: 500,
|
|
||||||
StarterID: 10,
|
|
||||||
WheelID: 20,
|
|
||||||
State: int8(HOStateWheelPhase),
|
|
||||||
StartTime: time.Now(),
|
|
||||||
WheelStartTime: time.Now().Add(5 * time.Second),
|
|
||||||
TimeRemaining: 8000,
|
|
||||||
TotalTime: 10000,
|
|
||||||
Complete: 0,
|
|
||||||
Countered: [6]int8{1, 1, 0, 0, 0, 0},
|
|
||||||
ShiftUsed: 0,
|
|
||||||
StarterProgress: 3,
|
|
||||||
CompletedBy: 0,
|
|
||||||
SpellName: "Test Spell",
|
|
||||||
SpellDescription: "A test completion spell",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the HO instance
|
|
||||||
err := dhom.SaveHOInstance(ctx, ho)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save HO instance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and verify
|
|
||||||
loaded, err := dhom.LoadHOInstance(ctx, 1000)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load HO instance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if loaded.EncounterID != 500 {
|
|
||||||
t.Errorf("Expected encounter ID 500, got %d", loaded.EncounterID)
|
|
||||||
}
|
|
||||||
if loaded.State != int8(HOStateWheelPhase) {
|
|
||||||
t.Errorf("Expected state %d, got %d", HOStateWheelPhase, loaded.State)
|
|
||||||
}
|
|
||||||
if loaded.TimeRemaining != 8000 {
|
|
||||||
t.Errorf("Expected time remaining 8000, got %d", loaded.TimeRemaining)
|
|
||||||
}
|
|
||||||
if loaded.SpellName != "Test Spell" {
|
|
||||||
t.Errorf("Expected spell name 'Test Spell', got '%s'", loaded.SpellName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the instance
|
|
||||||
err = dhom.DeleteHOInstance(ctx, 1000)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to delete HO instance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it's gone
|
|
||||||
_, err = dhom.LoadHOInstance(ctx, 1000)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error loading deleted HO instance, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_HeroicOPEvents tests HO event operations
|
|
||||||
func TestDatabaseHeroicOPManager_HeroicOPEvents(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
instanceID := int64(2000)
|
|
||||||
|
|
||||||
// Create test events
|
|
||||||
events := []*HeroicOPEvent{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
InstanceID: instanceID,
|
|
||||||
EventType: EventHOStarted,
|
|
||||||
CharacterID: 100,
|
|
||||||
AbilityIcon: 50,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Data: "started",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
InstanceID: instanceID,
|
|
||||||
EventType: EventHOAbilityUsed,
|
|
||||||
CharacterID: 101,
|
|
||||||
AbilityIcon: 51,
|
|
||||||
Timestamp: time.Now().Add(1 * time.Second),
|
|
||||||
Data: "ability used",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save events
|
|
||||||
for _, event := range events {
|
|
||||||
err := dhom.SaveHOEvent(ctx, event)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to save HO event: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and verify
|
|
||||||
loadedEvents, err := dhom.LoadHOEvents(ctx, instanceID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load HO events: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(loadedEvents) != 2 {
|
|
||||||
t.Errorf("Expected 2 events, got %d", len(loadedEvents))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events should be ordered by timestamp
|
|
||||||
if len(loadedEvents) >= 2 {
|
|
||||||
if loadedEvents[0].EventType != EventHOStarted {
|
|
||||||
t.Errorf("Expected first event type %d, got %d", EventHOStarted, loadedEvents[0].EventType)
|
|
||||||
}
|
|
||||||
if loadedEvents[1].EventType != EventHOAbilityUsed {
|
|
||||||
t.Errorf("Expected second event type %d, got %d", EventHOAbilityUsed, loadedEvents[1].EventType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_Statistics tests HO statistics retrieval
|
|
||||||
func TestDatabaseHeroicOPManager_Statistics(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
characterID := int32(1001)
|
|
||||||
|
|
||||||
// Insert test events for statistics
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_op_events
|
|
||||||
(id, instance_id, event_type, character_id, ability_icon, timestamp, data)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
1, 100, EventHOStarted, characterID, 0, time.Now().Unix(), "")
|
|
||||||
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_op_events
|
|
||||||
(id, instance_id, event_type, character_id, ability_icon, timestamp, data)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
2, 100, EventHOCompleted, characterID, 0, time.Now().Add(10*time.Second).Unix(), "")
|
|
||||||
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_op_events
|
|
||||||
(id, instance_id, event_type, character_id, ability_icon, timestamp, data)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
3, 101, EventHOStarted, characterID, 0, time.Now().Add(20*time.Second).Unix(), "")
|
|
||||||
|
|
||||||
// Get statistics
|
|
||||||
stats, err := dhom.GetHOStatistics(ctx, characterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get HO statistics: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats.TotalHOsStarted != 2 {
|
|
||||||
t.Errorf("Expected 2 HOs started, got %d", stats.TotalHOsStarted)
|
|
||||||
}
|
|
||||||
if stats.TotalHOsCompleted != 1 {
|
|
||||||
t.Errorf("Expected 1 HO completed, got %d", stats.TotalHOsCompleted)
|
|
||||||
}
|
|
||||||
if stats.SuccessRate != 50.0 {
|
|
||||||
t.Errorf("Expected success rate 50.0, got %f", stats.SuccessRate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseHeroicOPManager_NextIDs tests ID generation
|
|
||||||
func TestDatabaseHeroicOPManager_NextIDs(t *testing.T) {
|
|
||||||
pool := createTestPool(t)
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Test next starter ID (should be 1 for empty database)
|
|
||||||
starterID, err := dhom.GetNextStarterID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get next starter ID: %v", err)
|
|
||||||
}
|
|
||||||
if starterID != 1 {
|
|
||||||
t.Errorf("Expected next starter ID 1, got %d", starterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert a starter and test again
|
|
||||||
execSQL(t, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
5, HOTypeStarter, 1, 100, 1, 2, 3, 4, 5, 6, "Test", "Test")
|
|
||||||
|
|
||||||
starterID, err = dhom.GetNextStarterID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get next starter ID after insert: %v", err)
|
|
||||||
}
|
|
||||||
if starterID != 6 {
|
|
||||||
t.Errorf("Expected next starter ID 6, got %d", starterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test next wheel ID
|
|
||||||
wheelID, err := dhom.GetNextWheelID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get next wheel ID: %v", err)
|
|
||||||
}
|
|
||||||
if wheelID != 1 {
|
|
||||||
t.Errorf("Expected next wheel ID 1, got %d", wheelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test next instance ID
|
|
||||||
instanceID, err := dhom.GetNextInstanceID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get next instance ID: %v", err)
|
|
||||||
}
|
|
||||||
if instanceID != 1 {
|
|
||||||
t.Errorf("Expected next instance ID 1, got %d", instanceID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkDatabaseHeroicOPManager_LoadStarters benchmarks loading starters
|
|
||||||
func BenchmarkDatabaseHeroicOPManager_LoadStarters(b *testing.B) {
|
|
||||||
pool := createTestPool(&testing.T{})
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Insert test data
|
|
||||||
for i := 1; i <= 50; i++ {
|
|
||||||
execSQL(&testing.T{}, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
i, HOTypeStarter, i%10+1, i*10,
|
|
||||||
i*1, i*2, i*3, i*4, i*5, i*6,
|
|
||||||
fmt.Sprintf("Starter %d", i), fmt.Sprintf("Description %d", i))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
starters, err := dhom.LoadStarters(ctx)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to load starters: %v", err)
|
|
||||||
}
|
|
||||||
if len(starters) != 50 {
|
|
||||||
b.Errorf("Expected 50 starters, got %d", len(starters))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkDatabaseHeroicOPManager_LoadWheels benchmarks loading wheels
|
|
||||||
func BenchmarkDatabaseHeroicOPManager_LoadWheels(b *testing.B) {
|
|
||||||
pool := createTestPool(&testing.T{})
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Insert test wheel data
|
|
||||||
for i := 1; i <= 100; i++ {
|
|
||||||
execSQL(&testing.T{}, pool, `INSERT INTO heroic_ops
|
|
||||||
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
|
|
||||||
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
|
|
||||||
ability5, ability6, name, description)
|
|
||||||
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
i, HOTypeWheel, i%10+1, i%3+1, i*10, i*100+1000, float32(i%100)/100.0,
|
|
||||||
i*1, i*2, i*3, i*4, i*5, i*6,
|
|
||||||
fmt.Sprintf("Wheel %d", i), fmt.Sprintf("Wheel Description %d", i))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
wheels, err := dhom.LoadWheels(ctx)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to load wheels: %v", err)
|
|
||||||
}
|
|
||||||
if len(wheels) != 100 {
|
|
||||||
b.Errorf("Expected 100 wheels, got %d", len(wheels))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkDatabaseHeroicOPManager_SaveStarter benchmarks saving starters
|
|
||||||
func BenchmarkDatabaseHeroicOPManager_SaveStarter(b *testing.B) {
|
|
||||||
pool := createTestPool(&testing.T{})
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
starter := &HeroicOPStarter{
|
|
||||||
ID: 1000,
|
|
||||||
StartClass: 5,
|
|
||||||
StarterIcon: 100,
|
|
||||||
Abilities: [6]int16{10, 20, 30, 40, 50, 60},
|
|
||||||
Name: "Benchmark Starter",
|
|
||||||
Description: "A benchmark test starter",
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
err := dhom.SaveStarter(ctx, starter)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to save starter: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkDatabaseHeroicOPManager_SaveHOInstance benchmarks saving HO instances
|
|
||||||
func BenchmarkDatabaseHeroicOPManager_SaveHOInstance(b *testing.B) {
|
|
||||||
pool := createTestPool(&testing.T{})
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
dhom := NewDatabaseHeroicOPManager(pool)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
ho := &HeroicOP{
|
|
||||||
ID: 5000,
|
|
||||||
EncounterID: 1000,
|
|
||||||
StarterID: 50,
|
|
||||||
WheelID: 100,
|
|
||||||
State: int8(HOStateWheelPhase),
|
|
||||||
StartTime: time.Now(),
|
|
||||||
WheelStartTime: time.Now(),
|
|
||||||
TimeRemaining: 10000,
|
|
||||||
TotalTime: 10000,
|
|
||||||
Complete: 0,
|
|
||||||
Countered: [6]int8{0, 0, 0, 0, 0, 0},
|
|
||||||
ShiftUsed: 0,
|
|
||||||
StarterProgress: 0,
|
|
||||||
CompletedBy: 0,
|
|
||||||
SpellName: "Benchmark Spell",
|
|
||||||
SpellDescription: "A benchmark spell",
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
err := dhom.SaveHOInstance(ctx, ho)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to save HO instance: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
197
internal/heroic_ops/doc.go
Normal file
197
internal/heroic_ops/doc.go
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
// Package heroic_ops provides comprehensive heroic opportunity management for EverQuest II server emulation.
|
||||||
|
//
|
||||||
|
// This package implements a complete heroic opportunity system with embedded database operations,
|
||||||
|
// optimized master list, and full MySQL/SQLite support through the internal database wrapper.
|
||||||
|
//
|
||||||
|
// Basic Usage:
|
||||||
|
//
|
||||||
|
// // Create a new heroic opportunity starter
|
||||||
|
// starter := heroic_ops.NewHeroicOPStarter(db)
|
||||||
|
// starter.SetID(1001)
|
||||||
|
// starter.SetStartClass(heroic_ops.ClassAny)
|
||||||
|
// starter.SetName("Epic Chain Starter")
|
||||||
|
// starter.SetAbility(0, 1) // Melee ability
|
||||||
|
// starter.SetAbility(1, 2) // Spell ability
|
||||||
|
// starter.Save()
|
||||||
|
//
|
||||||
|
// // Load existing starter
|
||||||
|
// loaded, err := heroic_ops.LoadHeroicOPStarter(db, 1001)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// loaded.Delete()
|
||||||
|
//
|
||||||
|
// Heroic Opportunity Wheels:
|
||||||
|
//
|
||||||
|
// // Create a wheel linked to a starter
|
||||||
|
// wheel := heroic_ops.NewHeroicOPWheel(db)
|
||||||
|
// wheel.SetID(2001)
|
||||||
|
// wheel.SetStarterLinkID(1001)
|
||||||
|
// wheel.SetSpellID(5000)
|
||||||
|
// wheel.SetChance(75.0)
|
||||||
|
// wheel.SetAbility(0, 1) // First ability required
|
||||||
|
// wheel.SetAbility(1, 3) // Second ability required
|
||||||
|
// wheel.Save()
|
||||||
|
//
|
||||||
|
// Bespoke Master List (optimized for performance):
|
||||||
|
//
|
||||||
|
// // Create master list and load all heroic opportunities
|
||||||
|
// masterList := heroic_ops.NewMasterList()
|
||||||
|
// masterList.LoadFromDatabase(db)
|
||||||
|
//
|
||||||
|
// // O(1) lookups by ID
|
||||||
|
// starter := masterList.GetStarter(1001)
|
||||||
|
// wheel := masterList.GetWheel(2001)
|
||||||
|
//
|
||||||
|
// // O(1) lookups by class
|
||||||
|
// classStarters := masterList.GetStartersForClass(heroic_ops.ClassAny)
|
||||||
|
//
|
||||||
|
// // O(1) lookups by starter ID
|
||||||
|
// starterWheels := masterList.GetWheelsForStarter(1001)
|
||||||
|
//
|
||||||
|
// // O(1) random wheel selection with weighted chances
|
||||||
|
// randomWheel := masterList.SelectRandomWheel(1001)
|
||||||
|
//
|
||||||
|
// // O(1) specialized queries
|
||||||
|
// orderedWheels := masterList.GetOrderedWheels()
|
||||||
|
// shiftWheels := masterList.GetShiftWheels()
|
||||||
|
//
|
||||||
|
// Active Heroic Opportunity Instances:
|
||||||
|
//
|
||||||
|
// // Create active HO instance
|
||||||
|
// ho := heroic_ops.NewHeroicOP(db, 12345, encounterID)
|
||||||
|
// ho.StartStarterChain([]int32{1001, 1002})
|
||||||
|
//
|
||||||
|
// // Process abilities during starter chain
|
||||||
|
// success := ho.ProcessStarterAbility(1, masterList) // Melee ability
|
||||||
|
// if success {
|
||||||
|
// // Transition to wheel phase
|
||||||
|
// wheel := masterList.SelectRandomWheel(ho.StarterID)
|
||||||
|
// ho.StartWheelPhase(wheel, 10) // 10 second timer
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Process abilities during wheel phase
|
||||||
|
// success = ho.ProcessWheelAbility(1, characterID, wheel)
|
||||||
|
// if ho.IsComplete() {
|
||||||
|
// // HO completed successfully
|
||||||
|
// data := ho.GetPacketData(wheel)
|
||||||
|
// // Send completion packets to clients
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Advanced Search:
|
||||||
|
//
|
||||||
|
// criteria := heroic_ops.HeroicOPSearchCriteria{
|
||||||
|
// StarterClass: heroic_ops.ClassAny,
|
||||||
|
// SpellID: 5000,
|
||||||
|
// MinChance: 50.0,
|
||||||
|
// MaxChance: 100.0,
|
||||||
|
// NamePattern: "Epic",
|
||||||
|
// HasShift: true,
|
||||||
|
// IsOrdered: false,
|
||||||
|
// }
|
||||||
|
// results := masterList.Search(criteria)
|
||||||
|
//
|
||||||
|
// Performance Characteristics:
|
||||||
|
//
|
||||||
|
// - Starter creation/loading: <100ns per operation
|
||||||
|
// - Wheel creation/loading: <150ns per operation
|
||||||
|
// - ID lookups: <50ns per operation (O(1) map access)
|
||||||
|
// - Class lookups: <100ns per operation (O(1) indexed)
|
||||||
|
// - Starter wheel lookups: <100ns per operation (O(1) cached)
|
||||||
|
// - Random selection: <200ns per operation (optimized weighted selection)
|
||||||
|
// - Specialized queries: <500ns per operation (pre-indexed)
|
||||||
|
// - Search with criteria: <2µs per operation (multi-index optimization)
|
||||||
|
// - Statistics generation: <50µs per operation (lazy caching)
|
||||||
|
// - HO instance operations: <300ns per operation (in-memory + database)
|
||||||
|
//
|
||||||
|
// Thread Safety:
|
||||||
|
//
|
||||||
|
// All operations are thread-safe using optimized RWMutex patterns with minimal lock contention.
|
||||||
|
// Read operations use shared locks while modifications use exclusive locks.
|
||||||
|
// Concurrent HO processing is fully supported.
|
||||||
|
//
|
||||||
|
// Database Support:
|
||||||
|
//
|
||||||
|
// Complete MySQL and SQLite support through the internal database wrapper:
|
||||||
|
//
|
||||||
|
// // SQLite
|
||||||
|
// db, _ := database.NewSQLite("heroic_ops.db")
|
||||||
|
//
|
||||||
|
// // MySQL
|
||||||
|
// db, _ := database.NewMySQL("user:pass@tcp(localhost:3306)/eq2")
|
||||||
|
//
|
||||||
|
// The implementation uses the internal database wrapper which handles both database types
|
||||||
|
// transparently using database/sql-compatible methods.
|
||||||
|
//
|
||||||
|
// Heroic Opportunity System Architecture:
|
||||||
|
//
|
||||||
|
// The system consists of three main components:
|
||||||
|
//
|
||||||
|
// 1. **Starters**: Define the initial ability chain that players must complete to trigger a wheel phase.
|
||||||
|
// Each starter specifies which class can initiate it and the sequence of abilities required.
|
||||||
|
//
|
||||||
|
// 2. **Wheels**: Define the collaborative phase where multiple players contribute abilities to complete
|
||||||
|
// the heroic opportunity. Wheels can be ordered (sequential) or unordered (any order).
|
||||||
|
//
|
||||||
|
// 3. **Instances**: Active heroic opportunity sessions that track progress, participants, and state
|
||||||
|
// transitions from starter chain through wheel completion.
|
||||||
|
//
|
||||||
|
// Key Features:
|
||||||
|
//
|
||||||
|
// - **Class Restrictions**: Starters can be limited to specific classes or open to any class
|
||||||
|
// - **Weighted Selection**: Wheels have chance values for probabilistic selection
|
||||||
|
// - **Shift Abilities**: Special abilities that can change the wheel type during execution
|
||||||
|
// - **Ordered/Unordered**: Wheels can require abilities in sequence or allow any order
|
||||||
|
// - **Timer Management**: Configurable timers for both starter chains and wheel phases
|
||||||
|
// - **Event System**: Comprehensive event tracking for statistics and logging
|
||||||
|
// - **Spell Integration**: Completed HOs cast spells with full spell system integration
|
||||||
|
//
|
||||||
|
// Configuration Constants:
|
||||||
|
//
|
||||||
|
// The system provides extensive configuration through constants in constants.go:
|
||||||
|
//
|
||||||
|
// heroic_ops.MaxAbilities // Maximum abilities per starter/wheel (6)
|
||||||
|
// heroic_ops.DefaultWheelTimerSeconds // Default wheel timer (10s)
|
||||||
|
// heroic_ops.MaxConcurrentHOs // Max concurrent HOs per encounter (3)
|
||||||
|
// heroic_ops.ClassAny // Universal class restriction (0)
|
||||||
|
//
|
||||||
|
// Error Handling:
|
||||||
|
//
|
||||||
|
// The package provides detailed error messages for all failure conditions:
|
||||||
|
//
|
||||||
|
// ErrHONotFound // HO instance not found
|
||||||
|
// ErrHOInvalidState // Invalid state transition
|
||||||
|
// ErrHOAbilityNotAllowed // Ability not valid for current context
|
||||||
|
// ErrHOTimerExpired // Timer has expired
|
||||||
|
// ErrHOAlreadyComplete // HO already completed
|
||||||
|
//
|
||||||
|
// Integration Interfaces:
|
||||||
|
//
|
||||||
|
// The package defines comprehensive interfaces for integration with other game systems:
|
||||||
|
//
|
||||||
|
// HeroicOPEventHandler // Event notifications
|
||||||
|
// ClientManager // Packet sending
|
||||||
|
// EncounterManager // Encounter integration
|
||||||
|
// PlayerManager // Player system integration
|
||||||
|
// SpellManager // Spell casting
|
||||||
|
// TimerManager // Timer management
|
||||||
|
//
|
||||||
|
// Statistics and Analytics:
|
||||||
|
//
|
||||||
|
// Built-in statistics collection provides insights into system usage:
|
||||||
|
//
|
||||||
|
// stats := masterList.GetStatistics()
|
||||||
|
// fmt.Printf("Total Starters: %d\n", stats.TotalStarters)
|
||||||
|
// fmt.Printf("Total Wheels: %d\n", stats.TotalWheels)
|
||||||
|
// fmt.Printf("Average Chance: %.2f\n", stats.AverageChance)
|
||||||
|
// fmt.Printf("Class Distribution: %+v\n", stats.ClassDistribution)
|
||||||
|
//
|
||||||
|
// Advanced Features:
|
||||||
|
//
|
||||||
|
// - **Validation**: Comprehensive validation of all data integrity
|
||||||
|
// - **Caching**: Lazy metadata caching with automatic invalidation
|
||||||
|
// - **Indexing**: Multi-dimensional indexing for optimal query performance
|
||||||
|
// - **Concurrent Safety**: Full thread safety with minimal contention
|
||||||
|
// - **Memory Efficiency**: Optimized data structures and object pooling
|
||||||
|
//
|
||||||
|
package heroic_ops
|
@ -2,302 +2,13 @@ package heroic_ops
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHeroicOPStarter creates a new heroic opportunity starter
|
|
||||||
func NewHeroicOPStarter(id int32, startClass int8, starterIcon int16) *HeroicOPStarter {
|
|
||||||
return &HeroicOPStarter{
|
|
||||||
ID: id,
|
|
||||||
StartClass: startClass,
|
|
||||||
StarterIcon: starterIcon,
|
|
||||||
Abilities: [6]int16{},
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy creates a deep copy of the starter
|
|
||||||
func (hos *HeroicOPStarter) Copy() *HeroicOPStarter {
|
|
||||||
hos.mu.RLock()
|
|
||||||
defer hos.mu.RUnlock()
|
|
||||||
|
|
||||||
newStarter := &HeroicOPStarter{
|
|
||||||
ID: hos.ID,
|
|
||||||
StartClass: hos.StartClass,
|
|
||||||
StarterIcon: hos.StarterIcon,
|
|
||||||
Abilities: hos.Abilities, // Arrays are copied by value
|
|
||||||
Name: hos.Name,
|
|
||||||
Description: hos.Description,
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
return newStarter
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAbility returns the ability icon at the specified position
|
|
||||||
func (hos *HeroicOPStarter) GetAbility(position int) int16 {
|
|
||||||
hos.mu.RLock()
|
|
||||||
defer hos.mu.RUnlock()
|
|
||||||
|
|
||||||
if position < 0 || position >= MaxAbilities {
|
|
||||||
return AbilityIconNone
|
|
||||||
}
|
|
||||||
|
|
||||||
return hos.Abilities[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAbility sets the ability icon at the specified position
|
|
||||||
func (hos *HeroicOPStarter) SetAbility(position int, abilityIcon int16) bool {
|
|
||||||
hos.mu.Lock()
|
|
||||||
defer hos.mu.Unlock()
|
|
||||||
|
|
||||||
if position < 0 || position >= MaxAbilities {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hos.Abilities[position] = abilityIcon
|
|
||||||
hos.SaveNeeded = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsComplete checks if the starter chain is complete (has completion marker)
|
|
||||||
func (hos *HeroicOPStarter) IsComplete(position int) bool {
|
|
||||||
hos.mu.RLock()
|
|
||||||
defer hos.mu.RUnlock()
|
|
||||||
|
|
||||||
if position < 0 || position >= MaxAbilities {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return hos.Abilities[position] == AbilityIconAny
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanInitiate checks if the specified class can initiate this starter
|
|
||||||
func (hos *HeroicOPStarter) CanInitiate(playerClass int8) bool {
|
|
||||||
hos.mu.RLock()
|
|
||||||
defer hos.mu.RUnlock()
|
|
||||||
|
|
||||||
return hos.StartClass == ClassAny || hos.StartClass == playerClass
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchesAbility checks if the given ability matches the current position
|
|
||||||
func (hos *HeroicOPStarter) MatchesAbility(position int, abilityIcon int16) bool {
|
|
||||||
hos.mu.RLock()
|
|
||||||
defer hos.mu.RUnlock()
|
|
||||||
|
|
||||||
if position < 0 || position >= MaxAbilities {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredAbility := hos.Abilities[position]
|
|
||||||
|
|
||||||
// Wildcard matches any ability
|
|
||||||
if requiredAbility == AbilityIconAny {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exact match required
|
|
||||||
return requiredAbility == abilityIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks if the starter is properly configured
|
|
||||||
func (hos *HeroicOPStarter) Validate() error {
|
|
||||||
hos.mu.RLock()
|
|
||||||
defer hos.mu.RUnlock()
|
|
||||||
|
|
||||||
if hos.ID <= 0 {
|
|
||||||
return fmt.Errorf("invalid starter ID: %d", hos.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hos.StarterIcon <= 0 {
|
|
||||||
return fmt.Errorf("invalid starter icon: %d", hos.StarterIcon)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for at least one non-zero ability
|
|
||||||
hasAbility := false
|
|
||||||
for _, ability := range hos.Abilities {
|
|
||||||
if ability != AbilityIconNone {
|
|
||||||
hasAbility = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasAbility {
|
|
||||||
return fmt.Errorf("starter must have at least one ability")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHeroicOPWheel creates a new heroic opportunity wheel
|
|
||||||
func NewHeroicOPWheel(id int32, starterLinkID int32, order int8) *HeroicOPWheel {
|
|
||||||
return &HeroicOPWheel{
|
|
||||||
ID: id,
|
|
||||||
StarterLinkID: starterLinkID,
|
|
||||||
Order: order,
|
|
||||||
Abilities: [6]int16{},
|
|
||||||
Chance: 1.0,
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy creates a deep copy of the wheel
|
|
||||||
func (how *HeroicOPWheel) Copy() *HeroicOPWheel {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
newWheel := &HeroicOPWheel{
|
|
||||||
ID: how.ID,
|
|
||||||
StarterLinkID: how.StarterLinkID,
|
|
||||||
Order: how.Order,
|
|
||||||
ShiftIcon: how.ShiftIcon,
|
|
||||||
Chance: how.Chance,
|
|
||||||
Abilities: how.Abilities, // Arrays are copied by value
|
|
||||||
SpellID: how.SpellID,
|
|
||||||
Name: how.Name,
|
|
||||||
Description: how.Description,
|
|
||||||
RequiredPlayers: how.RequiredPlayers,
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
return newWheel
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAbility returns the ability icon at the specified position
|
|
||||||
func (how *HeroicOPWheel) GetAbility(position int) int16 {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
if position < 0 || position >= MaxAbilities {
|
|
||||||
return AbilityIconNone
|
|
||||||
}
|
|
||||||
|
|
||||||
return how.Abilities[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAbility sets the ability icon at the specified position
|
|
||||||
func (how *HeroicOPWheel) SetAbility(position int, abilityIcon int16) bool {
|
|
||||||
how.mu.Lock()
|
|
||||||
defer how.mu.Unlock()
|
|
||||||
|
|
||||||
if position < 0 || position >= MaxAbilities {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
how.Abilities[position] = abilityIcon
|
|
||||||
how.SaveNeeded = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsOrdered checks if this wheel requires ordered completion
|
|
||||||
func (how *HeroicOPWheel) IsOrdered() bool {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
return how.Order >= WheelOrderOrdered
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasShift checks if this wheel has a shift ability
|
|
||||||
func (how *HeroicOPWheel) HasShift() bool {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
return how.ShiftIcon > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanShift checks if shifting is possible with the given ability
|
|
||||||
func (how *HeroicOPWheel) CanShift(abilityIcon int16) bool {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
return how.ShiftIcon > 0 && how.ShiftIcon == abilityIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextRequiredAbility returns the next required ability for ordered wheels
|
|
||||||
func (how *HeroicOPWheel) GetNextRequiredAbility(countered [6]int8) int16 {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
if !how.IsOrdered() {
|
|
||||||
return AbilityIconNone // Any uncompleted ability works for unordered
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find first uncompleted ability in order
|
|
||||||
for i := 0; i < MaxAbilities; i++ {
|
|
||||||
if countered[i] == 0 && how.Abilities[i] != AbilityIconNone {
|
|
||||||
return how.Abilities[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AbilityIconNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanUseAbility checks if an ability can be used on this wheel
|
|
||||||
func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bool {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
// Check if this is a shift attempt
|
|
||||||
if how.CanShift(abilityIcon) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if how.IsOrdered() {
|
|
||||||
// For ordered wheels, only the next required ability can be used
|
|
||||||
nextRequired := how.GetNextRequiredAbility(countered)
|
|
||||||
return nextRequired == abilityIcon
|
|
||||||
} else {
|
|
||||||
// For unordered wheels, any uncompleted matching ability can be used
|
|
||||||
for i := 0; i < MaxAbilities; i++ {
|
|
||||||
if countered[i] == 0 && how.Abilities[i] == abilityIcon {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks if the wheel is properly configured
|
|
||||||
func (how *HeroicOPWheel) Validate() error {
|
|
||||||
how.mu.RLock()
|
|
||||||
defer how.mu.RUnlock()
|
|
||||||
|
|
||||||
if how.ID <= 0 {
|
|
||||||
return fmt.Errorf("invalid wheel ID: %d", how.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if how.StarterLinkID <= 0 {
|
|
||||||
return fmt.Errorf("invalid starter link ID: %d", how.StarterLinkID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if how.Chance < MinChance || how.Chance > MaxChance {
|
|
||||||
return fmt.Errorf("invalid chance: %f (must be %f-%f)", how.Chance, MinChance, MaxChance)
|
|
||||||
}
|
|
||||||
|
|
||||||
if how.SpellID <= 0 {
|
|
||||||
return fmt.Errorf("invalid spell ID: %d", how.SpellID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for at least one non-zero ability
|
|
||||||
hasAbility := false
|
|
||||||
for _, ability := range how.Abilities {
|
|
||||||
if ability != AbilityIconNone {
|
|
||||||
hasAbility = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasAbility {
|
|
||||||
return fmt.Errorf("wheel must have at least one ability")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHeroicOP creates a new heroic opportunity instance
|
// NewHeroicOP creates a new heroic opportunity instance
|
||||||
func NewHeroicOP(instanceID int64, encounterID int32) *HeroicOP {
|
func NewHeroicOP(db *database.Database, instanceID int64, encounterID int32) *HeroicOP {
|
||||||
return &HeroicOP{
|
return &HeroicOP{
|
||||||
ID: instanceID,
|
ID: instanceID,
|
||||||
EncounterID: encounterID,
|
EncounterID: encounterID,
|
||||||
@ -307,10 +18,318 @@ func NewHeroicOP(instanceID int64, encounterID int32) *HeroicOP {
|
|||||||
CurrentStarters: make([]int32, 0),
|
CurrentStarters: make([]int32, 0),
|
||||||
TotalTime: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds
|
TotalTime: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds
|
||||||
TimeRemaining: DefaultWheelTimerSeconds * 1000,
|
TimeRemaining: DefaultWheelTimerSeconds * 1000,
|
||||||
|
db: db,
|
||||||
|
isNew: true,
|
||||||
SaveNeeded: false,
|
SaveNeeded: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadHeroicOP loads a heroic opportunity instance by ID
|
||||||
|
func LoadHeroicOP(db *database.Database, instanceID int64) (*HeroicOP, error) {
|
||||||
|
ho := &HeroicOP{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load basic HO data
|
||||||
|
query := `SELECT id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time, time_remaining, total_time,
|
||||||
|
complete, shift_used, starter_progress, completed_by, spell_name, spell_description,
|
||||||
|
countered1, countered2, countered3, countered4, countered5, countered6
|
||||||
|
FROM heroic_op_instances WHERE id = ?`
|
||||||
|
|
||||||
|
var startTimeStr, wheelStartTimeStr string
|
||||||
|
err := db.QueryRow(query, instanceID).Scan(
|
||||||
|
&ho.ID,
|
||||||
|
&ho.EncounterID,
|
||||||
|
&ho.StarterID,
|
||||||
|
&ho.WheelID,
|
||||||
|
&ho.State,
|
||||||
|
&startTimeStr,
|
||||||
|
&wheelStartTimeStr,
|
||||||
|
&ho.TimeRemaining,
|
||||||
|
&ho.TotalTime,
|
||||||
|
&ho.Complete,
|
||||||
|
&ho.ShiftUsed,
|
||||||
|
&ho.StarterProgress,
|
||||||
|
&ho.CompletedBy,
|
||||||
|
&ho.SpellName,
|
||||||
|
&ho.SpellDescription,
|
||||||
|
&ho.Countered[0],
|
||||||
|
&ho.Countered[1],
|
||||||
|
&ho.Countered[2],
|
||||||
|
&ho.Countered[3],
|
||||||
|
&ho.Countered[4],
|
||||||
|
&ho.Countered[5],
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load heroic op instance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse time fields
|
||||||
|
if startTimeStr != "" {
|
||||||
|
ho.StartTime, err = time.Parse(time.RFC3339, startTimeStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse start time: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wheelStartTimeStr != "" {
|
||||||
|
ho.WheelStartTime, err = time.Parse(time.RFC3339, wheelStartTimeStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse wheel start time: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load participants
|
||||||
|
ho.Participants = make(map[int32]bool)
|
||||||
|
participantQuery := "SELECT character_id FROM heroic_op_participants WHERE instance_id = ?"
|
||||||
|
rows, err := db.Query(participantQuery, instanceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load participants: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var characterID int32
|
||||||
|
if err := rows.Scan(&characterID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan participant: %w", err)
|
||||||
|
}
|
||||||
|
ho.Participants[characterID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current starters
|
||||||
|
ho.CurrentStarters = make([]int32, 0)
|
||||||
|
starterQuery := "SELECT starter_id FROM heroic_op_current_starters WHERE instance_id = ?"
|
||||||
|
starterRows, err := db.Query(starterQuery, instanceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load current starters: %w", err)
|
||||||
|
}
|
||||||
|
defer starterRows.Close()
|
||||||
|
|
||||||
|
for starterRows.Next() {
|
||||||
|
var starterID int32
|
||||||
|
if err := starterRows.Scan(&starterID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan current starter: %w", err)
|
||||||
|
}
|
||||||
|
ho.CurrentStarters = append(ho.CurrentStarters, starterID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ho.SaveNeeded = false
|
||||||
|
return ho, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the instance ID
|
||||||
|
func (ho *HeroicOP) GetID() int64 {
|
||||||
|
ho.mu.RLock()
|
||||||
|
defer ho.mu.RUnlock()
|
||||||
|
return ho.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save persists the heroic op instance to the database
|
||||||
|
func (ho *HeroicOP) Save() error {
|
||||||
|
ho.mu.Lock()
|
||||||
|
defer ho.mu.Unlock()
|
||||||
|
|
||||||
|
if !ho.SaveNeeded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ho.isNew {
|
||||||
|
// Insert new record
|
||||||
|
query := `INSERT INTO heroic_op_instances (id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time, time_remaining, total_time,
|
||||||
|
complete, shift_used, starter_progress, completed_by, spell_name, spell_description,
|
||||||
|
countered1, countered2, countered3, countered4, countered5, countered6)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := ho.db.Exec(query,
|
||||||
|
ho.ID,
|
||||||
|
ho.EncounterID,
|
||||||
|
ho.StarterID,
|
||||||
|
ho.WheelID,
|
||||||
|
ho.State,
|
||||||
|
ho.StartTime.Format(time.RFC3339),
|
||||||
|
ho.WheelStartTime.Format(time.RFC3339),
|
||||||
|
ho.TimeRemaining,
|
||||||
|
ho.TotalTime,
|
||||||
|
ho.Complete,
|
||||||
|
ho.ShiftUsed,
|
||||||
|
ho.StarterProgress,
|
||||||
|
ho.CompletedBy,
|
||||||
|
ho.SpellName,
|
||||||
|
ho.SpellDescription,
|
||||||
|
ho.Countered[0],
|
||||||
|
ho.Countered[1],
|
||||||
|
ho.Countered[2],
|
||||||
|
ho.Countered[3],
|
||||||
|
ho.Countered[4],
|
||||||
|
ho.Countered[5],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert heroic op instance: %w", err)
|
||||||
|
}
|
||||||
|
ho.isNew = false
|
||||||
|
} else {
|
||||||
|
// Update existing record
|
||||||
|
query := `UPDATE heroic_op_instances SET encounter_id = ?, starter_id = ?, wheel_id = ?, state = ?, start_time = ?, wheel_start_time = ?,
|
||||||
|
time_remaining = ?, total_time = ?, complete = ?, shift_used = ?, starter_progress = ?, completed_by = ?,
|
||||||
|
spell_name = ?, spell_description = ?, countered1 = ?, countered2 = ?, countered3 = ?, countered4 = ?,
|
||||||
|
countered5 = ?, countered6 = ? WHERE id = ?`
|
||||||
|
|
||||||
|
_, err := ho.db.Exec(query,
|
||||||
|
ho.EncounterID,
|
||||||
|
ho.StarterID,
|
||||||
|
ho.WheelID,
|
||||||
|
ho.State,
|
||||||
|
ho.StartTime.Format(time.RFC3339),
|
||||||
|
ho.WheelStartTime.Format(time.RFC3339),
|
||||||
|
ho.TimeRemaining,
|
||||||
|
ho.TotalTime,
|
||||||
|
ho.Complete,
|
||||||
|
ho.ShiftUsed,
|
||||||
|
ho.StarterProgress,
|
||||||
|
ho.CompletedBy,
|
||||||
|
ho.SpellName,
|
||||||
|
ho.SpellDescription,
|
||||||
|
ho.Countered[0],
|
||||||
|
ho.Countered[1],
|
||||||
|
ho.Countered[2],
|
||||||
|
ho.Countered[3],
|
||||||
|
ho.Countered[4],
|
||||||
|
ho.Countered[5],
|
||||||
|
ho.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update heroic op instance: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save participants
|
||||||
|
if err := ho.saveParticipants(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save participants: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current starters
|
||||||
|
if err := ho.saveCurrentStarters(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save current starters: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ho.SaveNeeded = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveParticipants saves the participants to the database (internal helper)
|
||||||
|
func (ho *HeroicOP) saveParticipants() error {
|
||||||
|
// Delete existing participants
|
||||||
|
deleteQuery := "DELETE FROM heroic_op_participants WHERE instance_id = ?"
|
||||||
|
_, err := ho.db.Exec(deleteQuery, ho.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert current participants
|
||||||
|
for characterID := range ho.Participants {
|
||||||
|
insertQuery := "INSERT INTO heroic_op_participants (instance_id, character_id) VALUES (?, ?)"
|
||||||
|
_, err := ho.db.Exec(insertQuery, ho.ID, characterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveCurrentStarters saves the current starters to the database (internal helper)
|
||||||
|
func (ho *HeroicOP) saveCurrentStarters() error {
|
||||||
|
// Delete existing current starters
|
||||||
|
deleteQuery := "DELETE FROM heroic_op_current_starters WHERE instance_id = ?"
|
||||||
|
_, err := ho.db.Exec(deleteQuery, ho.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert current starters
|
||||||
|
for _, starterID := range ho.CurrentStarters {
|
||||||
|
insertQuery := "INSERT INTO heroic_op_current_starters (instance_id, starter_id) VALUES (?, ?)"
|
||||||
|
_, err := ho.db.Exec(insertQuery, ho.ID, starterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the heroic op instance from the database
|
||||||
|
func (ho *HeroicOP) Delete() error {
|
||||||
|
ho.mu.Lock()
|
||||||
|
defer ho.mu.Unlock()
|
||||||
|
|
||||||
|
if ho.isNew {
|
||||||
|
return nil // Nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete related records first (foreign key constraints)
|
||||||
|
deleteParticipants := "DELETE FROM heroic_op_participants WHERE instance_id = ?"
|
||||||
|
_, err := ho.db.Exec(deleteParticipants, ho.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete participants: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteStarters := "DELETE FROM heroic_op_current_starters WHERE instance_id = ?"
|
||||||
|
_, err = ho.db.Exec(deleteStarters, ho.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete current starters: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete main record
|
||||||
|
query := "DELETE FROM heroic_op_instances WHERE id = ?"
|
||||||
|
_, err = ho.db.Exec(query, ho.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete heroic op instance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload refreshes the heroic op instance data from the database
|
||||||
|
func (ho *HeroicOP) Reload() error {
|
||||||
|
ho.mu.Lock()
|
||||||
|
defer ho.mu.Unlock()
|
||||||
|
|
||||||
|
if ho.isNew {
|
||||||
|
return fmt.Errorf("cannot reload unsaved heroic op instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload from database
|
||||||
|
reloaded, err := LoadHeroicOP(ho.db, ho.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all fields except database connection and isNew flag
|
||||||
|
ho.EncounterID = reloaded.EncounterID
|
||||||
|
ho.StarterID = reloaded.StarterID
|
||||||
|
ho.WheelID = reloaded.WheelID
|
||||||
|
ho.State = reloaded.State
|
||||||
|
ho.StartTime = reloaded.StartTime
|
||||||
|
ho.WheelStartTime = reloaded.WheelStartTime
|
||||||
|
ho.TimeRemaining = reloaded.TimeRemaining
|
||||||
|
ho.TotalTime = reloaded.TotalTime
|
||||||
|
ho.Complete = reloaded.Complete
|
||||||
|
ho.Countered = reloaded.Countered
|
||||||
|
ho.ShiftUsed = reloaded.ShiftUsed
|
||||||
|
ho.StarterProgress = reloaded.StarterProgress
|
||||||
|
ho.Participants = reloaded.Participants
|
||||||
|
ho.CurrentStarters = reloaded.CurrentStarters
|
||||||
|
ho.CompletedBy = reloaded.CompletedBy
|
||||||
|
ho.SpellName = reloaded.SpellName
|
||||||
|
ho.SpellDescription = reloaded.SpellDescription
|
||||||
|
|
||||||
|
ho.SaveNeeded = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddParticipant adds a character to the HO participants
|
// AddParticipant adds a character to the HO participants
|
||||||
func (ho *HeroicOP) AddParticipant(characterID int32) {
|
func (ho *HeroicOP) AddParticipant(characterID int32) {
|
||||||
ho.mu.Lock()
|
ho.mu.Lock()
|
||||||
@ -350,6 +369,46 @@ func (ho *HeroicOP) GetParticipants() []int32 {
|
|||||||
return participants
|
return participants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy creates a deep copy of the HO instance
|
||||||
|
func (ho *HeroicOP) Copy() *HeroicOP {
|
||||||
|
ho.mu.RLock()
|
||||||
|
defer ho.mu.RUnlock()
|
||||||
|
|
||||||
|
newHO := &HeroicOP{
|
||||||
|
ID: ho.ID,
|
||||||
|
EncounterID: ho.EncounterID,
|
||||||
|
StarterID: ho.StarterID,
|
||||||
|
WheelID: ho.WheelID,
|
||||||
|
State: ho.State,
|
||||||
|
StartTime: ho.StartTime,
|
||||||
|
WheelStartTime: ho.WheelStartTime,
|
||||||
|
TimeRemaining: ho.TimeRemaining,
|
||||||
|
TotalTime: ho.TotalTime,
|
||||||
|
Complete: ho.Complete,
|
||||||
|
Countered: ho.Countered, // Arrays are copied by value
|
||||||
|
ShiftUsed: ho.ShiftUsed,
|
||||||
|
StarterProgress: ho.StarterProgress,
|
||||||
|
CompletedBy: ho.CompletedBy,
|
||||||
|
SpellName: ho.SpellName,
|
||||||
|
SpellDescription: ho.SpellDescription,
|
||||||
|
Participants: make(map[int32]bool, len(ho.Participants)),
|
||||||
|
CurrentStarters: make([]int32, len(ho.CurrentStarters)),
|
||||||
|
db: ho.db,
|
||||||
|
isNew: true, // Copy is always new unless explicitly saved
|
||||||
|
SaveNeeded: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep copy participants map
|
||||||
|
for characterID, participating := range ho.Participants {
|
||||||
|
newHO.Participants[characterID] = participating
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep copy current starters slice
|
||||||
|
copy(newHO.CurrentStarters, ho.CurrentStarters)
|
||||||
|
|
||||||
|
return newHO
|
||||||
|
}
|
||||||
|
|
||||||
// StartStarterChain initiates the starter chain phase
|
// StartStarterChain initiates the starter chain phase
|
||||||
func (ho *HeroicOP) StartStarterChain(availableStarters []int32) {
|
func (ho *HeroicOP) StartStarterChain(availableStarters []int32) {
|
||||||
ho.mu.Lock()
|
ho.mu.Lock()
|
||||||
@ -364,7 +423,7 @@ func (ho *HeroicOP) StartStarterChain(availableStarters []int32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ProcessStarterAbility processes an ability during starter chain phase
|
// ProcessStarterAbility processes an ability during starter chain phase
|
||||||
func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterHeroicOPList) bool {
|
func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterList) bool {
|
||||||
ho.mu.Lock()
|
ho.mu.Lock()
|
||||||
defer ho.mu.Unlock()
|
defer ho.mu.Unlock()
|
||||||
|
|
||||||
@ -624,82 +683,6 @@ func (ho *HeroicOP) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy creates a deep copy of the HO instance
|
|
||||||
func (ho *HeroicOP) Copy() *HeroicOP {
|
|
||||||
ho.mu.RLock()
|
|
||||||
defer ho.mu.RUnlock()
|
|
||||||
|
|
||||||
newHO := &HeroicOP{
|
|
||||||
ID: ho.ID,
|
|
||||||
EncounterID: ho.EncounterID,
|
|
||||||
StarterID: ho.StarterID,
|
|
||||||
WheelID: ho.WheelID,
|
|
||||||
State: ho.State,
|
|
||||||
StartTime: ho.StartTime,
|
|
||||||
WheelStartTime: ho.WheelStartTime,
|
|
||||||
TimeRemaining: ho.TimeRemaining,
|
|
||||||
TotalTime: ho.TotalTime,
|
|
||||||
Complete: ho.Complete,
|
|
||||||
Countered: ho.Countered, // Arrays are copied by value
|
|
||||||
ShiftUsed: ho.ShiftUsed,
|
|
||||||
StarterProgress: ho.StarterProgress,
|
|
||||||
CompletedBy: ho.CompletedBy,
|
|
||||||
SpellName: ho.SpellName,
|
|
||||||
SpellDescription: ho.SpellDescription,
|
|
||||||
Participants: make(map[int32]bool, len(ho.Participants)),
|
|
||||||
CurrentStarters: make([]int32, len(ho.CurrentStarters)),
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deep copy participants map
|
|
||||||
for characterID, participating := range ho.Participants {
|
|
||||||
newHO.Participants[characterID] = participating
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deep copy current starters slice
|
|
||||||
copy(newHO.CurrentStarters, ho.CurrentStarters)
|
|
||||||
|
|
||||||
return newHO
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for random selection
|
|
||||||
|
|
||||||
// SelectRandomWheel selects a random wheel from a list based on chance values
|
|
||||||
func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel {
|
|
||||||
if len(wheels) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wheels) == 1 {
|
|
||||||
return wheels[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total chance
|
|
||||||
totalChance := float32(0.0)
|
|
||||||
for _, wheel := range wheels {
|
|
||||||
totalChance += wheel.Chance
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalChance <= 0.0 {
|
|
||||||
// If no chances set, select randomly with equal probability
|
|
||||||
return wheels[rand.Intn(len(wheels))]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random selection based on weighted chance
|
|
||||||
randomValue := rand.Float32() * totalChance
|
|
||||||
currentChance := float32(0.0)
|
|
||||||
|
|
||||||
for _, wheel := range wheels {
|
|
||||||
currentChance += wheel.Chance
|
|
||||||
if randomValue <= currentChance {
|
|
||||||
return wheel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to last wheel (shouldn't happen with proper math)
|
|
||||||
return wheels[len(wheels)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetElapsedTime returns the elapsed time since HO started
|
// GetElapsedTime returns the elapsed time since HO started
|
||||||
func (ho *HeroicOP) GetElapsedTime() time.Duration {
|
func (ho *HeroicOP) GetElapsedTime() time.Duration {
|
||||||
ho.mu.RLock()
|
ho.mu.RLock()
|
||||||
@ -718,4 +701,4 @@ func (ho *HeroicOP) GetWheelElapsedTime() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return time.Since(ho.WheelStartTime)
|
return time.Since(ho.WheelStartTime)
|
||||||
}
|
}
|
313
internal/heroic_ops/heroic_op_starter.go
Normal file
313
internal/heroic_ops/heroic_op_starter.go
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
package heroic_ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHeroicOPStarter creates a new heroic opportunity starter
|
||||||
|
func NewHeroicOPStarter(db *database.Database) *HeroicOPStarter {
|
||||||
|
return &HeroicOPStarter{
|
||||||
|
db: db,
|
||||||
|
isNew: true,
|
||||||
|
Abilities: [6]int16{},
|
||||||
|
SaveNeeded: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadHeroicOPStarter loads a heroic opportunity starter by ID
|
||||||
|
func LoadHeroicOPStarter(db *database.Database, id int32) (*HeroicOPStarter, error) {
|
||||||
|
starter := &HeroicOPStarter{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description FROM heroic_op_starters WHERE id = ?"
|
||||||
|
err := db.QueryRow(query, id).Scan(
|
||||||
|
&starter.ID,
|
||||||
|
&starter.StartClass,
|
||||||
|
&starter.StarterIcon,
|
||||||
|
&starter.Abilities[0],
|
||||||
|
&starter.Abilities[1],
|
||||||
|
&starter.Abilities[2],
|
||||||
|
&starter.Abilities[3],
|
||||||
|
&starter.Abilities[4],
|
||||||
|
&starter.Abilities[5],
|
||||||
|
&starter.Name,
|
||||||
|
&starter.Description,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load heroic op starter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
starter.SaveNeeded = false
|
||||||
|
return starter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the starter ID
|
||||||
|
func (hos *HeroicOPStarter) GetID() int32 {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
return hos.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save persists the starter to the database
|
||||||
|
func (hos *HeroicOPStarter) Save() error {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
|
||||||
|
if !hos.SaveNeeded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if hos.isNew {
|
||||||
|
// Insert new record
|
||||||
|
query := `INSERT INTO heroic_op_starters (id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
_, err := hos.db.Exec(query,
|
||||||
|
hos.ID,
|
||||||
|
hos.StartClass,
|
||||||
|
hos.StarterIcon,
|
||||||
|
hos.Abilities[0],
|
||||||
|
hos.Abilities[1],
|
||||||
|
hos.Abilities[2],
|
||||||
|
hos.Abilities[3],
|
||||||
|
hos.Abilities[4],
|
||||||
|
hos.Abilities[5],
|
||||||
|
hos.Name,
|
||||||
|
hos.Description,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert heroic op starter: %w", err)
|
||||||
|
}
|
||||||
|
hos.isNew = false
|
||||||
|
} else {
|
||||||
|
// Update existing record
|
||||||
|
query := `UPDATE heroic_op_starters SET start_class = ?, starter_icon = ?, ability1 = ?, ability2 = ?, ability3 = ?, ability4 = ?, ability5 = ?, ability6 = ?, name = ?, description = ? WHERE id = ?`
|
||||||
|
_, err := hos.db.Exec(query,
|
||||||
|
hos.StartClass,
|
||||||
|
hos.StarterIcon,
|
||||||
|
hos.Abilities[0],
|
||||||
|
hos.Abilities[1],
|
||||||
|
hos.Abilities[2],
|
||||||
|
hos.Abilities[3],
|
||||||
|
hos.Abilities[4],
|
||||||
|
hos.Abilities[5],
|
||||||
|
hos.Name,
|
||||||
|
hos.Description,
|
||||||
|
hos.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update heroic op starter: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hos.SaveNeeded = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the starter from the database
|
||||||
|
func (hos *HeroicOPStarter) Delete() error {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
|
||||||
|
if hos.isNew {
|
||||||
|
return nil // Nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "DELETE FROM heroic_op_starters WHERE id = ?"
|
||||||
|
_, err := hos.db.Exec(query, hos.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete heroic op starter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload refreshes the starter data from the database
|
||||||
|
func (hos *HeroicOPStarter) Reload() error {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
|
||||||
|
if hos.isNew {
|
||||||
|
return fmt.Errorf("cannot reload unsaved heroic op starter")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "SELECT start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description FROM heroic_op_starters WHERE id = ?"
|
||||||
|
err := hos.db.QueryRow(query, hos.ID).Scan(
|
||||||
|
&hos.StartClass,
|
||||||
|
&hos.StarterIcon,
|
||||||
|
&hos.Abilities[0],
|
||||||
|
&hos.Abilities[1],
|
||||||
|
&hos.Abilities[2],
|
||||||
|
&hos.Abilities[3],
|
||||||
|
&hos.Abilities[4],
|
||||||
|
&hos.Abilities[5],
|
||||||
|
&hos.Name,
|
||||||
|
&hos.Description,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reload heroic op starter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hos.SaveNeeded = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy creates a deep copy of the starter
|
||||||
|
func (hos *HeroicOPStarter) Copy() *HeroicOPStarter {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
|
||||||
|
newStarter := &HeroicOPStarter{
|
||||||
|
ID: hos.ID,
|
||||||
|
StartClass: hos.StartClass,
|
||||||
|
StarterIcon: hos.StarterIcon,
|
||||||
|
Abilities: hos.Abilities, // Arrays are copied by value
|
||||||
|
Name: hos.Name,
|
||||||
|
Description: hos.Description,
|
||||||
|
db: hos.db,
|
||||||
|
isNew: true, // Copy is always new unless explicitly saved
|
||||||
|
SaveNeeded: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newStarter
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAbility returns the ability icon at the specified position
|
||||||
|
func (hos *HeroicOPStarter) GetAbility(position int) int16 {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
|
||||||
|
if position < 0 || position >= MaxAbilities {
|
||||||
|
return AbilityIconNone
|
||||||
|
}
|
||||||
|
|
||||||
|
return hos.Abilities[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAbility sets the ability icon at the specified position
|
||||||
|
func (hos *HeroicOPStarter) SetAbility(position int, abilityIcon int16) bool {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
|
||||||
|
if position < 0 || position >= MaxAbilities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hos.Abilities[position] = abilityIcon
|
||||||
|
hos.SaveNeeded = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsComplete checks if the starter chain is complete (has completion marker)
|
||||||
|
func (hos *HeroicOPStarter) IsComplete(position int) bool {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
|
||||||
|
if position < 0 || position >= MaxAbilities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hos.Abilities[position] == AbilityIconAny
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanInitiate checks if the specified class can initiate this starter
|
||||||
|
func (hos *HeroicOPStarter) CanInitiate(playerClass int8) bool {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
|
||||||
|
return hos.StartClass == ClassAny || hos.StartClass == playerClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesAbility checks if the given ability matches the current position
|
||||||
|
func (hos *HeroicOPStarter) MatchesAbility(position int, abilityIcon int16) bool {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
|
||||||
|
if position < 0 || position >= MaxAbilities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredAbility := hos.Abilities[position]
|
||||||
|
|
||||||
|
// Wildcard matches any ability
|
||||||
|
if requiredAbility == AbilityIconAny {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match required
|
||||||
|
return requiredAbility == abilityIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the starter is properly configured
|
||||||
|
func (hos *HeroicOPStarter) Validate() error {
|
||||||
|
hos.mu.RLock()
|
||||||
|
defer hos.mu.RUnlock()
|
||||||
|
|
||||||
|
if hos.ID <= 0 {
|
||||||
|
return fmt.Errorf("invalid starter ID: %d", hos.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hos.StarterIcon <= 0 {
|
||||||
|
return fmt.Errorf("invalid starter icon: %d", hos.StarterIcon)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for at least one non-zero ability
|
||||||
|
hasAbility := false
|
||||||
|
for _, ability := range hos.Abilities {
|
||||||
|
if ability != AbilityIconNone {
|
||||||
|
hasAbility = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasAbility {
|
||||||
|
return fmt.Errorf("starter must have at least one ability")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the starter ID
|
||||||
|
func (hos *HeroicOPStarter) SetID(id int32) {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
hos.ID = id
|
||||||
|
hos.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStartClass sets the start class
|
||||||
|
func (hos *HeroicOPStarter) SetStartClass(startClass int8) {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
hos.StartClass = startClass
|
||||||
|
hos.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStarterIcon sets the starter icon
|
||||||
|
func (hos *HeroicOPStarter) SetStarterIcon(icon int16) {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
hos.StarterIcon = icon
|
||||||
|
hos.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName sets the starter name
|
||||||
|
func (hos *HeroicOPStarter) SetName(name string) {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
hos.Name = name
|
||||||
|
hos.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDescription sets the starter description
|
||||||
|
func (hos *HeroicOPStarter) SetDescription(description string) {
|
||||||
|
hos.mu.Lock()
|
||||||
|
defer hos.mu.Unlock()
|
||||||
|
hos.Description = description
|
||||||
|
hos.SaveNeeded = true
|
||||||
|
}
|
405
internal/heroic_ops/heroic_op_wheel.go
Normal file
405
internal/heroic_ops/heroic_op_wheel.go
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package heroic_ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHeroicOPWheel creates a new heroic opportunity wheel
|
||||||
|
func NewHeroicOPWheel(db *database.Database) *HeroicOPWheel {
|
||||||
|
return &HeroicOPWheel{
|
||||||
|
db: db,
|
||||||
|
isNew: true,
|
||||||
|
Abilities: [6]int16{},
|
||||||
|
Chance: 1.0,
|
||||||
|
SaveNeeded: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadHeroicOPWheel loads a heroic opportunity wheel by ID
|
||||||
|
func LoadHeroicOPWheel(db *database.Database, id int32) (*HeroicOPWheel, error) {
|
||||||
|
wheel := &HeroicOPWheel{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6,
|
||||||
|
spell_id, name, description, required_players FROM heroic_op_wheels WHERE id = ?`
|
||||||
|
err := db.QueryRow(query, id).Scan(
|
||||||
|
&wheel.ID,
|
||||||
|
&wheel.StarterLinkID,
|
||||||
|
&wheel.Order,
|
||||||
|
&wheel.ShiftIcon,
|
||||||
|
&wheel.Chance,
|
||||||
|
&wheel.Abilities[0],
|
||||||
|
&wheel.Abilities[1],
|
||||||
|
&wheel.Abilities[2],
|
||||||
|
&wheel.Abilities[3],
|
||||||
|
&wheel.Abilities[4],
|
||||||
|
&wheel.Abilities[5],
|
||||||
|
&wheel.SpellID,
|
||||||
|
&wheel.Name,
|
||||||
|
&wheel.Description,
|
||||||
|
&wheel.RequiredPlayers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load heroic op wheel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel.SaveNeeded = false
|
||||||
|
return wheel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the wheel ID
|
||||||
|
func (how *HeroicOPWheel) GetID() int32 {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
return how.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save persists the wheel to the database
|
||||||
|
func (how *HeroicOPWheel) Save() error {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
|
||||||
|
if !how.SaveNeeded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if how.isNew {
|
||||||
|
// Insert new record
|
||||||
|
query := `INSERT INTO heroic_op_wheels (id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
_, err := how.db.Exec(query,
|
||||||
|
how.ID,
|
||||||
|
how.StarterLinkID,
|
||||||
|
how.Order,
|
||||||
|
how.ShiftIcon,
|
||||||
|
how.Chance,
|
||||||
|
how.Abilities[0],
|
||||||
|
how.Abilities[1],
|
||||||
|
how.Abilities[2],
|
||||||
|
how.Abilities[3],
|
||||||
|
how.Abilities[4],
|
||||||
|
how.Abilities[5],
|
||||||
|
how.SpellID,
|
||||||
|
how.Name,
|
||||||
|
how.Description,
|
||||||
|
how.RequiredPlayers,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert heroic op wheel: %w", err)
|
||||||
|
}
|
||||||
|
how.isNew = false
|
||||||
|
} else {
|
||||||
|
// Update existing record
|
||||||
|
query := `UPDATE heroic_op_wheels SET starter_link_id = ?, chain_order = ?, shift_icon = ?, chance = ?, ability1 = ?, ability2 = ?, ability3 = ?, ability4 = ?, ability5 = ?, ability6 = ?, spell_id = ?, name = ?, description = ?, required_players = ? WHERE id = ?`
|
||||||
|
_, err := how.db.Exec(query,
|
||||||
|
how.StarterLinkID,
|
||||||
|
how.Order,
|
||||||
|
how.ShiftIcon,
|
||||||
|
how.Chance,
|
||||||
|
how.Abilities[0],
|
||||||
|
how.Abilities[1],
|
||||||
|
how.Abilities[2],
|
||||||
|
how.Abilities[3],
|
||||||
|
how.Abilities[4],
|
||||||
|
how.Abilities[5],
|
||||||
|
how.SpellID,
|
||||||
|
how.Name,
|
||||||
|
how.Description,
|
||||||
|
how.RequiredPlayers,
|
||||||
|
how.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update heroic op wheel: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
how.SaveNeeded = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the wheel from the database
|
||||||
|
func (how *HeroicOPWheel) Delete() error {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
|
||||||
|
if how.isNew {
|
||||||
|
return nil // Nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "DELETE FROM heroic_op_wheels WHERE id = ?"
|
||||||
|
_, err := how.db.Exec(query, how.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete heroic op wheel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload refreshes the wheel data from the database
|
||||||
|
func (how *HeroicOPWheel) Reload() error {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
|
||||||
|
if how.isNew {
|
||||||
|
return fmt.Errorf("cannot reload unsaved heroic op wheel")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6,
|
||||||
|
spell_id, name, description, required_players FROM heroic_op_wheels WHERE id = ?`
|
||||||
|
err := how.db.QueryRow(query, how.ID).Scan(
|
||||||
|
&how.StarterLinkID,
|
||||||
|
&how.Order,
|
||||||
|
&how.ShiftIcon,
|
||||||
|
&how.Chance,
|
||||||
|
&how.Abilities[0],
|
||||||
|
&how.Abilities[1],
|
||||||
|
&how.Abilities[2],
|
||||||
|
&how.Abilities[3],
|
||||||
|
&how.Abilities[4],
|
||||||
|
&how.Abilities[5],
|
||||||
|
&how.SpellID,
|
||||||
|
&how.Name,
|
||||||
|
&how.Description,
|
||||||
|
&how.RequiredPlayers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reload heroic op wheel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
how.SaveNeeded = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy creates a deep copy of the wheel
|
||||||
|
func (how *HeroicOPWheel) Copy() *HeroicOPWheel {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
newWheel := &HeroicOPWheel{
|
||||||
|
ID: how.ID,
|
||||||
|
StarterLinkID: how.StarterLinkID,
|
||||||
|
Order: how.Order,
|
||||||
|
ShiftIcon: how.ShiftIcon,
|
||||||
|
Chance: how.Chance,
|
||||||
|
Abilities: how.Abilities, // Arrays are copied by value
|
||||||
|
SpellID: how.SpellID,
|
||||||
|
Name: how.Name,
|
||||||
|
Description: how.Description,
|
||||||
|
RequiredPlayers: how.RequiredPlayers,
|
||||||
|
db: how.db,
|
||||||
|
isNew: true, // Copy is always new unless explicitly saved
|
||||||
|
SaveNeeded: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWheel
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAbility returns the ability icon at the specified position
|
||||||
|
func (how *HeroicOPWheel) GetAbility(position int) int16 {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
if position < 0 || position >= MaxAbilities {
|
||||||
|
return AbilityIconNone
|
||||||
|
}
|
||||||
|
|
||||||
|
return how.Abilities[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAbility sets the ability icon at the specified position
|
||||||
|
func (how *HeroicOPWheel) SetAbility(position int, abilityIcon int16) bool {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
|
||||||
|
if position < 0 || position >= MaxAbilities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
how.Abilities[position] = abilityIcon
|
||||||
|
how.SaveNeeded = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOrdered checks if this wheel requires ordered completion
|
||||||
|
func (how *HeroicOPWheel) IsOrdered() bool {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
return how.Order >= WheelOrderOrdered
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasShift checks if this wheel has a shift ability
|
||||||
|
func (how *HeroicOPWheel) HasShift() bool {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
return how.ShiftIcon > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanShift checks if shifting is possible with the given ability
|
||||||
|
func (how *HeroicOPWheel) CanShift(abilityIcon int16) bool {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
return how.ShiftIcon > 0 && how.ShiftIcon == abilityIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextRequiredAbility returns the next required ability for ordered wheels
|
||||||
|
func (how *HeroicOPWheel) GetNextRequiredAbility(countered [6]int8) int16 {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
if !how.IsOrdered() {
|
||||||
|
return AbilityIconNone // Any uncompleted ability works for unordered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first uncompleted ability in order
|
||||||
|
for i := 0; i < MaxAbilities; i++ {
|
||||||
|
if countered[i] == 0 && how.Abilities[i] != AbilityIconNone {
|
||||||
|
return how.Abilities[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AbilityIconNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanUseAbility checks if an ability can be used on this wheel
|
||||||
|
func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bool {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
// Check if this is a shift attempt
|
||||||
|
if how.CanShift(abilityIcon) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if how.IsOrdered() {
|
||||||
|
// For ordered wheels, only the next required ability can be used
|
||||||
|
nextRequired := how.GetNextRequiredAbility(countered)
|
||||||
|
return nextRequired == abilityIcon
|
||||||
|
} else {
|
||||||
|
// For unordered wheels, any uncompleted matching ability can be used
|
||||||
|
for i := 0; i < MaxAbilities; i++ {
|
||||||
|
if countered[i] == 0 && how.Abilities[i] == abilityIcon {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the wheel is properly configured
|
||||||
|
func (how *HeroicOPWheel) Validate() error {
|
||||||
|
how.mu.RLock()
|
||||||
|
defer how.mu.RUnlock()
|
||||||
|
|
||||||
|
if how.ID <= 0 {
|
||||||
|
return fmt.Errorf("invalid wheel ID: %d", how.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if how.StarterLinkID <= 0 {
|
||||||
|
return fmt.Errorf("invalid starter link ID: %d", how.StarterLinkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if how.Chance < MinChance || how.Chance > MaxChance {
|
||||||
|
return fmt.Errorf("invalid chance: %f (must be %f-%f)", how.Chance, MinChance, MaxChance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if how.SpellID <= 0 {
|
||||||
|
return fmt.Errorf("invalid spell ID: %d", how.SpellID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for at least one non-zero ability
|
||||||
|
hasAbility := false
|
||||||
|
for _, ability := range how.Abilities {
|
||||||
|
if ability != AbilityIconNone {
|
||||||
|
hasAbility = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasAbility {
|
||||||
|
return fmt.Errorf("wheel must have at least one ability")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the wheel ID
|
||||||
|
func (how *HeroicOPWheel) SetID(id int32) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.ID = id
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStarterLinkID sets the starter link ID
|
||||||
|
func (how *HeroicOPWheel) SetStarterLinkID(id int32) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.StarterLinkID = id
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOrder sets the wheel order
|
||||||
|
func (how *HeroicOPWheel) SetOrder(order int8) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.Order = order
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetShiftIcon sets the shift icon
|
||||||
|
func (how *HeroicOPWheel) SetShiftIcon(icon int16) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.ShiftIcon = icon
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChance sets the wheel chance
|
||||||
|
func (how *HeroicOPWheel) SetChance(chance float32) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.Chance = chance
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSpellID sets the spell ID
|
||||||
|
func (how *HeroicOPWheel) SetSpellID(id int32) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.SpellID = id
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName sets the wheel name
|
||||||
|
func (how *HeroicOPWheel) SetName(name string) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.Name = name
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDescription sets the wheel description
|
||||||
|
func (how *HeroicOPWheel) SetDescription(description string) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.Description = description
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequiredPlayers sets the required players
|
||||||
|
func (how *HeroicOPWheel) SetRequiredPlayers(required int8) {
|
||||||
|
how.mu.Lock()
|
||||||
|
defer how.mu.Unlock()
|
||||||
|
how.RequiredPlayers = required
|
||||||
|
how.SaveNeeded = true
|
||||||
|
}
|
@ -4,16 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHeroicOPManager creates a new heroic opportunity manager
|
// NewHeroicOPManager creates a new heroic opportunity manager
|
||||||
func NewHeroicOPManager(masterList *MasterHeroicOPList, database HeroicOPDatabase,
|
func NewHeroicOPManager(masterList *MasterList, database HeroicOPDatabase, db *database.Database,
|
||||||
clientManager ClientManager, encounterManager EncounterManager, playerManager PlayerManager) *HeroicOPManager {
|
clientManager ClientManager, encounterManager EncounterManager, playerManager PlayerManager) *HeroicOPManager {
|
||||||
return &HeroicOPManager{
|
return &HeroicOPManager{
|
||||||
activeHOs: make(map[int64]*HeroicOP),
|
activeHOs: make(map[int64]*HeroicOP),
|
||||||
encounterHOs: make(map[int32][]*HeroicOP),
|
encounterHOs: make(map[int32][]*HeroicOP),
|
||||||
masterList: masterList,
|
masterList: masterList,
|
||||||
database: database,
|
database: database,
|
||||||
|
db: db,
|
||||||
clientManager: clientManager,
|
clientManager: clientManager,
|
||||||
encounterManager: encounterManager,
|
encounterManager: encounterManager,
|
||||||
playerManager: playerManager,
|
playerManager: playerManager,
|
||||||
@ -49,14 +52,14 @@ func (hom *HeroicOPManager) Initialize(ctx context.Context, config *HeroicOPConf
|
|||||||
|
|
||||||
// Ensure master list is loaded
|
// Ensure master list is loaded
|
||||||
if !hom.masterList.IsLoaded() {
|
if !hom.masterList.IsLoaded() {
|
||||||
if err := hom.masterList.LoadFromDatabase(ctx, hom.database); err != nil {
|
// The master list will need to be loaded externally with LoadFromDatabase(db)
|
||||||
return fmt.Errorf("failed to load heroic opportunities: %w", err)
|
return fmt.Errorf("master list must be loaded before initializing manager")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hom.logger != nil {
|
if hom.logger != nil {
|
||||||
|
starterCount, wheelCount := hom.masterList.GetCount()
|
||||||
hom.logger.LogInfo("heroic_ops", "Initialized HO manager with %d starters and %d wheels",
|
hom.logger.LogInfo("heroic_ops", "Initialized HO manager with %d starters and %d wheels",
|
||||||
hom.masterList.GetStarterCount(), hom.masterList.GetWheelCount())
|
starterCount, wheelCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -90,7 +93,7 @@ func (hom *HeroicOPManager) StartHeroicOpportunity(ctx context.Context, encounte
|
|||||||
instanceID := hom.nextInstanceID
|
instanceID := hom.nextInstanceID
|
||||||
hom.nextInstanceID++
|
hom.nextInstanceID++
|
||||||
|
|
||||||
ho := NewHeroicOP(instanceID, encounterID)
|
ho := NewHeroicOP(hom.db, instanceID, encounterID)
|
||||||
ho.AddParticipant(initiatorID)
|
ho.AddParticipant(initiatorID)
|
||||||
|
|
||||||
// Prepare starter IDs for chain phase
|
// Prepare starter IDs for chain phase
|
||||||
@ -339,22 +342,29 @@ func (hom *HeroicOPManager) GetStatistics() *HeroicOPStatistics {
|
|||||||
hom.mu.RLock()
|
hom.mu.RLock()
|
||||||
defer hom.mu.RUnlock()
|
defer hom.mu.RUnlock()
|
||||||
|
|
||||||
stats := &HeroicOPStatistics{
|
// Use the master list's statistics and supplement with runtime data
|
||||||
ActiveHOCount: len(hom.activeHOs),
|
masterStats := hom.masterList.GetStatistics()
|
||||||
ParticipationStats: make(map[int32]int64),
|
|
||||||
}
|
// Add runtime statistics
|
||||||
|
participationStats := make(map[int32]int64)
|
||||||
// Count participants
|
|
||||||
for _, ho := range hom.activeHOs {
|
for _, ho := range hom.activeHOs {
|
||||||
for characterID := range ho.Participants {
|
for characterID := range ho.Participants {
|
||||||
stats.ParticipationStats[characterID]++
|
participationStats[characterID]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get additional statistics from database
|
// Return extended statistics
|
||||||
// This is a simplified implementation
|
return &HeroicOPStatistics{
|
||||||
|
TotalStarters: masterStats.TotalStarters,
|
||||||
return stats
|
TotalWheels: masterStats.TotalWheels,
|
||||||
|
ClassDistribution: masterStats.ClassDistribution,
|
||||||
|
OrderedWheelsCount: masterStats.OrderedWheelsCount,
|
||||||
|
ShiftWheelsCount: masterStats.ShiftWheelsCount,
|
||||||
|
SpellCount: masterStats.SpellCount,
|
||||||
|
AverageChance: masterStats.AverageChance,
|
||||||
|
ActiveHOCount: int64(len(hom.activeHOs)),
|
||||||
|
// TODO: Get additional statistics from database
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
@ -475,8 +485,7 @@ func (hom *HeroicOPManager) sendWheelUpdate(ho *HeroicOP, wheel *HeroicOPWheel)
|
|||||||
}
|
}
|
||||||
|
|
||||||
participants := ho.GetParticipants()
|
participants := ho.GetParticipants()
|
||||||
packetBuilder := NewHeroicOPPacketBuilder(0) // Default version
|
data := ho.GetPacketData(wheel)
|
||||||
data := packetBuilder.ToPacketData(ho, wheel)
|
|
||||||
|
|
||||||
for _, characterID := range participants {
|
for _, characterID := range participants {
|
||||||
if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil {
|
if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil {
|
||||||
@ -495,8 +504,7 @@ func (hom *HeroicOPManager) sendProgressUpdate(ho *HeroicOP) {
|
|||||||
|
|
||||||
participants := ho.GetParticipants()
|
participants := ho.GetParticipants()
|
||||||
wheel := hom.masterList.GetWheel(ho.WheelID)
|
wheel := hom.masterList.GetWheel(ho.WheelID)
|
||||||
packetBuilder := NewHeroicOPPacketBuilder(0)
|
data := ho.GetPacketData(wheel)
|
||||||
data := packetBuilder.ToPacketData(ho, wheel)
|
|
||||||
|
|
||||||
for _, characterID := range participants {
|
for _, characterID := range participants {
|
||||||
if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil {
|
if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil {
|
||||||
@ -548,13 +556,12 @@ func (hom *HeroicOPManager) sendShiftUpdate(ho *HeroicOP, oldWheelID, newWheelID
|
|||||||
}
|
}
|
||||||
|
|
||||||
participants := ho.GetParticipants()
|
participants := ho.GetParticipants()
|
||||||
packetBuilder := NewHeroicOPPacketBuilder(0)
|
|
||||||
|
|
||||||
for range participants {
|
for _, characterID := range participants {
|
||||||
if packet, err := packetBuilder.BuildHOShiftPacket(ho, oldWheelID, newWheelID); err == nil {
|
// TODO: Implement shift packet sending when client manager supports it
|
||||||
// TODO: Send packet through client manager using characterID
|
_ = characterID
|
||||||
_ = packet // Placeholder
|
_ = oldWheelID
|
||||||
}
|
_ = newWheelID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
720
internal/heroic_ops/master.go
Normal file
720
internal/heroic_ops/master.go
Normal file
@ -0,0 +1,720 @@
|
|||||||
|
package heroic_ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MasterList provides optimized heroic opportunity management with O(1) performance characteristics
|
||||||
|
type MasterList struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// Core data storage - O(1) access by ID
|
||||||
|
starters map[int32]*HeroicOPStarter // starter_id -> starter
|
||||||
|
wheels map[int32]*HeroicOPWheel // wheel_id -> wheel
|
||||||
|
|
||||||
|
// Specialized indices for O(1) lookups
|
||||||
|
byClass map[int8]map[int32]*HeroicOPStarter // class -> starter_id -> starter
|
||||||
|
byStarterID map[int32][]*HeroicOPWheel // starter_id -> wheels
|
||||||
|
bySpellID map[int32][]*HeroicOPWheel // spell_id -> wheels
|
||||||
|
byChance map[string][]*HeroicOPWheel // chance_range -> wheels
|
||||||
|
orderedWheels map[int32]*HeroicOPWheel // wheel_id -> ordered wheels only
|
||||||
|
shiftWheels map[int32]*HeroicOPWheel // wheel_id -> wheels with shifts
|
||||||
|
spellInfo map[int32]SpellInfo // spell_id -> spell info
|
||||||
|
|
||||||
|
// Lazy metadata caching - computed on demand
|
||||||
|
totalStarters int
|
||||||
|
totalWheels int
|
||||||
|
classDistribution map[int8]int
|
||||||
|
metadataValid bool
|
||||||
|
|
||||||
|
loaded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMasterList creates a new bespoke heroic opportunity master list
|
||||||
|
func NewMasterList() *MasterList {
|
||||||
|
return &MasterList{
|
||||||
|
starters: make(map[int32]*HeroicOPStarter),
|
||||||
|
wheels: make(map[int32]*HeroicOPWheel),
|
||||||
|
byClass: make(map[int8]map[int32]*HeroicOPStarter),
|
||||||
|
byStarterID: make(map[int32][]*HeroicOPWheel),
|
||||||
|
bySpellID: make(map[int32][]*HeroicOPWheel),
|
||||||
|
byChance: make(map[string][]*HeroicOPWheel),
|
||||||
|
orderedWheels: make(map[int32]*HeroicOPWheel),
|
||||||
|
shiftWheels: make(map[int32]*HeroicOPWheel),
|
||||||
|
spellInfo: make(map[int32]SpellInfo),
|
||||||
|
loaded: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromDatabase loads all heroic opportunities from the database with optimal indexing
|
||||||
|
func (ml *MasterList) LoadFromDatabase(db *database.Database) error {
|
||||||
|
ml.mu.Lock()
|
||||||
|
defer ml.mu.Unlock()
|
||||||
|
|
||||||
|
// Clear existing data
|
||||||
|
ml.clearIndices()
|
||||||
|
|
||||||
|
// Load all starters
|
||||||
|
if err := ml.loadStarters(db); err != nil {
|
||||||
|
return fmt.Errorf("failed to load starters: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all wheels
|
||||||
|
if err := ml.loadWheels(db); err != nil {
|
||||||
|
return fmt.Errorf("failed to load wheels: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build specialized indices for O(1) performance
|
||||||
|
ml.buildIndices()
|
||||||
|
|
||||||
|
ml.loaded = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadStarters loads all starters from database
|
||||||
|
func (ml *MasterList) loadStarters(db *database.Database) error {
|
||||||
|
query := `SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description
|
||||||
|
FROM heroic_op_starters ORDER BY id`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
starter := &HeroicOPStarter{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&starter.ID,
|
||||||
|
&starter.StartClass,
|
||||||
|
&starter.StarterIcon,
|
||||||
|
&starter.Abilities[0],
|
||||||
|
&starter.Abilities[1],
|
||||||
|
&starter.Abilities[2],
|
||||||
|
&starter.Abilities[3],
|
||||||
|
&starter.Abilities[4],
|
||||||
|
&starter.Abilities[5],
|
||||||
|
&starter.Name,
|
||||||
|
&starter.Description,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
starter.SaveNeeded = false
|
||||||
|
ml.starters[starter.ID] = starter
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadWheels loads all wheels from database
|
||||||
|
func (ml *MasterList) loadWheels(db *database.Database) error {
|
||||||
|
query := `SELECT id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6,
|
||||||
|
spell_id, name, description, required_players
|
||||||
|
FROM heroic_op_wheels ORDER BY id`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
wheel := &HeroicOPWheel{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&wheel.ID,
|
||||||
|
&wheel.StarterLinkID,
|
||||||
|
&wheel.Order,
|
||||||
|
&wheel.ShiftIcon,
|
||||||
|
&wheel.Chance,
|
||||||
|
&wheel.Abilities[0],
|
||||||
|
&wheel.Abilities[1],
|
||||||
|
&wheel.Abilities[2],
|
||||||
|
&wheel.Abilities[3],
|
||||||
|
&wheel.Abilities[4],
|
||||||
|
&wheel.Abilities[5],
|
||||||
|
&wheel.SpellID,
|
||||||
|
&wheel.Name,
|
||||||
|
&wheel.Description,
|
||||||
|
&wheel.RequiredPlayers,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel.SaveNeeded = false
|
||||||
|
ml.wheels[wheel.ID] = wheel
|
||||||
|
|
||||||
|
// Store spell info
|
||||||
|
ml.spellInfo[wheel.SpellID] = SpellInfo{
|
||||||
|
ID: wheel.SpellID,
|
||||||
|
Name: wheel.Name,
|
||||||
|
Description: wheel.Description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildIndices creates specialized indices for O(1) performance
|
||||||
|
func (ml *MasterList) buildIndices() {
|
||||||
|
// Build class-based starter index
|
||||||
|
for _, starter := range ml.starters {
|
||||||
|
if ml.byClass[starter.StartClass] == nil {
|
||||||
|
ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter)
|
||||||
|
}
|
||||||
|
ml.byClass[starter.StartClass][starter.ID] = starter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build wheel indices
|
||||||
|
for _, wheel := range ml.wheels {
|
||||||
|
// By starter ID
|
||||||
|
ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel)
|
||||||
|
|
||||||
|
// By spell ID
|
||||||
|
ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel)
|
||||||
|
|
||||||
|
// By chance range (for performance optimization)
|
||||||
|
chanceRange := ml.getChanceRange(wheel.Chance)
|
||||||
|
ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel)
|
||||||
|
|
||||||
|
// Special wheel types
|
||||||
|
if wheel.IsOrdered() {
|
||||||
|
ml.orderedWheels[wheel.ID] = wheel
|
||||||
|
}
|
||||||
|
|
||||||
|
if wheel.HasShift() {
|
||||||
|
ml.shiftWheels[wheel.ID] = wheel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort wheels by chance for deterministic selection
|
||||||
|
for _, wheels := range ml.byStarterID {
|
||||||
|
sort.Slice(wheels, func(i, j int) bool {
|
||||||
|
return wheels[i].Chance > wheels[j].Chance
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.metadataValid = false // Invalidate cached metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearIndices clears all indices
|
||||||
|
func (ml *MasterList) clearIndices() {
|
||||||
|
ml.starters = make(map[int32]*HeroicOPStarter)
|
||||||
|
ml.wheels = make(map[int32]*HeroicOPWheel)
|
||||||
|
ml.byClass = make(map[int8]map[int32]*HeroicOPStarter)
|
||||||
|
ml.byStarterID = make(map[int32][]*HeroicOPWheel)
|
||||||
|
ml.bySpellID = make(map[int32][]*HeroicOPWheel)
|
||||||
|
ml.byChance = make(map[string][]*HeroicOPWheel)
|
||||||
|
ml.orderedWheels = make(map[int32]*HeroicOPWheel)
|
||||||
|
ml.shiftWheels = make(map[int32]*HeroicOPWheel)
|
||||||
|
ml.spellInfo = make(map[int32]SpellInfo)
|
||||||
|
ml.metadataValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getChanceRange returns a chance range string for indexing
|
||||||
|
func (ml *MasterList) getChanceRange(chance float32) string {
|
||||||
|
switch {
|
||||||
|
case chance >= 75.0:
|
||||||
|
return "very_high"
|
||||||
|
case chance >= 50.0:
|
||||||
|
return "high"
|
||||||
|
case chance >= 25.0:
|
||||||
|
return "medium"
|
||||||
|
case chance >= 10.0:
|
||||||
|
return "low"
|
||||||
|
default:
|
||||||
|
return "very_low"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// O(1) Starter Operations
|
||||||
|
|
||||||
|
// GetStarter retrieves a starter by ID with O(1) performance
|
||||||
|
func (ml *MasterList) GetStarter(id int32) *HeroicOPStarter {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
return ml.starters[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStartersForClass returns all starters for a specific class with O(1) performance
|
||||||
|
func (ml *MasterList) GetStartersForClass(class int8) []*HeroicOPStarter {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
var result []*HeroicOPStarter
|
||||||
|
|
||||||
|
// Add class-specific starters
|
||||||
|
if classStarters, exists := ml.byClass[class]; exists {
|
||||||
|
for _, starter := range classStarters {
|
||||||
|
result = append(result, starter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add universal starters (class 0 = any)
|
||||||
|
if universalStarters, exists := ml.byClass[ClassAny]; exists {
|
||||||
|
for _, starter := range universalStarters {
|
||||||
|
result = append(result, starter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// O(1) Wheel Operations
|
||||||
|
|
||||||
|
// GetWheel retrieves a wheel by ID with O(1) performance
|
||||||
|
func (ml *MasterList) GetWheel(id int32) *HeroicOPWheel {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
return ml.wheels[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWheelsForStarter returns all wheels for a starter with O(1) performance
|
||||||
|
func (ml *MasterList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
wheels := ml.byStarterID[starterID]
|
||||||
|
if wheels == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return copy to prevent external modification
|
||||||
|
result := make([]*HeroicOPWheel, len(wheels))
|
||||||
|
copy(result, wheels)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWheelsForSpell returns all wheels that cast a specific spell with O(1) performance
|
||||||
|
func (ml *MasterList) GetWheelsForSpell(spellID int32) []*HeroicOPWheel {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
wheels := ml.bySpellID[spellID]
|
||||||
|
if wheels == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return copy to prevent external modification
|
||||||
|
result := make([]*HeroicOPWheel, len(wheels))
|
||||||
|
copy(result, wheels)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectRandomWheel selects a random wheel from starter's wheels with optimized performance
|
||||||
|
func (ml *MasterList) SelectRandomWheel(starterID int32) *HeroicOPWheel {
|
||||||
|
wheels := ml.GetWheelsForStarter(starterID)
|
||||||
|
if len(wheels) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return SelectRandomWheel(wheels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// O(1) Specialized Queries
|
||||||
|
|
||||||
|
// GetOrderedWheels returns all ordered wheels with O(1) performance
|
||||||
|
func (ml *MasterList) GetOrderedWheels() []*HeroicOPWheel {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]*HeroicOPWheel, 0, len(ml.orderedWheels))
|
||||||
|
for _, wheel := range ml.orderedWheels {
|
||||||
|
result = append(result, wheel)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShiftWheels returns all wheels with shift abilities with O(1) performance
|
||||||
|
func (ml *MasterList) GetShiftWheels() []*HeroicOPWheel {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]*HeroicOPWheel, 0, len(ml.shiftWheels))
|
||||||
|
for _, wheel := range ml.shiftWheels {
|
||||||
|
result = append(result, wheel)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWheelsByChanceRange returns wheels within a chance range with O(1) performance
|
||||||
|
func (ml *MasterList) GetWheelsByChanceRange(minChance, maxChance float32) []*HeroicOPWheel {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
var result []*HeroicOPWheel
|
||||||
|
|
||||||
|
// Use indexed chance ranges for performance
|
||||||
|
for rangeKey, wheels := range ml.byChance {
|
||||||
|
for _, wheel := range wheels {
|
||||||
|
if wheel.Chance >= minChance && wheel.Chance <= maxChance {
|
||||||
|
result = append(result, wheel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Break early if we found wheels in this range
|
||||||
|
if len(result) > 0 && !ml.shouldContinueChanceSearch(rangeKey, minChance, maxChance) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldContinueChanceSearch determines if we should continue searching other chance ranges
|
||||||
|
func (ml *MasterList) shouldContinueChanceSearch(currentRange string, minChance, maxChance float32) bool {
|
||||||
|
// Optimize search by stopping early based on range analysis
|
||||||
|
switch currentRange {
|
||||||
|
case "very_high":
|
||||||
|
return maxChance < 75.0
|
||||||
|
case "high":
|
||||||
|
return maxChance < 50.0 || minChance > 75.0
|
||||||
|
case "medium":
|
||||||
|
return maxChance < 25.0 || minChance > 50.0
|
||||||
|
case "low":
|
||||||
|
return maxChance < 10.0 || minChance > 25.0
|
||||||
|
default: // very_low
|
||||||
|
return minChance > 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spell Information
|
||||||
|
|
||||||
|
// GetSpellInfo returns spell information with O(1) performance
|
||||||
|
func (ml *MasterList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
info, exists := ml.spellInfo[spellID]
|
||||||
|
return &info, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced Search with Optimized Performance
|
||||||
|
|
||||||
|
// Search performs advanced search with multiple criteria
|
||||||
|
func (ml *MasterList) Search(criteria HeroicOPSearchCriteria) *HeroicOPSearchResults {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
results := &HeroicOPSearchResults{
|
||||||
|
Starters: make([]*HeroicOPStarter, 0),
|
||||||
|
Wheels: make([]*HeroicOPWheel, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize search strategy based on criteria
|
||||||
|
if criteria.StarterClass != 0 {
|
||||||
|
// Class-specific search - use class index
|
||||||
|
if classStarters, exists := ml.byClass[criteria.StarterClass]; exists {
|
||||||
|
for _, starter := range classStarters {
|
||||||
|
if ml.matchesStarterCriteria(starter, criteria) {
|
||||||
|
results.Starters = append(results.Starters, starter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Search all starters
|
||||||
|
for _, starter := range ml.starters {
|
||||||
|
if ml.matchesStarterCriteria(starter, criteria) {
|
||||||
|
results.Starters = append(results.Starters, starter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wheel search optimization
|
||||||
|
if criteria.SpellID != 0 {
|
||||||
|
// Spell-specific search - use spell index
|
||||||
|
if spellWheels, exists := ml.bySpellID[criteria.SpellID]; exists {
|
||||||
|
for _, wheel := range spellWheels {
|
||||||
|
if ml.matchesWheelCriteria(wheel, criteria) {
|
||||||
|
results.Wheels = append(results.Wheels, wheel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if criteria.MinChance > 0 || criteria.MaxChance > 0 {
|
||||||
|
// Chance-based search - use chance ranges
|
||||||
|
minChance := criteria.MinChance
|
||||||
|
maxChance := criteria.MaxChance
|
||||||
|
if maxChance == 0 {
|
||||||
|
maxChance = MaxChance
|
||||||
|
}
|
||||||
|
results.Wheels = ml.GetWheelsByChanceRange(minChance, maxChance)
|
||||||
|
} else {
|
||||||
|
// Search all wheels
|
||||||
|
for _, wheel := range ml.wheels {
|
||||||
|
if ml.matchesWheelCriteria(wheel, criteria) {
|
||||||
|
results.Wheels = append(results.Wheels, wheel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesStarterCriteria checks if starter matches search criteria
|
||||||
|
func (ml *MasterList) matchesStarterCriteria(starter *HeroicOPStarter, criteria HeroicOPSearchCriteria) bool {
|
||||||
|
if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.NamePattern != "" {
|
||||||
|
if !strings.Contains(strings.ToLower(starter.Name), strings.ToLower(criteria.NamePattern)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesWheelCriteria checks if wheel matches search criteria
|
||||||
|
func (ml *MasterList) matchesWheelCriteria(wheel *HeroicOPWheel, criteria HeroicOPSearchCriteria) bool {
|
||||||
|
if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.NamePattern != "" {
|
||||||
|
if !strings.Contains(strings.ToLower(wheel.Name), strings.ToLower(criteria.NamePattern)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.HasShift && !wheel.HasShift() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.IsOrdered && !wheel.IsOrdered() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics with Lazy Caching
|
||||||
|
|
||||||
|
// GetStatistics returns comprehensive statistics with lazy caching for optimal performance
|
||||||
|
func (ml *MasterList) GetStatistics() *HeroicOPStatistics {
|
||||||
|
ml.mu.Lock()
|
||||||
|
defer ml.mu.Unlock()
|
||||||
|
|
||||||
|
ml.ensureMetadataValid()
|
||||||
|
|
||||||
|
return &HeroicOPStatistics{
|
||||||
|
TotalStarters: int64(ml.totalStarters),
|
||||||
|
TotalWheels: int64(ml.totalWheels),
|
||||||
|
ClassDistribution: ml.copyClassDistribution(),
|
||||||
|
OrderedWheelsCount: int64(len(ml.orderedWheels)),
|
||||||
|
ShiftWheelsCount: int64(len(ml.shiftWheels)),
|
||||||
|
SpellCount: int64(len(ml.spellInfo)),
|
||||||
|
AverageChance: ml.calculateAverageChance(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureMetadataValid ensures cached metadata is current
|
||||||
|
func (ml *MasterList) ensureMetadataValid() {
|
||||||
|
if ml.metadataValid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute cached metadata
|
||||||
|
ml.totalStarters = len(ml.starters)
|
||||||
|
ml.totalWheels = len(ml.wheels)
|
||||||
|
|
||||||
|
ml.classDistribution = make(map[int8]int)
|
||||||
|
for _, starter := range ml.starters {
|
||||||
|
ml.classDistribution[starter.StartClass]++
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.metadataValid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyClassDistribution creates a copy of class distribution for thread safety
|
||||||
|
func (ml *MasterList) copyClassDistribution() map[int32]int64 {
|
||||||
|
result := make(map[int32]int64)
|
||||||
|
for class, count := range ml.classDistribution {
|
||||||
|
result[int32(class)] = int64(count)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateAverageChance computes average wheel chance
|
||||||
|
func (ml *MasterList) calculateAverageChance() float64 {
|
||||||
|
if len(ml.wheels) == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
total := float64(0)
|
||||||
|
for _, wheel := range ml.wheels {
|
||||||
|
total += float64(wheel.Chance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total / float64(len(ml.wheels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modification Operations
|
||||||
|
|
||||||
|
// AddStarter adds a starter to the master list with index updates
|
||||||
|
func (ml *MasterList) AddStarter(starter *HeroicOPStarter) error {
|
||||||
|
ml.mu.Lock()
|
||||||
|
defer ml.mu.Unlock()
|
||||||
|
|
||||||
|
if err := starter.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid starter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := ml.starters[starter.ID]; exists {
|
||||||
|
return fmt.Errorf("starter ID %d already exists", starter.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to primary storage
|
||||||
|
ml.starters[starter.ID] = starter
|
||||||
|
|
||||||
|
// Update indices
|
||||||
|
if ml.byClass[starter.StartClass] == nil {
|
||||||
|
ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter)
|
||||||
|
}
|
||||||
|
ml.byClass[starter.StartClass][starter.ID] = starter
|
||||||
|
|
||||||
|
ml.metadataValid = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWheel adds a wheel to the master list with index updates
|
||||||
|
func (ml *MasterList) AddWheel(wheel *HeroicOPWheel) error {
|
||||||
|
ml.mu.Lock()
|
||||||
|
defer ml.mu.Unlock()
|
||||||
|
|
||||||
|
if err := wheel.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid wheel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := ml.wheels[wheel.ID]; exists {
|
||||||
|
return fmt.Errorf("wheel ID %d already exists", wheel.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify starter exists
|
||||||
|
if _, exists := ml.starters[wheel.StarterLinkID]; !exists {
|
||||||
|
return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to primary storage
|
||||||
|
ml.wheels[wheel.ID] = wheel
|
||||||
|
|
||||||
|
// Update indices
|
||||||
|
ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel)
|
||||||
|
ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel)
|
||||||
|
|
||||||
|
chanceRange := ml.getChanceRange(wheel.Chance)
|
||||||
|
ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel)
|
||||||
|
|
||||||
|
if wheel.IsOrdered() {
|
||||||
|
ml.orderedWheels[wheel.ID] = wheel
|
||||||
|
}
|
||||||
|
|
||||||
|
if wheel.HasShift() {
|
||||||
|
ml.shiftWheels[wheel.ID] = wheel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store spell info
|
||||||
|
ml.spellInfo[wheel.SpellID] = SpellInfo{
|
||||||
|
ID: wheel.SpellID,
|
||||||
|
Name: wheel.Name,
|
||||||
|
Description: wheel.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.metadataValid = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Methods
|
||||||
|
|
||||||
|
// IsLoaded returns whether data has been loaded
|
||||||
|
func (ml *MasterList) IsLoaded() bool {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
return ml.loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCount returns total counts with O(1) performance
|
||||||
|
func (ml *MasterList) GetCount() (starters, wheels int) {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
return len(ml.starters), len(ml.wheels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs comprehensive validation of the master list
|
||||||
|
func (ml *MasterList) Validate() []error {
|
||||||
|
ml.mu.RLock()
|
||||||
|
defer ml.mu.RUnlock()
|
||||||
|
|
||||||
|
var errors []error
|
||||||
|
|
||||||
|
// Validate all starters
|
||||||
|
for _, starter := range ml.starters {
|
||||||
|
if err := starter.Validate(); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("starter %d: %w", starter.ID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all wheels
|
||||||
|
for _, wheel := range ml.wheels {
|
||||||
|
if err := wheel.Validate(); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if starter exists for this wheel
|
||||||
|
if _, exists := ml.starters[wheel.StarterLinkID]; !exists {
|
||||||
|
errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for orphaned starters (starters with no wheels)
|
||||||
|
for _, starter := range ml.starters {
|
||||||
|
if wheels := ml.byStarterID[starter.ID]; len(wheels) == 0 {
|
||||||
|
errors = append(errors, fmt.Errorf("starter %d has no associated wheels", starter.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeroicOPSearchResults contains search results
|
||||||
|
type HeroicOPSearchResults struct {
|
||||||
|
Starters []*HeroicOPStarter
|
||||||
|
Wheels []*HeroicOPWheel
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeroicOPStatistics contains extended statistics
|
||||||
|
type HeroicOPStatistics struct {
|
||||||
|
TotalStarters int64 `json:"total_starters"`
|
||||||
|
TotalWheels int64 `json:"total_wheels"`
|
||||||
|
ClassDistribution map[int32]int64 `json:"class_distribution"`
|
||||||
|
OrderedWheelsCount int64 `json:"ordered_wheels_count"`
|
||||||
|
ShiftWheelsCount int64 `json:"shift_wheels_count"`
|
||||||
|
SpellCount int64 `json:"spell_count"`
|
||||||
|
AverageChance float64 `json:"average_chance"`
|
||||||
|
ActiveHOCount int64 `json:"active_ho_count"`
|
||||||
|
}
|
@ -1,607 +0,0 @@
|
|||||||
package heroic_ops
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewMasterHeroicOPList creates a new master heroic opportunity list
|
|
||||||
func NewMasterHeroicOPList() *MasterHeroicOPList {
|
|
||||||
return &MasterHeroicOPList{
|
|
||||||
starters: make(map[int8]map[int32]*HeroicOPStarter),
|
|
||||||
wheels: make(map[int32][]*HeroicOPWheel),
|
|
||||||
spells: make(map[int32]SpellInfo),
|
|
||||||
loaded: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFromDatabase loads all heroic opportunities from the database
|
|
||||||
func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database HeroicOPDatabase) error {
|
|
||||||
mhol.mu.Lock()
|
|
||||||
defer mhol.mu.Unlock()
|
|
||||||
|
|
||||||
// Clear existing data
|
|
||||||
mhol.starters = make(map[int8]map[int32]*HeroicOPStarter)
|
|
||||||
mhol.wheels = make(map[int32][]*HeroicOPWheel)
|
|
||||||
mhol.spells = make(map[int32]SpellInfo)
|
|
||||||
|
|
||||||
// Load starters
|
|
||||||
starterData, err := database.LoadStarters(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load starters: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, data := range starterData {
|
|
||||||
starter := &HeroicOPStarter{
|
|
||||||
ID: data.ID,
|
|
||||||
StartClass: data.StarterClass,
|
|
||||||
StarterIcon: data.StarterIcon,
|
|
||||||
Name: data.Name,
|
|
||||||
Description: data.Description,
|
|
||||||
Abilities: [6]int16{
|
|
||||||
data.Ability1, data.Ability2, data.Ability3,
|
|
||||||
data.Ability4, data.Ability5, data.Ability6,
|
|
||||||
},
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate starter
|
|
||||||
if err := starter.Validate(); err != nil {
|
|
||||||
continue // Skip invalid starters
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to map structure
|
|
||||||
if mhol.starters[starter.StartClass] == nil {
|
|
||||||
mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter)
|
|
||||||
}
|
|
||||||
mhol.starters[starter.StartClass][starter.ID] = starter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load wheels
|
|
||||||
wheelData, err := database.LoadWheels(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load wheels: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, data := range wheelData {
|
|
||||||
wheel := &HeroicOPWheel{
|
|
||||||
ID: data.ID,
|
|
||||||
StarterLinkID: data.StarterLinkID,
|
|
||||||
Order: data.ChainOrder,
|
|
||||||
ShiftIcon: data.ShiftIcon,
|
|
||||||
Chance: data.Chance,
|
|
||||||
SpellID: data.SpellID,
|
|
||||||
Name: data.Name,
|
|
||||||
Description: data.Description,
|
|
||||||
Abilities: [6]int16{
|
|
||||||
data.Ability1, data.Ability2, data.Ability3,
|
|
||||||
data.Ability4, data.Ability5, data.Ability6,
|
|
||||||
},
|
|
||||||
SaveNeeded: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate wheel
|
|
||||||
if err := wheel.Validate(); err != nil {
|
|
||||||
continue // Skip invalid wheels
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to wheels map
|
|
||||||
mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel)
|
|
||||||
|
|
||||||
// Store spell info
|
|
||||||
mhol.spells[wheel.SpellID] = SpellInfo{
|
|
||||||
ID: wheel.SpellID,
|
|
||||||
Name: wheel.Name,
|
|
||||||
Description: wheel.Description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mhol.loaded = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStartersForClass returns all starters that the specified class can initiate
|
|
||||||
func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicOPStarter {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
var starters []*HeroicOPStarter
|
|
||||||
|
|
||||||
// Add class-specific starters
|
|
||||||
if classStarters, exists := mhol.starters[playerClass]; exists {
|
|
||||||
for _, starter := range classStarters {
|
|
||||||
starters = append(starters, starter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add universal starters (class 0 = any)
|
|
||||||
if universalStarters, exists := mhol.starters[ClassAny]; exists {
|
|
||||||
for _, starter := range universalStarters {
|
|
||||||
starters = append(starters, starter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return starters
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStarter returns a specific starter by ID
|
|
||||||
func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
// Search through all classes
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
if starter, exists := classStarters[starterID]; exists {
|
|
||||||
return starter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWheelsForStarter returns all wheels associated with a starter
|
|
||||||
func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
if wheels, exists := mhol.wheels[starterID]; exists {
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
result := make([]*HeroicOPWheel, len(wheels))
|
|
||||||
copy(result, wheels)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWheel returns a specific wheel by ID
|
|
||||||
func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
// Search through all wheel lists
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
for _, wheel := range wheelList {
|
|
||||||
if wheel.ID == wheelID {
|
|
||||||
return wheel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelectRandomWheel randomly selects a wheel from the starter's available wheels
|
|
||||||
func (mhol *MasterHeroicOPList) SelectRandomWheel(starterID int32) *HeroicOPWheel {
|
|
||||||
wheels := mhol.GetWheelsForStarter(starterID)
|
|
||||||
if len(wheels) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return SelectRandomWheel(wheels)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSpellInfo returns spell information for a given spell ID
|
|
||||||
func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
if spell, exists := mhol.spells[spellID]; exists {
|
|
||||||
return &spell, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddStarter adds a new starter to the master list
|
|
||||||
func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error {
|
|
||||||
mhol.mu.Lock()
|
|
||||||
defer mhol.mu.Unlock()
|
|
||||||
|
|
||||||
if err := starter.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("invalid starter: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate ID
|
|
||||||
if existingStarter := mhol.getStarterNoLock(starter.ID); existingStarter != nil {
|
|
||||||
return fmt.Errorf("starter ID %d already exists", starter.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to map structure
|
|
||||||
if mhol.starters[starter.StartClass] == nil {
|
|
||||||
mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter)
|
|
||||||
}
|
|
||||||
mhol.starters[starter.StartClass][starter.ID] = starter
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddWheel adds a new wheel to the master list
|
|
||||||
func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error {
|
|
||||||
mhol.mu.Lock()
|
|
||||||
defer mhol.mu.Unlock()
|
|
||||||
|
|
||||||
if err := wheel.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("invalid wheel: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate ID
|
|
||||||
if existingWheel := mhol.getWheelNoLock(wheel.ID); existingWheel != nil {
|
|
||||||
return fmt.Errorf("wheel ID %d already exists", wheel.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify starter exists
|
|
||||||
if mhol.getStarterNoLock(wheel.StarterLinkID) == nil {
|
|
||||||
return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to wheels map
|
|
||||||
mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel)
|
|
||||||
|
|
||||||
// Store spell info
|
|
||||||
mhol.spells[wheel.SpellID] = SpellInfo{
|
|
||||||
ID: wheel.SpellID,
|
|
||||||
Name: wheel.Name,
|
|
||||||
Description: wheel.Description,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveStarter removes a starter and all its associated wheels
|
|
||||||
func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool {
|
|
||||||
mhol.mu.Lock()
|
|
||||||
defer mhol.mu.Unlock()
|
|
||||||
|
|
||||||
// Find and remove starter
|
|
||||||
found := false
|
|
||||||
for class, classStarters := range mhol.starters {
|
|
||||||
if _, exists := classStarters[starterID]; exists {
|
|
||||||
delete(classStarters, starterID)
|
|
||||||
found = true
|
|
||||||
|
|
||||||
// Clean up empty class map
|
|
||||||
if len(classStarters) == 0 {
|
|
||||||
delete(mhol.starters, class)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove associated wheels
|
|
||||||
delete(mhol.wheels, starterID)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveWheel removes a specific wheel
|
|
||||||
func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool {
|
|
||||||
mhol.mu.Lock()
|
|
||||||
defer mhol.mu.Unlock()
|
|
||||||
|
|
||||||
// Find and remove wheel
|
|
||||||
for starterID, wheelList := range mhol.wheels {
|
|
||||||
for i, wheel := range wheelList {
|
|
||||||
if wheel.ID == wheelID {
|
|
||||||
// Remove wheel from slice
|
|
||||||
mhol.wheels[starterID] = append(wheelList[:i], wheelList[i+1:]...)
|
|
||||||
|
|
||||||
// Clean up empty wheel list
|
|
||||||
if len(mhol.wheels[starterID]) == 0 {
|
|
||||||
delete(mhol.wheels, starterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllStarters returns all starters in the system
|
|
||||||
func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
var allStarters []*HeroicOPStarter
|
|
||||||
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
for _, starter := range classStarters {
|
|
||||||
allStarters = append(allStarters, starter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by ID for consistent ordering
|
|
||||||
sort.Slice(allStarters, func(i, j int) bool {
|
|
||||||
return allStarters[i].ID < allStarters[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return allStarters
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllWheels returns all wheels in the system
|
|
||||||
func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
var allWheels []*HeroicOPWheel
|
|
||||||
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
allWheels = append(allWheels, wheelList...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by ID for consistent ordering
|
|
||||||
sort.Slice(allWheels, func(i, j int) bool {
|
|
||||||
return allWheels[i].ID < allWheels[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return allWheels
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStarterCount returns the total number of starters
|
|
||||||
func (mhol *MasterHeroicOPList) GetStarterCount() int {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
count += len(classStarters)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWheelCount returns the total number of wheels
|
|
||||||
func (mhol *MasterHeroicOPList) GetWheelCount() int {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
count += len(wheelList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLoaded returns whether data has been loaded
|
|
||||||
func (mhol *MasterHeroicOPList) IsLoaded() bool {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
return mhol.loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchStarters searches for starters matching the given criteria
|
|
||||||
func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria) []*HeroicOPStarter {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
var results []*HeroicOPStarter
|
|
||||||
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
for _, starter := range classStarters {
|
|
||||||
if mhol.matchesStarterCriteria(starter, criteria) {
|
|
||||||
results = append(results, starter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort results by ID
|
|
||||||
sort.Slice(results, func(i, j int) bool {
|
|
||||||
return results[i].ID < results[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchWheels searches for wheels matching the given criteria
|
|
||||||
func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []*HeroicOPWheel {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
var results []*HeroicOPWheel
|
|
||||||
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
for _, wheel := range wheelList {
|
|
||||||
if mhol.matchesWheelCriteria(wheel, criteria) {
|
|
||||||
results = append(results, wheel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort results by ID
|
|
||||||
sort.Slice(results, func(i, j int) bool {
|
|
||||||
return results[i].ID < results[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatistics returns usage statistics for the HO system
|
|
||||||
func (mhol *MasterHeroicOPList) GetStatistics() map[string]any {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
stats := make(map[string]any)
|
|
||||||
|
|
||||||
// Basic counts
|
|
||||||
stats["total_starters"] = mhol.getStarterCountNoLock()
|
|
||||||
stats["total_wheels"] = mhol.getWheelCountNoLock()
|
|
||||||
stats["total_spells"] = len(mhol.spells)
|
|
||||||
stats["loaded"] = mhol.loaded
|
|
||||||
|
|
||||||
// Class distribution
|
|
||||||
classDistribution := make(map[string]int)
|
|
||||||
for class, classStarters := range mhol.starters {
|
|
||||||
if className, exists := ClassNames[class]; exists {
|
|
||||||
classDistribution[className] = len(classStarters)
|
|
||||||
} else {
|
|
||||||
classDistribution[fmt.Sprintf("Class_%d", class)] = len(classStarters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats["class_distribution"] = classDistribution
|
|
||||||
|
|
||||||
// Wheel distribution per starter
|
|
||||||
wheelDistribution := make(map[string]int)
|
|
||||||
for starterID, wheelList := range mhol.wheels {
|
|
||||||
wheelDistribution[fmt.Sprintf("starter_%d", starterID)] = len(wheelList)
|
|
||||||
}
|
|
||||||
stats["wheel_distribution"] = wheelDistribution
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks the integrity of the master list
|
|
||||||
func (mhol *MasterHeroicOPList) Validate() []error {
|
|
||||||
mhol.mu.RLock()
|
|
||||||
defer mhol.mu.RUnlock()
|
|
||||||
|
|
||||||
var errors []error
|
|
||||||
|
|
||||||
// Validate all starters
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
for _, starter := range classStarters {
|
|
||||||
if err := starter.Validate(); err != nil {
|
|
||||||
errors = append(errors, fmt.Errorf("starter %d: %w", starter.ID, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all wheels
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
for _, wheel := range wheelList {
|
|
||||||
if err := wheel.Validate(); err != nil {
|
|
||||||
errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if starter exists for this wheel
|
|
||||||
if mhol.getStarterNoLock(wheel.StarterLinkID) == nil {
|
|
||||||
errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for orphaned wheels (starters with no wheels)
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
for starterID := range classStarters {
|
|
||||||
if _, hasWheels := mhol.wheels[starterID]; !hasWheels {
|
|
||||||
errors = append(errors, fmt.Errorf("starter %d has no associated wheels", starterID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal helper methods (no lock versions)
|
|
||||||
|
|
||||||
func (mhol *MasterHeroicOPList) getStarterNoLock(starterID int32) *HeroicOPStarter {
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
if starter, exists := classStarters[starterID]; exists {
|
|
||||||
return starter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mhol *MasterHeroicOPList) getWheelNoLock(wheelID int32) *HeroicOPWheel {
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
for _, wheel := range wheelList {
|
|
||||||
if wheel.ID == wheelID {
|
|
||||||
return wheel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mhol *MasterHeroicOPList) getStarterCountNoLock() int {
|
|
||||||
count := 0
|
|
||||||
for _, classStarters := range mhol.starters {
|
|
||||||
count += len(classStarters)
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mhol *MasterHeroicOPList) getWheelCountNoLock() int {
|
|
||||||
count := 0
|
|
||||||
for _, wheelList := range mhol.wheels {
|
|
||||||
count += len(wheelList)
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mhol *MasterHeroicOPList) matchesStarterCriteria(starter *HeroicOPStarter, criteria HeroicOPSearchCriteria) bool {
|
|
||||||
// Class filter
|
|
||||||
if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name pattern filter
|
|
||||||
if criteria.NamePattern != "" {
|
|
||||||
// Simple case-insensitive substring match
|
|
||||||
// In a real implementation, you might want to use regular expressions
|
|
||||||
if !containsIgnoreCase(starter.Name, criteria.NamePattern) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mhol *MasterHeroicOPList) matchesWheelCriteria(wheel *HeroicOPWheel, criteria HeroicOPSearchCriteria) bool {
|
|
||||||
// Spell ID filter
|
|
||||||
if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chance range filter
|
|
||||||
if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required players filter
|
|
||||||
if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name pattern filter
|
|
||||||
if criteria.NamePattern != "" {
|
|
||||||
if !containsIgnoreCase(wheel.Name, criteria.NamePattern) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift availability filter
|
|
||||||
if criteria.HasShift && !wheel.HasShift() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order type filter
|
|
||||||
if criteria.IsOrdered && !wheel.IsOrdered() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple case-insensitive substring search
|
|
||||||
func containsIgnoreCase(s, substr string) bool {
|
|
||||||
// Convert both strings to lowercase for comparison
|
|
||||||
// In a real implementation, you might want to use strings.ToLower
|
|
||||||
// or a proper Unicode-aware comparison
|
|
||||||
return len(substr) == 0 // Empty substring matches everything
|
|
||||||
// TODO: Implement proper case-insensitive search
|
|
||||||
}
|
|
@ -3,6 +3,8 @@ package heroic_ops
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HeroicOPStarter represents a starter chain for heroic opportunities
|
// HeroicOPStarter represents a starter chain for heroic opportunities
|
||||||
@ -15,6 +17,10 @@ type HeroicOPStarter struct {
|
|||||||
Name string `json:"name"` // Display name for this starter
|
Name string `json:"name"` // Display name for this starter
|
||||||
Description string `json:"description"` // Description text
|
Description string `json:"description"` // Description text
|
||||||
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
||||||
|
|
||||||
|
// Database integration fields
|
||||||
|
db *database.Database `json:"-"` // Database connection
|
||||||
|
isNew bool `json:"-"` // True if this is a new record not yet saved
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeroicOPWheel represents the wheel phase of a heroic opportunity
|
// HeroicOPWheel represents the wheel phase of a heroic opportunity
|
||||||
@ -31,6 +37,10 @@ type HeroicOPWheel struct {
|
|||||||
Description string `json:"description"` // Description text
|
Description string `json:"description"` // Description text
|
||||||
RequiredPlayers int8 `json:"required_players"` // Minimum players required
|
RequiredPlayers int8 `json:"required_players"` // Minimum players required
|
||||||
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
||||||
|
|
||||||
|
// Database integration fields
|
||||||
|
db *database.Database `json:"-"` // Database connection
|
||||||
|
isNew bool `json:"-"` // True if this is a new record not yet saved
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeroicOP represents an active heroic opportunity instance
|
// HeroicOP represents an active heroic opportunity instance
|
||||||
@ -55,6 +65,10 @@ type HeroicOP struct {
|
|||||||
SpellName string `json:"spell_name"` // Name of completion spell
|
SpellName string `json:"spell_name"` // Name of completion spell
|
||||||
SpellDescription string `json:"spell_description"` // Description of completion spell
|
SpellDescription string `json:"spell_description"` // Description of completion spell
|
||||||
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
||||||
|
|
||||||
|
// Database integration fields
|
||||||
|
db *database.Database `json:"-"` // Database connection
|
||||||
|
isNew bool `json:"-"` // True if this is a new record not yet saved
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeroicOPProgress tracks progress during starter chain phase
|
// HeroicOPProgress tracks progress during starter chain phase
|
||||||
@ -85,23 +99,14 @@ type HeroicOPData struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MasterHeroicOPList manages all heroic opportunity configurations
|
|
||||||
type MasterHeroicOPList struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
// Structure: map[class]map[starter_id][]wheel
|
|
||||||
starters map[int8]map[int32]*HeroicOPStarter
|
|
||||||
wheels map[int32][]*HeroicOPWheel // starter_id -> wheels
|
|
||||||
spells map[int32]SpellInfo // spell_id -> spell info
|
|
||||||
loaded bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeroicOPManager manages active heroic opportunity instances
|
// HeroicOPManager manages active heroic opportunity instances
|
||||||
type HeroicOPManager struct {
|
type HeroicOPManager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
activeHOs map[int64]*HeroicOP // instance_id -> HO
|
activeHOs map[int64]*HeroicOP // instance_id -> HO
|
||||||
encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs
|
encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs
|
||||||
masterList *MasterHeroicOPList
|
masterList *MasterList
|
||||||
database HeroicOPDatabase
|
database HeroicOPDatabase
|
||||||
|
db *database.Database // Direct database connection
|
||||||
eventHandler HeroicOPEventHandler
|
eventHandler HeroicOPEventHandler
|
||||||
logger LogHandler
|
logger LogHandler
|
||||||
clientManager ClientManager
|
clientManager ClientManager
|
||||||
@ -123,20 +128,6 @@ type SpellInfo struct {
|
|||||||
Icon int16 `json:"icon"`
|
Icon int16 `json:"icon"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeroicOPStatistics tracks system usage statistics
|
|
||||||
type HeroicOPStatistics struct {
|
|
||||||
TotalHOsStarted int64 `json:"total_hos_started"`
|
|
||||||
TotalHOsCompleted int64 `json:"total_hos_completed"`
|
|
||||||
TotalHOsFailed int64 `json:"total_hos_failed"`
|
|
||||||
TotalHOsTimedOut int64 `json:"total_hos_timed_out"`
|
|
||||||
AverageCompletionTime float64 `json:"average_completion_time"` // seconds
|
|
||||||
MostUsedStarter int32 `json:"most_used_starter"`
|
|
||||||
MostUsedWheel int32 `json:"most_used_wheel"`
|
|
||||||
SuccessRate float64 `json:"success_rate"` // percentage
|
|
||||||
ShiftUsageRate float64 `json:"shift_usage_rate"` // percentage
|
|
||||||
ActiveHOCount int `json:"active_ho_count"`
|
|
||||||
ParticipationStats map[int32]int64 `json:"participation_stats"` // character_id -> HO count
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeroicOPSearchCriteria for searching heroic opportunities
|
// HeroicOPSearchCriteria for searching heroic opportunities
|
||||||
type HeroicOPSearchCriteria struct {
|
type HeroicOPSearchCriteria struct {
|
||||||
|
50
internal/heroic_ops/utils.go
Normal file
50
internal/heroic_ops/utils.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package heroic_ops
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelectRandomWheel selects a random wheel from a list based on chance values
|
||||||
|
func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel {
|
||||||
|
if len(wheels) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wheels) == 1 {
|
||||||
|
return wheels[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total chance
|
||||||
|
totalChance := float32(0.0)
|
||||||
|
for _, wheel := range wheels {
|
||||||
|
totalChance += wheel.Chance
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalChance <= 0.0 {
|
||||||
|
// If no chances set, select randomly with equal probability
|
||||||
|
return wheels[rand.Intn(len(wheels))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random selection based on weighted chance
|
||||||
|
randomValue := rand.Float32() * totalChance
|
||||||
|
currentChance := float32(0.0)
|
||||||
|
|
||||||
|
for _, wheel := range wheels {
|
||||||
|
currentChance += wheel.Chance
|
||||||
|
if randomValue <= currentChance {
|
||||||
|
return wheel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to last wheel (shouldn't happen with proper math)
|
||||||
|
return wheels[len(wheels)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple case-insensitive substring search
|
||||||
|
func containsIgnoreCase(s, substr string) bool {
|
||||||
|
// Convert both strings to lowercase for comparison
|
||||||
|
// In a real implementation, you might want to use strings.ToLower
|
||||||
|
// or a proper Unicode-aware comparison
|
||||||
|
return len(substr) == 0 // Empty substring matches everything
|
||||||
|
// TODO: Implement proper case-insensitive search
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user