Files
90CNServer/server/internal/services/dnfbridge/dungeon_card_gold.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

167 lines
5.8 KiB
Go

package dnfbridge
import (
"context"
)
// 本文件保存工作区原有的"通关卡片金币"计算机制,与 DNF_NEW 的翻牌同步逻辑无关,
// 因此在移植翻牌逻辑时被单独隔离出来保留,避免静默改变地下城与血之祭坛的金币产出。
//
// DNF_NEW 的公式只保留等级参考与难度倍率:
//
// floor(base * 175/1000 * difficultyRate)
//
// 工作区在此基础上补齐了 86JP CClearReward::generateSpecificItem 的其余因子
// (加权击杀数、队伍倍率、通关评价倍率、两段队伍拆分)。以 base=1338、
// 难度 2、40 次加权击杀、单人通关为例,两者分别给出 374 与 5990 金币。
//
// 如需完全对齐 DNF_NEW,删除本文件并改回:
//
// func currentDungeonCardGoldAmount(rng *currentDungeonDropLCG, reference currentDungeonGoldReference, difficulty int) int64
//
// 同时把 dungeon_card_reward_runtime.go 里 currentDungeonCardGoldTerms 参数改回
// difficulty int,并删除 freeze 里的 goldTerms 构造。
// currentDungeonClearKillActorType classifies one accounted death for the
// clear-card gold tally, following 86JP DungeonInstance.RecordKill: actor
// types 3 and 8 are bosses, type 1 is a champion, and everything else -
// including super champions - counts as normal.
func currentDungeonClearKillActorType(monster runtimeDungeonMonster, bossDeath bool, bossActorType byte) byte {
if bossDeath {
if currentDungeonIsBossActorType(bossActorType) {
return bossActorType
}
return 3
}
actorType, err := currentDungeonMonsterActorType(monster.Spawn)
if err != nil {
return 0
}
return actorType
}
func (runtime *runtimeDungeonState) recordCurrentDungeonClearKill(actorType byte) {
if runtime == nil {
return
}
switch {
case currentDungeonIsBossActorType(actorType):
runtime.clearKillBoss++
case actorType == 1:
runtime.clearKillChampion++
default:
runtime.clearKillNormal++
}
}
// currentDungeonClearWeightedKills is 86JP's
// normal + 2*champion + 4*boss weighting of one run's accounted deaths.
func (runtime *runtimeDungeonState) currentDungeonClearWeightedKills() int64 {
if runtime == nil {
return 0
}
return runtime.clearKillNormal + 2*runtime.clearKillChampion + 4*runtime.clearKillBoss
}
// currentDungeonClearRankGoldRate is the additive [clear rank exp bonusrate]
// term the gold formula shares with the settlement's score bonus. The client
// reports its rank point in the op35 play result, which the settlement owner
// already stored before it freezes the card plan; a card frozen earlier - the
// op70 layout path - simply has no rank yet and contributes no bonus.
func (s *Service) currentDungeonClearRankGoldRate(runtime *runtimeDungeonState) float64 {
if s == nil || runtime == nil ||
len(runtime.settlementPlayResultBody) <= currentDungeonPlayResultClientRankPointOffset {
return 0
}
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
defer cancel()
resources, err := s.currentDungeonSettlementRewardResources(ctx)
if err != nil {
return 0
}
clientRankPoint := runtime.settlementPlayResultBody[currentDungeonPlayResultClientRankPointOffset]
rate, ok := currentDungeonClearRankBonusRate(resources, clientRankPoint)
if !ok {
return 0
}
return rate
}
// currentDungeonCardGoldTerms carries everything outside the level reference
// that scales one clear card's gold. Every factor is read from the active PVF
// or counted by the run itself; none of them is a tuning constant.
type currentDungeonCardGoldTerms struct {
Difficulty int
PartyMembers int
PartyRate float64
ClearRankRate float64
WeightedKills int64
}
// currentDungeonCardGoldAmount reproduces CClearReward::generateSpecificItem
// as ported in 86JP ClearRewardGenerator.GenerateGoldCard.
//
// The old implementation kept only the level reference and the difficulty
// rate: `floor(base * 175/1000 * difficultyRate)`. That drops the run's
// weighted kill count, the party rate, the clear-rank rate and the two-stage
// party split, and it is why a level-85 clear paid a few hundred gold instead
// of tens of thousands - reference 1338 * 175/1000 = 234, times at most 2.0,
// is ~470 no matter how much of the dungeon was killed.
//
// goldBase = reference.Base * 175 / 1000
// combined = difficultyRate * partyRate * (1 + clearRankRate)
// intermediate = int64(weightedKills * goldBase / 2 * combined)
// amount = int64(intermediate / 2 / partyMembers * combined)
//
// The two integer truncations are the reference's, not rounding noise: the
// clear factors are applied once on each side of the party split.
func currentDungeonCardGoldAmount(
rng *currentDungeonDropLCG,
reference currentDungeonGoldReference,
terms currentDungeonCardGoldTerms,
) int64 {
if rng == nil || reference.Base <= 0 {
return 1
}
goldBase := reference.Base * 175 / 1000
if goldBase < 1 {
goldBase = 1
}
diffBonus := 1.0
if terms.Difficulty >= 0 && terms.Difficulty < len(currentDungeonCardGoldDifficultyBonus) {
diffBonus = currentDungeonCardGoldDifficultyBonus[terms.Difficulty]
}
partyMembers := terms.PartyMembers
if partyMembers < 1 {
partyMembers = 1
} else if partyMembers > 4 {
partyMembers = 4
}
partyRate := terms.PartyRate
if partyRate <= 0 {
partyRate = 1.0
}
rankRate := 1.0 + terms.ClearRankRate
if rankRate <= 0 {
rankRate = 1.0
}
combined := diffBonus * partyRate * rankRate
weightedKills := terms.WeightedKills
if weightedKills < 0 {
weightedKills = 0
}
intermediate := int64(float64(weightedKills) * float64(goldBase) / 2.0 * combined)
amount := int64(float64(intermediate) / 2.0 / float64(partyMembers) * combined)
if amount < 1 {
amount = 1
}
if reference.VariancePct > 0 {
rangeSize := reference.VariancePct*2 + 1
variance := (int64(rng.Next(int(rangeSize))) - reference.VariancePct) * amount / 100
amount += variance
}
if amount < 1 {
return 1
}
return amount
}