DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,543 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfdungeon "longheng.io/server/internal/modules/dnf/dungeon"
|
||||
dnfprofession "longheng.io/server/internal/modules/dnf/profession"
|
||||
"longheng.io/server/internal/modules/dnf/progression"
|
||||
dnfquest "longheng.io/server/internal/modules/dnf/quest"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfskill "longheng.io/server/internal/modules/dnf/skill"
|
||||
"longheng.io/server/internal/modules/dnf/skillcmd"
|
||||
)
|
||||
|
||||
const companionProgressionReceiptPrefix = "companion_progression_receipt_"
|
||||
|
||||
// syncCurrentDungeonCompanionProgression mirrors one already committed owner
|
||||
// event to the account's selected support soldiers. The owner is committed
|
||||
// first; each support soldier then commits in its own settlement transaction.
|
||||
// The hashed event receipt makes retries safe without adding a new table.
|
||||
func (s *Service) syncCurrentDungeonCompanionProgression(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
eventKey string,
|
||||
tables *progression.Tables,
|
||||
gain uint32,
|
||||
ownerQuest *dnfrepo.QuestRecord,
|
||||
) {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 ||
|
||||
(strings.TrimSpace(eventKey) == "") || (gain == 0 && ownerQuest == nil) {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
repos, ok := s.repositoryGroup()
|
||||
if !ok || repos.CharacterSettlement == nil || repos.Character == nil || repos.Skill == nil || (gain > 0 && tables == nil) {
|
||||
s.logGameEvent(session, "game-dungeon-companion-progression-skipped",
|
||||
"event_key", eventKey, "reason", "repository_or_progression_unavailable")
|
||||
return
|
||||
}
|
||||
ownerID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
if accountID == "" {
|
||||
if owner, found, loadErr := repos.Character.Load(ctx, ownerID); loadErr == nil && found {
|
||||
accountID = strings.TrimSpace(owner.AccountID)
|
||||
}
|
||||
}
|
||||
cfg, _, _, err := s.loadCurrentDungeonCompanionConfig(ctx, ownerID, accountID, repos)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-progression-skipped", "event_key", eventKey, "reason", "config_unavailable", "error", err)
|
||||
return
|
||||
}
|
||||
if !cfg.Enabled || !cfg.ProgressionEnabled {
|
||||
return
|
||||
}
|
||||
if accountID == "" {
|
||||
return
|
||||
}
|
||||
// Every quest sync already carries the owner's complete committed quest
|
||||
// snapshot. Carry its durable effects too, even when the support soldier's
|
||||
// quest rows were copied by an older version and need no further changes.
|
||||
var ownerCharacter *dnfrepo.CharacterRecord
|
||||
if ownerQuest != nil {
|
||||
owner, found, loadErr := repos.Character.Load(ctx, ownerID)
|
||||
if loadErr != nil || !found || owner.CharacterID != ownerID ||
|
||||
strings.TrimSpace(owner.AccountID) != accountID || ownerQuest.CharacterID != ownerID {
|
||||
s.logGameEvent(session, "game-dungeon-companion-progression-skipped",
|
||||
"event_key", eventKey, "reason", "committed_owner_unavailable_or_mismatched", "error", loadErr)
|
||||
return
|
||||
}
|
||||
ownerCharacter = &owner
|
||||
}
|
||||
profiles, skillCatalog, resourceErr := s.currentProfessionResources(ctx)
|
||||
if resourceErr != nil {
|
||||
// Ordinary synchronization can proceed without automatic learning.
|
||||
// A pending profession change requires these resources and aborts that source's
|
||||
// settlement below so quest state cannot outpace its durable effects.
|
||||
s.logGameEvent(session, "game-dungeon-companion-skill-auto-learn-deferred",
|
||||
"event_key", eventKey, "reason", "skill_pvf_unavailable", "error", resourceErr)
|
||||
profiles, skillCatalog = nil, nil
|
||||
}
|
||||
for _, sourceID := range cfg.Sources() {
|
||||
if sourceID == ownerID {
|
||||
continue
|
||||
}
|
||||
if identity, online := s.onlineGameSessionCharacterSnapshot(parseCurrentCompanionCharacterID(sourceID)); online && identity.session != session {
|
||||
s.logGameEvent(session, "game-dungeon-companion-progression-skipped",
|
||||
"source_character_id", sourceID, "event_key", eventKey, "reason", "source_online")
|
||||
continue
|
||||
}
|
||||
deferred, err := s.applyCurrentDungeonCompanionProgression(
|
||||
ctx, repos, accountID, sourceID, ownerID+":"+eventKey, tables, gain, ownerQuest,
|
||||
ownerCharacter, profiles, skillCatalog,
|
||||
)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-progression-failed",
|
||||
"source_character_id", sourceID, "event_key", eventKey, "gain", gain, "error", err)
|
||||
continue
|
||||
}
|
||||
if deferred {
|
||||
s.logGameEvent(session, "game-dungeon-companion-awakening-deferred",
|
||||
"source_character_id", sourceID, "event_key", eventKey,
|
||||
"reason", "source_profession_does_not_support_owner_awakening")
|
||||
}
|
||||
s.logGameEvent(session, "game-dungeon-companion-progression-applied",
|
||||
"source_character_id", sourceID, "event_key", eventKey, "gain", gain)
|
||||
s.queueCurrentDungeonCompanionProgressionRefresh(session, sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func parseCurrentCompanionCharacterID(raw string) uint16 {
|
||||
value, err := strconv.ParseUint(strings.TrimSpace(raw), 10, 16)
|
||||
if err != nil || value == 0 {
|
||||
return 0
|
||||
}
|
||||
return uint16(value)
|
||||
}
|
||||
|
||||
func companionProgressionReceiptKey(eventKey string) string {
|
||||
hash := sha256.Sum256([]byte(strings.TrimSpace(eventKey)))
|
||||
return fmt.Sprintf("%s%x", companionProgressionReceiptPrefix, hash[:])
|
||||
}
|
||||
|
||||
// currentCompanionMonsterExperienceEventKey 为「打怪经验」这笔结算生成一个
|
||||
// 可重放的稳定事件键。清图回执要等 boss 死才创建,所以这里用**本次副本入场
|
||||
// 的身份**(入场时间戳 + 种子 + 副本 + 层 + object key)当前缀,避免同一副本
|
||||
// 的多次进场共用同一份回执、把第二次的经验镜像误判成已完成。
|
||||
func currentCompanionMonsterExperienceEventKey(runtime *runtimeDungeonState, objectKey uint32) string {
|
||||
if runtime == nil {
|
||||
return fmt.Sprintf("monster:0:0:0:0:%d", objectKey)
|
||||
}
|
||||
return fmt.Sprintf("monster:%d:%d:%d:%d:%d", runtime.startedAt.UnixNano(),
|
||||
runtime.Seed, runtime.Dungeon.ID, runtime.MazeIndex, objectKey)
|
||||
}
|
||||
|
||||
func (s *Service) applyCurrentDungeonCompanionProgression(
|
||||
ctx context.Context,
|
||||
repos dnfrepo.Group,
|
||||
accountID, sourceID, eventKey string,
|
||||
tables *progression.Tables,
|
||||
gain uint32,
|
||||
ownerQuest *dnfrepo.QuestRecord,
|
||||
ownerCharacter *dnfrepo.CharacterRecord,
|
||||
profiles *dnfprofession.Profiles,
|
||||
skillCatalog *dnfskill.Table,
|
||||
) (bool, error) {
|
||||
if repos.CharacterSettlement == nil {
|
||||
return false, dnfrepo.ErrCharacterSettlementTransactionUnavailable
|
||||
}
|
||||
receiptKey := companionProgressionReceiptKey(eventKey)
|
||||
now := time.Now().UTC()
|
||||
var awakeningDeferred bool
|
||||
err := repos.CharacterSettlement.WithinCharacterSettlement(ctx, sourceID, func(tx dnfrepo.Group) error {
|
||||
if tx.Character == nil || tx.Skill == nil {
|
||||
return dnfrepo.ErrCharacterSettlementTransactionUnavailable
|
||||
}
|
||||
character, found, err := tx.Character.Load(ctx, sourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || character.CharacterID != sourceID || strings.TrimSpace(character.AccountID) != accountID {
|
||||
return fmt.Errorf("support soldier character/account mismatch: source=%s", sourceID)
|
||||
}
|
||||
// Receipts suppress EXP only. A replay must still be able to repair
|
||||
// quest effects missed by the previous state-only implementation.
|
||||
applyGain := gain > 0 && character.Stats[receiptKey] != 1
|
||||
skill, found, err := tx.Skill.Load(ctx, sourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || skill.CharacterID != sourceID {
|
||||
return fmt.Errorf("support soldier skill record missing: source=%s", sourceID)
|
||||
}
|
||||
character = dnfrepo.CloneCharacter(character)
|
||||
skill = dnfrepo.CloneSkill(skill)
|
||||
if character.Stats == nil {
|
||||
character.Stats = make(map[string]int64)
|
||||
}
|
||||
// 历史数据自愈:技能记录的 synced_level 可能**超前**于角色等级
|
||||
// (GM 改过等级、克隆角色只复制了一半记录等)。实测取证:角色 3
|
||||
// level=87 但 skill.synced_level=88,导致下面的
|
||||
// PlanSettlementProgressionAtCap 以
|
||||
// "dungeon settlement persisted state is invalid" 拒绝整笔镜像,
|
||||
// 支援兵于是永远停在 grow_type=0(未觉醒)。
|
||||
//
|
||||
// 角色等级是权威,把记账字段拉回角色等级即可;技能本体和 SP/TP 都不动。
|
||||
// 反向(synced_level 落后)不做静默对齐——那会跳过中间等级的 SP 发放,
|
||||
// 宁可报错也不篡改成长账目。
|
||||
if skill.Points.SyncedLevel != character.Level {
|
||||
if skill.Points.SyncedLevel > character.Level {
|
||||
skill.Points.SyncedLevel = character.Level
|
||||
} else {
|
||||
return fmt.Errorf("support soldier skill synced level behind character: source=%s synced=%d level=%d",
|
||||
sourceID, skill.Points.SyncedLevel, character.Level)
|
||||
}
|
||||
}
|
||||
if applyGain {
|
||||
experience, present := character.Stats["exp"]
|
||||
if !present || experience < 0 || character.Level <= 0 {
|
||||
return fmt.Errorf("support soldier progression state invalid: source=%s", sourceID)
|
||||
}
|
||||
if uint64(experience) > ^uint64(0)>>32 {
|
||||
return fmt.Errorf("support soldier experience outside u32: source=%s", sourceID)
|
||||
}
|
||||
planned, planErr := dnfdungeon.PlanSettlementProgressionAtCap(
|
||||
tables, character.Level, uint32(experience), gain,
|
||||
skill.Points, progression.CharacterLevelCap,
|
||||
)
|
||||
if planErr != nil {
|
||||
return planErr
|
||||
}
|
||||
character.Level = planned.Experience.NewLevel
|
||||
character.Stats["exp"] = int64(planned.Experience.NewExperience)
|
||||
skill.Points = planned.SkillPoints.New
|
||||
if planned.Experience.NewLevel != planned.Experience.PreviousLevel {
|
||||
if levelStats, statErr := s.characterPVFStatValuesForLevel(character, planned.Experience.NewLevel); statErr == nil {
|
||||
for key, value := range levelStats {
|
||||
character.Stats[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
effectsChanged, deferred, err := applyCurrentDungeonCompanionQuestEffects(&character, &skill, ownerCharacter, profiles, skillCatalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
awakeningDeferred = deferred
|
||||
if profiles != nil && skillCatalog != nil {
|
||||
if err := autoLearnCurrentDungeonCompanionSkills(character, &skill, profiles, skillCatalog); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if applyGain {
|
||||
character.Stats[receiptKey] = 1
|
||||
}
|
||||
if applyGain || effectsChanged {
|
||||
character.UpdatedAt = now
|
||||
if err := dnfrepo.SaveCharacterFields(ctx, tx.Character, character, dnfrepo.CharacterFieldBase, dnfrepo.CharacterFieldStats); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
skill.UpdatedAt = now
|
||||
if err := dnfrepo.SaveSkillFields(ctx, tx.Skill, skill, dnfrepo.SkillFieldSkills, dnfrepo.SkillFieldPoints, dnfrepo.SkillFieldLayouts); err != nil {
|
||||
return err
|
||||
}
|
||||
if ownerQuest != nil {
|
||||
if tx.Quest == nil {
|
||||
return dnfrepo.ErrCharacterSettlementTransactionUnavailable
|
||||
}
|
||||
if err := mergeCurrentDungeonCompanionQuest(ctx, tx.Quest, sourceID, *ownerQuest, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return awakeningDeferred && err == nil, err
|
||||
}
|
||||
|
||||
// Quest completion and its durable effects share the support soldier's
|
||||
// settlement transaction. Ordinary item/gold rewards remain suppressed.
|
||||
// An unchosen class is selected randomly from the source job's PVF branches,
|
||||
// never copied from the owner. Existing classes are retained, and awakening
|
||||
// advances that same branch. Jobs without class branches keep branch zero.
|
||||
func applyCurrentDungeonCompanionQuestEffects(
|
||||
character *dnfrepo.CharacterRecord,
|
||||
skill *dnfrepo.SkillRecord,
|
||||
owner *dnfrepo.CharacterRecord,
|
||||
profiles *dnfprofession.Profiles,
|
||||
catalog *dnfskill.Table,
|
||||
) (changed, deferred bool, err error) {
|
||||
if owner == nil {
|
||||
return false, false, nil
|
||||
}
|
||||
ownerGrow, sourceGrow := owner.Stats["grow_type"], character.Stats["grow_type"]
|
||||
if ownerGrow < 0 || ownerGrow > 255 || sourceGrow < 0 || sourceGrow > 255 {
|
||||
return false, false, fmt.Errorf("support soldier grow_type invalid: owner=%d source=%d", ownerGrow, sourceGrow)
|
||||
}
|
||||
ownerBranch, ownerAwakening := dnfprofession.Decode(byte(ownerGrow))
|
||||
branch, sourceAwakening := dnfprofession.Decode(byte(sourceGrow))
|
||||
if ownerAwakening > 2 || sourceAwakening > 2 {
|
||||
return false, false, fmt.Errorf("support soldier awakening stage invalid: owner=%d source=%d", ownerAwakening, sourceAwakening)
|
||||
}
|
||||
if character.Stats == nil {
|
||||
character.Stats = make(map[string]int64)
|
||||
}
|
||||
// Retain every existing source bit; only proven owner unlocks are added.
|
||||
beforeSlots := character.Stats["ex_equip_slot_stat"]
|
||||
ownerSlots := owner.Stats["ex_equip_slot_stat"]
|
||||
if ownerSlots < 0 || beforeSlots < 0 {
|
||||
return false, false, fmt.Errorf("support soldier equipment slot state invalid")
|
||||
}
|
||||
if slots := beforeSlots | (ownerSlots & int64(dnfquest.ExEquipSlotAll)); slots != beforeSlots {
|
||||
character.Stats["ex_equip_slot_stat"] = slots
|
||||
changed = true
|
||||
}
|
||||
needsClassChange := sourceGrow == 0 && (ownerBranch != 0 || ownerAwakening != 0)
|
||||
if !needsClassChange && sourceAwakening >= ownerAwakening {
|
||||
return changed, false, nil
|
||||
}
|
||||
if profiles == nil || catalog == nil {
|
||||
return false, false, dnfprofession.ErrSourceRequired
|
||||
}
|
||||
job, parseErr := strconv.ParseUint(strings.TrimSpace(character.Job), 10, 8)
|
||||
if parseErr != nil {
|
||||
return false, false, fmt.Errorf("support soldier job invalid: %w", parseErr)
|
||||
}
|
||||
if needsClassChange {
|
||||
if !profiles.SupportsTarget(byte(job), 0) {
|
||||
return false, false, fmt.Errorf("%w: support soldier job=%d", dnfprofession.ErrProfileMissing, job)
|
||||
}
|
||||
choices := make([]dnfprofession.Transition, 0, 4)
|
||||
for target := byte(1); target <= 0x0f; target++ {
|
||||
transition, planErr := profiles.PlanTransition(byte(job), 0, dnfprofession.Request{
|
||||
Kind: dnfprofession.KindClassChange, ChainType: 1, GrowNumber: target, JobChangeQuest: 1,
|
||||
})
|
||||
if planErr == nil {
|
||||
choices = append(choices, transition)
|
||||
}
|
||||
}
|
||||
if len(choices) != 0 {
|
||||
transition := choices[rand.IntN(len(choices))]
|
||||
updated, skillErr := profiles.ApplySkillTransition(catalog, byte(job), character.Level, transition, *skill, skill.Points)
|
||||
if skillErr != nil {
|
||||
return false, false, skillErr
|
||||
}
|
||||
character.Stats["grow_type"] = int64(transition.NewGrowType)
|
||||
branch = transition.FirstGrowType
|
||||
*skill = updated
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
for stage := sourceAwakening + 1; stage <= ownerAwakening; stage++ {
|
||||
target, _ := dnfprofession.Encode(branch, stage)
|
||||
if !profiles.SupportsTarget(byte(job), target) {
|
||||
return changed, true, nil
|
||||
}
|
||||
request, requestErr := dnfprofession.ParseReward(int(stage)+1, "[awakening type]", []int64{int64(stage)})
|
||||
if requestErr != nil {
|
||||
return false, false, requestErr
|
||||
}
|
||||
transition, planErr := profiles.PlanTransition(byte(job), byte(character.Stats["grow_type"]), request)
|
||||
if planErr != nil {
|
||||
return false, false, planErr
|
||||
}
|
||||
updated, skillErr := profiles.ApplySkillTransition(catalog, byte(job), character.Level, transition, *skill, skill.Points)
|
||||
if skillErr != nil {
|
||||
return false, false, skillErr
|
||||
}
|
||||
character.Stats["grow_type"] = int64(transition.NewGrowType)
|
||||
*skill = updated
|
||||
changed = true
|
||||
}
|
||||
return changed, false, nil
|
||||
}
|
||||
|
||||
// autoLearnCurrentDungeonCompanionSkills keeps PVF profession grants and
|
||||
// fixed-level skills in sync, then spends newly available SP/TP on the first
|
||||
// level of every newly eligible skill in deterministic PVF order. Existing
|
||||
// player allocations are preserved; only an unlearned skill can be added.
|
||||
func autoLearnCurrentDungeonCompanionSkills(
|
||||
character dnfrepo.CharacterRecord,
|
||||
record *dnfrepo.SkillRecord,
|
||||
profiles *dnfprofession.Profiles,
|
||||
catalog *dnfskill.Table,
|
||||
) error {
|
||||
if record == nil || profiles == nil || catalog == nil {
|
||||
return nil
|
||||
}
|
||||
jobValue, err := strconv.ParseUint(strings.TrimSpace(character.Job), 10, 8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
job := byte(jobValue)
|
||||
growType := byte(numericCharacterStatValue(character, "grow_type"))
|
||||
first, awakening := dnfprofession.Decode(growType)
|
||||
if record.Skills == nil {
|
||||
record.Skills = make(map[int64]dnfrepo.SkillState)
|
||||
}
|
||||
if grants, ok := profiles.FreeGrantsAtCharacterLevel(catalog, job, first, awakening, character.Level); ok {
|
||||
for _, grant := range grants {
|
||||
state := record.Skills[int64(grant.SkillID)]
|
||||
if state.Level < grant.Level {
|
||||
state.Level = grant.Level
|
||||
state.Enabled = true
|
||||
record.Skills[int64(grant.SkillID)] = state
|
||||
}
|
||||
}
|
||||
}
|
||||
definitions := catalog.SkillsForJob(job)
|
||||
sort.SliceStable(definitions, func(i, j int) bool {
|
||||
if definitions[i].RequiredLevel != definitions[j].RequiredLevel {
|
||||
return definitions[i].RequiredLevel < definitions[j].RequiredLevel
|
||||
}
|
||||
return definitions[i].ID < definitions[j].ID
|
||||
})
|
||||
for pass := 0; pass < len(definitions); pass++ {
|
||||
changed := false
|
||||
for _, definition := range definitions {
|
||||
if definition.RequiredLevel > character.Level || !definition.SupportsCharacterGrowth(int(growType)) {
|
||||
continue
|
||||
}
|
||||
state := record.Skills[int64(definition.ID)]
|
||||
if state.Level > 0 || skillPrerequisitesBrokenForCompanion(record.Skills, definition) {
|
||||
continue
|
||||
}
|
||||
maximum := definition.MaximumLevelForCharacterGrowth(int(growType))
|
||||
if maximum == 0 {
|
||||
continue
|
||||
}
|
||||
cost := definition.LevelCost(0)
|
||||
if cost < 0 {
|
||||
continue
|
||||
}
|
||||
if definition.IsTPSkill() {
|
||||
if record.Points.RemainingTP < cost {
|
||||
continue
|
||||
}
|
||||
record.Points.RemainingTP -= cost
|
||||
} else {
|
||||
if record.Points.RemainingSP < cost {
|
||||
continue
|
||||
}
|
||||
record.Points.RemainingSP -= cost
|
||||
}
|
||||
record.Skills[int64(definition.ID)] = dnfrepo.SkillState{Level: 1, Enabled: true}
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
}
|
||||
existing := dnfrepo.SkillLayout(nil)
|
||||
if record.Layouts != nil {
|
||||
existing = record.Layouts[0]
|
||||
}
|
||||
layout, err := skillcmd.BuildCurrentSkillLayout(catalog, job, 0, record.Skills, existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record.Layouts == nil {
|
||||
record.Layouts = make(map[int]dnfrepo.SkillLayout)
|
||||
}
|
||||
record.Layouts[0] = layout
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillPrerequisitesBrokenForCompanion(states map[int64]dnfrepo.SkillState, definition dnfskill.Skill) bool {
|
||||
for _, prerequisite := range definition.Prerequisites {
|
||||
if states[int64(prerequisite.SkillID)].Level < prerequisite.Level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeCurrentDungeonCompanionQuest(
|
||||
ctx context.Context,
|
||||
repo dnfrepo.QuestRepository,
|
||||
sourceID string,
|
||||
owner dnfrepo.QuestRecord,
|
||||
now time.Time,
|
||||
) error {
|
||||
current, found, err := repo.Load(ctx, sourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
current = dnfrepo.QuestRecord{CharacterID: sourceID}
|
||||
}
|
||||
current = dnfrepo.CloneQuest(current)
|
||||
changedStates := mergeCompanionQuestStates(¤t.States, owner.States)
|
||||
changedProgress := mergeCompanionQuestStates(¤t.Progress, owner.Progress)
|
||||
if !changedStates && !changedProgress {
|
||||
return nil
|
||||
}
|
||||
current.CharacterID = sourceID
|
||||
current.UpdatedAt = now
|
||||
fields := make([]dnfrepo.QuestField, 0, 2)
|
||||
if changedStates {
|
||||
fields = append(fields, dnfrepo.QuestFieldStates)
|
||||
}
|
||||
if changedProgress {
|
||||
fields = append(fields, dnfrepo.QuestFieldProgress)
|
||||
}
|
||||
return dnfrepo.SaveQuestFields(ctx, repo, current, fields...)
|
||||
}
|
||||
|
||||
func mergeCompanionQuestStates(target *map[int64]dnfrepo.QuestState, source map[int64]dnfrepo.QuestState) bool {
|
||||
if target == nil || len(source) == 0 {
|
||||
return false
|
||||
}
|
||||
if *target == nil {
|
||||
*target = make(map[int64]dnfrepo.QuestState)
|
||||
}
|
||||
changed := false
|
||||
for id, state := range source {
|
||||
if !isCompanionQuestCompletedStatus(state.Status) {
|
||||
continue
|
||||
}
|
||||
previous, exists := (*target)[id]
|
||||
if exists && isCompanionQuestCompletedStatus(previous.Status) {
|
||||
continue
|
||||
}
|
||||
// Copy completion semantics, never the owner's reward receipt or active
|
||||
// objectives. The source receives EXP separately and cannot claim these
|
||||
// rewards again under the owner's character/account receipt.
|
||||
extra := map[string]string{
|
||||
"reward_state": "granted", "auto_completed": "true",
|
||||
"auto_complete_reason": "companion_progression",
|
||||
}
|
||||
for _, key := range []string{"pvf_path", "quest_type", "clear_flag"} {
|
||||
if value := state.Extra[key]; value != "" {
|
||||
extra[key] = value
|
||||
}
|
||||
}
|
||||
state.Extra = extra
|
||||
state.ProgressValue, state.RewardSelectIndex, state.Multiplier = 0, 0, 0
|
||||
(*target)[id] = state
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func isCompanionQuestCompletedStatus(status string) bool {
|
||||
status = strings.ToLower(strings.TrimSpace(status))
|
||||
status = strings.ReplaceAll(status, "_", "")
|
||||
status = strings.ReplaceAll(status, "-", "")
|
||||
status = strings.ReplaceAll(status, " ", "")
|
||||
switch status {
|
||||
case "complete", "completed", "cleared", "finished", "done":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user