feat(guild): implement rookie and return guild assignment

New/returning players are now auto-assigned to temporary holding guilds
on MSG_MHF_ENTRY_ROOKIE_GUILD (pkt.Unk=0 → rookie guild, ≥1 → comeback
guild). Guilds are created on demand and capped at 60 members. Players
leave via the OperateGuildGraduateRookie/Return actions. The guild info
response now reports isReturnGuild from the DB instead of hardcoded false.
Migration 0014_return_guilds adds return_type to the guilds table.
This commit is contained in:
Houmgaor
2026-03-22 00:27:05 +01:00
parent 5fe1b22550
commit 5ee9a0e635
11 changed files with 150 additions and 20 deletions

View File

@@ -35,6 +35,7 @@ SELECT
leader_id,
c.name AS leader_name,
comment,
return_type,
COALESCE(pugi_name_1, '') AS pugi_name_1,
COALESCE(pugi_name_2, '') AS pugi_name_2,
COALESCE(pugi_name_3, '') AS pugi_name_3,
@@ -196,6 +197,62 @@ func (r *GuildRepository) Create(leaderCharID uint32, guildName string) (int32,
return guildID, nil
}
// FindOrCreateReturnGuild finds an existing return guild of the given type with fewer
// than 60 members, or creates a new one. The name template receives the guild count+1
// as its single %d argument. Returns the guild ID.
func (r *GuildRepository) FindOrCreateReturnGuild(returnType uint8, nameTemplate string) (uint32, error) {
var guildID uint32
err := r.db.QueryRow(`
SELECT g.id FROM guilds g
WHERE g.return_type = $1
AND (SELECT COUNT(1) FROM guild_characters gc WHERE gc.guild_id = g.id) < 60
LIMIT 1
`, returnType).Scan(&guildID)
if err == nil {
return guildID, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return 0, err
}
// No suitable guild — count existing ones and create a new one.
var count int
if err := r.db.QueryRow(
`SELECT COUNT(1) FROM guilds WHERE return_type = $1`, returnType,
).Scan(&count); err != nil {
return 0, err
}
tx, err := r.db.BeginTxx(context.Background(), nil)
if err != nil {
return 0, err
}
defer func() { _ = tx.Rollback() }()
name := fmt.Sprintf(nameTemplate, count+1)
if err := tx.QueryRow(
`INSERT INTO guilds (name, leader_id, return_type, rank_rp) VALUES ($1, 0, $2, 1200) RETURNING id`,
name, returnType,
).Scan(&guildID); err != nil {
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, err
}
return guildID, nil
}
// AddMember inserts a character into a guild's member list.
func (r *GuildRepository) AddMember(guildID, charID uint32) error {
_, err := r.db.Exec(`
INSERT INTO guild_characters (guild_id, character_id, order_index)
VALUES ($1, $2, (SELECT COALESCE(MAX(order_index), 0) + 1 FROM guild_characters WHERE guild_id = $1))
ON CONFLICT (guild_id, character_id) DO NOTHING
`, guildID, charID)
return err
}
// Save persists guild metadata changes.
func (r *GuildRepository) Save(guild *Guild) error {
_, err := r.db.Exec(`