Merge pull request #60 from ChurchTao/codex/tjc/bugfix
Optimize battle page return button and fix 'Tai Xu' entry bug
This commit is contained in:
25 files changed
+7863
-16
No files matched your search
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "wanjiedaoyou_sect_memberships" ADD COLUMN "lifetime_contribution" integer DEFAULT 0 NOT NULL;
|
||||
UPDATE "wanjiedaoyou_sect_memberships"
|
||||
SET "lifetime_contribution" = "contribution"
|
||||
WHERE "lifetime_contribution" = 0 AND "contribution" > 0;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -225,6 +225,13 @@
|
||||
"when": 1786931826016,
|
||||
"tag": "0031_damp_karnak",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1787307226056,
|
||||
"tag": "0032_rainy_ender_wiggin",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { InkButton } from '@app/components/ui/InkButton';
|
||||
import type { UnitStateSnapshot } from '@shared/engine/battle-v5/systems/state/types';
|
||||
import type {
|
||||
BattlePlaybackRecordV3,
|
||||
PublicBattleUnitSnapshotV1,
|
||||
} from '@shared/types/battle';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { CombatAttributeModal } from '../v5/CombatAttributeModal';
|
||||
import { CombatControlBar } from '../v5/CombatControlBar';
|
||||
import { CombatStatusHeader } from '../v5/CombatStatusHeader';
|
||||
@@ -32,10 +34,25 @@ export function BattlePlaybackPanel({
|
||||
playback,
|
||||
statusActions,
|
||||
}: BattlePlaybackPanelProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!battleResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasExitAction = statusActions?.some((action) =>
|
||||
/返回|离开|进入游戏/.test(action.label),
|
||||
);
|
||||
const resolvedStatusActions: BattleStatusAction[] = hasExitAction
|
||||
? (statusActions ?? [])
|
||||
: [
|
||||
{ label: '返回', onClick: () => navigate(-1) },
|
||||
...(statusActions ?? []),
|
||||
];
|
||||
const hasBattleFrames = Boolean(
|
||||
playback.currentPlayerFrame && playback.currentOpponentFrame,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full min-h-0 flex-col gap-3 md:gap-4">
|
||||
@@ -71,7 +88,7 @@ export function BattlePlaybackPanel({
|
||||
onReset={playback.reset}
|
||||
/>
|
||||
}
|
||||
statusActions={statusActions}
|
||||
statusActions={resolvedStatusActions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -81,6 +98,24 @@ export function BattlePlaybackPanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!hasBattleFrames ? (
|
||||
<div className="battle-dock fixed inset-x-0 bottom-0 z-40 select-none">
|
||||
<div className="mx-auto flex max-w-4xl items-center gap-3 pt-2 pr-[max(env(safe-area-inset-right),0.75rem)] pb-[calc(env(safe-area-inset-bottom)+0.9rem)] pl-[max(env(safe-area-inset-left),0.75rem)] md:pr-[max(env(safe-area-inset-right),1.5rem)] md:pl-[max(env(safe-area-inset-left),1.5rem)]">
|
||||
{resolvedStatusActions.map((action) => (
|
||||
<InkButton
|
||||
key={action.label}
|
||||
variant="ghost"
|
||||
onClick={action.onClick}
|
||||
href={action.href}
|
||||
className="px-0 py-0 text-[13px] leading-5"
|
||||
>
|
||||
{action.label}
|
||||
</InkButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CombatAttributeModal
|
||||
unit={
|
||||
hasExactAttributes(playback.selectedUnit)
|
||||
|
||||
@@ -105,6 +105,14 @@ export function SectIdentityDetails({
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<IdentityRow
|
||||
label="累计贡献"
|
||||
value={
|
||||
<span className="text-wood/80 font-mono">
|
||||
{formatContribution(context.lifetimeContribution)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{showJoinedAt ? (
|
||||
<IdentityRow
|
||||
label="入门时间"
|
||||
|
||||
@@ -190,6 +190,7 @@ function membershipState(
|
||||
joinedAt: context.joinedAt,
|
||||
discipleRank: context.discipleRank,
|
||||
contribution: context.contribution,
|
||||
lifetimeContribution: context.lifetimeContribution,
|
||||
office: context.office,
|
||||
promotedAt: context.promotedAt,
|
||||
configVersion: context.configVersion,
|
||||
|
||||
@@ -86,7 +86,6 @@ export default function BattleReplayPage() {
|
||||
playback={playback}
|
||||
statusActions={[
|
||||
{ label: '分享', onClick: () => setShareOpen(true) },
|
||||
{ label: '返回战绩', href: '/game/battle/history' },
|
||||
]}
|
||||
/>
|
||||
{battleResult ? (
|
||||
|
||||
@@ -230,6 +230,7 @@ export const sectMemberships = pgTable(
|
||||
joinedAt: timestamp('joined_at'),
|
||||
activePathId: varchar('active_path_id', { length: 64 }),
|
||||
contribution: integer('contribution').notNull().default(0),
|
||||
lifetimeContribution: integer('lifetime_contribution').notNull().default(0),
|
||||
discipleRank: varchar('disciple_rank', { length: 16 })
|
||||
.notNull()
|
||||
.default('registered'),
|
||||
|
||||
@@ -96,6 +96,8 @@ export async function addSectContribution(
|
||||
.update(sectMemberships)
|
||||
.set({
|
||||
contribution: sql`${sectMemberships.contribution} + ${amount}`,
|
||||
lifetimeContribution:
|
||||
sql`${sectMemberships.lifetimeContribution} + ${amount}`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(sectMemberships.id, membershipId))
|
||||
|
||||
@@ -197,6 +197,7 @@ async function hydrateMembership(
|
||||
joinedAt: membership.joinedAt?.toISOString(),
|
||||
activePathId: membership.activePathId ?? undefined,
|
||||
contribution: membership.contribution,
|
||||
lifetimeContribution: membership.lifetimeContribution,
|
||||
discipleRank:
|
||||
membership.discipleRank as CultivatorSectState['discipleRank'],
|
||||
office: membership.office as CultivatorSectState['office'],
|
||||
@@ -321,6 +322,8 @@ export async function activateMembership(
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
contribution: definition.onboarding.initialContribution,
|
||||
// Rejoining a sect resets spendable balance, but never erases earned history.
|
||||
lifetimeContribution: sql`GREATEST(${sectMemberships.lifetimeContribution}, ${definition.onboarding.initialContribution})`,
|
||||
activePathId: null,
|
||||
configVersion: definition.configVersion,
|
||||
})
|
||||
|
||||
@@ -270,6 +270,7 @@ function membershipQueryAdapter(
|
||||
cultivatorId: row.cultivatorId,
|
||||
discipleRank: row.discipleRank as SectDiscipleRank,
|
||||
contribution: row.contribution,
|
||||
lifetimeContribution: row.lifetimeContribution,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
@@ -877,6 +878,7 @@ export function createPostgresSectCommandContext(args: {
|
||||
cultivatorId: row.cultivatorId,
|
||||
discipleRank: row.discipleRank as SectDiscipleRank,
|
||||
contribution: row.contribution,
|
||||
lifetimeContribution: row.lifetimeContribution,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
@@ -1051,6 +1053,7 @@ export function createPostgresSectQueryContext(args: {
|
||||
cultivatorId: row.cultivatorId,
|
||||
discipleRank: row.discipleRank as SectDiscipleRank,
|
||||
contribution: row.contribution,
|
||||
lifetimeContribution: row.lifetimeContribution,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ export class SectAdmissionApplicationService {
|
||||
joinedAt: sect.joinedAt,
|
||||
discipleRank,
|
||||
contribution: sect.contribution,
|
||||
lifetimeContribution: sect.lifetimeContribution ?? sect.contribution,
|
||||
office: sect.office ?? 'none',
|
||||
promotedAt: sect.promotedAt,
|
||||
permissions: organization.capabilities.snapshot(discipleRank),
|
||||
|
||||
@@ -97,6 +97,7 @@ export class SectMembershipApplicationService {
|
||||
realm,
|
||||
stage,
|
||||
contribution: membership.contribution,
|
||||
lifetimeContribution: membership.lifetimeContribution,
|
||||
dailyCompletions,
|
||||
completedTaskTags,
|
||||
},
|
||||
@@ -171,6 +172,8 @@ export class SectMembershipApplicationService {
|
||||
nextRank,
|
||||
missing,
|
||||
allowed: Boolean(nextRank && missing.length === 0),
|
||||
contribution: membership.contribution,
|
||||
lifetimeContribution: membership.lifetimeContribution,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,6 +225,8 @@ export class SectMembershipApplicationService {
|
||||
joinedAt: result.joinedAt,
|
||||
discipleRank,
|
||||
contribution: result.contribution,
|
||||
lifetimeContribution:
|
||||
result.lifetimeContribution ?? result.contribution,
|
||||
office: result.office ?? 'none',
|
||||
promotedAt: result.promotedAt,
|
||||
permissions: organization.capabilities.snapshot(discipleRank),
|
||||
|
||||
@@ -142,6 +142,7 @@ export interface SectMembershipRecord {
|
||||
cultivatorId: string;
|
||||
discipleRank: SectDiscipleRank;
|
||||
contribution: number;
|
||||
lifetimeContribution: number;
|
||||
}
|
||||
|
||||
export interface SectTaskRecord {
|
||||
|
||||
@@ -175,6 +175,7 @@ export function createSectsRouter(
|
||||
discipleRank:
|
||||
membership.discipleRank as SectContextData['discipleRank'],
|
||||
contribution: membership.contribution,
|
||||
lifetimeContribution: membership.lifetimeContribution,
|
||||
office: membership.office as SectContextData['office'],
|
||||
promotedAt: membership.promotedAt?.toISOString(),
|
||||
permissions: organization.capabilities.snapshot(
|
||||
|
||||
@@ -148,6 +148,7 @@ export const SECT_RESOURCE_DATA_SCHEMAS = {
|
||||
joinedAt: z.string().optional(),
|
||||
discipleRank: z.enum(['registered', 'outer', 'inner', 'true']),
|
||||
contribution: z.number(),
|
||||
lifetimeContribution: z.number(),
|
||||
office: z.enum(['none', 'steward', 'protector', 'elder']),
|
||||
promotedAt: z.string().optional(),
|
||||
permissions: z.record(
|
||||
|
||||
@@ -272,6 +272,7 @@ export interface SectContextData {
|
||||
joinedAt?: string;
|
||||
discipleRank: SectDiscipleRank;
|
||||
contribution: number;
|
||||
lifetimeContribution: number;
|
||||
office: CultivatorSectState['office'];
|
||||
promotedAt?: string;
|
||||
permissions: Record<string, SectPermissionState>;
|
||||
@@ -303,6 +304,8 @@ export interface SectPromotionEvaluationData {
|
||||
nextRank: SectDiscipleRank | null;
|
||||
missing: string[];
|
||||
allowed: boolean;
|
||||
contribution: number;
|
||||
lifetimeContribution: number;
|
||||
}
|
||||
|
||||
export const SectPromotionEvaluationDataSchema: z.ZodType<SectPromotionEvaluationData> =
|
||||
@@ -311,6 +314,8 @@ export const SectPromotionEvaluationDataSchema: z.ZodType<SectPromotionEvaluatio
|
||||
nextRank: z.enum(['registered', 'outer', 'inner', 'true']).nullable(),
|
||||
missing: z.array(z.string()),
|
||||
allowed: z.boolean(),
|
||||
contribution: z.number().int().nonnegative(),
|
||||
lifetimeContribution: z.number().int().nonnegative(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -563,6 +563,8 @@ export interface PercentDamageModifierParams {
|
||||
mode: 'increase' | 'reduce';
|
||||
value: number;
|
||||
cap?: number;
|
||||
/** 条件满足并实际应用修正时,在战斗日志中显示该触发名称。 */
|
||||
logTriggerName?: string;
|
||||
/** 按产生监听器的 Buff 当前层数放大数值。 */
|
||||
scaleByBuffLayer?: boolean;
|
||||
allowedDamageSources?: DamageSource[];
|
||||
|
||||
@@ -41,11 +41,21 @@ export class PercentDamageModifierEffect extends GameplayEffect {
|
||||
if (this.params.mode === 'increase') {
|
||||
damageRequestEvent.damageIncreasePctBucket =
|
||||
(damageRequestEvent.damageIncreasePctBucket ?? 0) + value;
|
||||
return;
|
||||
} else if (this.params.mode === 'reduce') {
|
||||
damageRequestEvent.damageReductionPctBucket =
|
||||
(damageRequestEvent.damageReductionPctBucket ?? 0) + value;
|
||||
}
|
||||
|
||||
if (this.params.logTriggerName && value > 0) {
|
||||
context.commit(damageRequestEvent.target, {
|
||||
type: 'mechanic',
|
||||
code: 'conditional_damage_modifier_trigger',
|
||||
payload: {
|
||||
kind: 'named_trigger',
|
||||
label: this.params.logTriggerName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AbilityType, AttributeType, BuffType, DamageType } from '../../core/typ
|
||||
import { AbilityFactory } from '../../factories/AbilityFactory';
|
||||
import { BuffFactory } from '../../factories/BuffFactory';
|
||||
import { Unit } from '../../units/Unit';
|
||||
import type { CombatResultCommittedEventV3 } from '../../v3/events';
|
||||
|
||||
describe('免疫效果集成测试', () => {
|
||||
beforeEach(() => {
|
||||
@@ -22,7 +23,11 @@ describe('免疫效果集成测试', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function addPassiveDamageListener(unit: Unit, effects: Array<EffectConfig>): void {
|
||||
function addPassiveDamageListener(
|
||||
unit: Unit,
|
||||
effects: Array<EffectConfig>,
|
||||
eventType: 'DamageEvent' | 'DamageRequestEvent' = 'DamageEvent',
|
||||
): void {
|
||||
unit.abilities.addAbility(
|
||||
AbilityFactory.create({
|
||||
slug: `passive_${effects.map((effect) => effect.type).join('_')}`,
|
||||
@@ -31,7 +36,7 @@ describe('免疫效果集成测试', () => {
|
||||
tags: [GameplayTags.ABILITY.KIND.PASSIVE],
|
||||
listeners: [
|
||||
{
|
||||
eventType: 'DamageEvent',
|
||||
eventType,
|
||||
scope: 'owner_as_target',
|
||||
priority: EventPriorityLevel.DAMAGE_APPLY,
|
||||
effects,
|
||||
@@ -41,6 +46,54 @@ describe('免疫效果集成测试', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('条件减伤实际生效时应提交带词条名称的战斗日志事实', () => {
|
||||
const attacker = createUnit('attacker', '进攻者');
|
||||
const defender = createUnit('defender', '防御者');
|
||||
addPassiveDamageListener(
|
||||
defender,
|
||||
[
|
||||
{
|
||||
type: 'percent_damage_modifier',
|
||||
params: { mode: 'reduce', value: 0.5, logTriggerName: '金甲' },
|
||||
},
|
||||
],
|
||||
'DamageRequestEvent',
|
||||
);
|
||||
|
||||
const committed: CombatResultCommittedEventV3[] = [];
|
||||
const handler = (event: CombatResultCommittedEventV3) => {
|
||||
committed.push(event);
|
||||
};
|
||||
EventBus.instance.subscribe<CombatResultCommittedEventV3>(
|
||||
'CombatResultCommittedEventV3',
|
||||
handler,
|
||||
EventPriorityLevel.COMBAT_LOG,
|
||||
);
|
||||
|
||||
const damageRequest = {
|
||||
type: 'DamageRequestEvent' as const,
|
||||
timestamp: Date.now(),
|
||||
caster: attacker,
|
||||
target: defender,
|
||||
damageType: DamageType.PHYSICAL,
|
||||
baseDamage: 100,
|
||||
finalDamage: 100,
|
||||
};
|
||||
EventBus.instance.publish(damageRequest);
|
||||
|
||||
expect(damageRequest.damageReductionPctBucket).toBe(0.5);
|
||||
expect(committed.map((event) => event.result)).toContainEqual({
|
||||
type: 'mechanic',
|
||||
code: 'conditional_damage_modifier_trigger',
|
||||
payload: { kind: 'named_trigger', label: '金甲' },
|
||||
});
|
||||
|
||||
EventBus.instance.unsubscribe<CombatResultCommittedEventV3>(
|
||||
'CombatResultCommittedEventV3',
|
||||
handler,
|
||||
);
|
||||
});
|
||||
|
||||
it('魔法盾应消耗法力并吸收 98% 伤害', () => {
|
||||
const attacker = createUnit('attacker', '进攻者');
|
||||
const defender = createUnit('defender', '护盾者');
|
||||
|
||||
@@ -26,19 +26,41 @@ export class AffixEffectTranslator {
|
||||
* 翻译词缀定义 + 品质 → 具体 EffectConfig
|
||||
*/
|
||||
translate(affix: RolledAffix, quality: Quality): EffectConfig {
|
||||
const resolved = this.withDeathPreventTriggerKey(
|
||||
this.resolveTemplate(
|
||||
affix.effectTemplate,
|
||||
QUALITY_ORDER[quality],
|
||||
affix.finalMultiplier,
|
||||
),
|
||||
affix.id,
|
||||
);
|
||||
return this.withConditions(
|
||||
affix,
|
||||
this.withDeathPreventTriggerKey(
|
||||
this.resolveTemplate(
|
||||
affix.effectTemplate,
|
||||
QUALITY_ORDER[quality],
|
||||
affix.finalMultiplier,
|
||||
),
|
||||
affix.id,
|
||||
),
|
||||
this.withConditionalTriggerName(affix, resolved),
|
||||
);
|
||||
}
|
||||
|
||||
private withConditionalTriggerName(
|
||||
affix: RolledAffix,
|
||||
effect: EffectConfig,
|
||||
): EffectConfig {
|
||||
if (
|
||||
effect.type !== 'percent_damage_modifier' ||
|
||||
!affix.effectTemplate.conditions?.some(
|
||||
(condition) => condition.type === 'chance',
|
||||
)
|
||||
) {
|
||||
return effect;
|
||||
}
|
||||
return {
|
||||
...effect,
|
||||
params: {
|
||||
...effect.params,
|
||||
logTriggerName: affix.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private withConditions(def: RolledAffix, effect: EffectConfig): EffectConfig {
|
||||
const conditions = def.effectTemplate.conditions;
|
||||
const globalUnique = def.globalUnique;
|
||||
|
||||
@@ -73,6 +73,7 @@ describe('creation-v2 affix match contract', () => {
|
||||
params: { tag: GameplayTags.ABILITY.FUNCTION.CONTROL },
|
||||
},
|
||||
]);
|
||||
expect(result.params).not.toHaveProperty('logTriggerName');
|
||||
});
|
||||
|
||||
it('多 debuff 协同应显式声明 debuff_count_at_least 条件', () => {
|
||||
@@ -108,6 +109,33 @@ describe('creation-v2 affix match contract', () => {
|
||||
params: { value: 2 },
|
||||
},
|
||||
]);
|
||||
expect(result.params).not.toHaveProperty('logTriggerName');
|
||||
});
|
||||
|
||||
it('概率条件词条应携带触发日志名称,确定性条件不应刷屏', () => {
|
||||
const rolledAffix: RolledAffix = {
|
||||
id: 'test-probability-trigger',
|
||||
name: '概率护体',
|
||||
slot: 'modifier',
|
||||
rarity: 'rare',
|
||||
energyCost: 10,
|
||||
rollScore: 1,
|
||||
rollEfficiency: 1,
|
||||
finalMultiplier: 1,
|
||||
isPerfect: false,
|
||||
weight: 10,
|
||||
match: {},
|
||||
tags: [],
|
||||
effectTemplate: {
|
||||
type: 'percent_damage_modifier',
|
||||
conditions: [{ type: 'chance', params: { value: 0.2 } }],
|
||||
params: { mode: 'reduce', value: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
expect(translator.translate(rolledAffix, '玄品').params).toMatchObject({
|
||||
logTriggerName: '概率护体',
|
||||
});
|
||||
});
|
||||
|
||||
it('递归 effect 模板应解析为 battle-v5 EffectConfig', () => {
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface CultivatorSectState {
|
||||
joinedAt?: string;
|
||||
activePathId?: SectPathId;
|
||||
contribution: number;
|
||||
/** Total contribution earned; unlike contribution, this is not reduced by spending. */
|
||||
lifetimeContribution?: number;
|
||||
discipleRank?: SectDiscipleRank;
|
||||
office?: SectOffice;
|
||||
promotedAt?: string;
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('PromotionRequirementSpecification', () => {
|
||||
realm: '筑基',
|
||||
stage: '初期',
|
||||
contribution: 500,
|
||||
lifetimeContribution: 500,
|
||||
dailyCompletions: 3,
|
||||
completedTaskTags: new Set(['fixture.trial']),
|
||||
},
|
||||
@@ -30,6 +31,7 @@ describe('PromotionRequirementSpecification', () => {
|
||||
realm: '炼气',
|
||||
stage: '初期',
|
||||
contribution: 10,
|
||||
lifetimeContribution: 10,
|
||||
dailyCompletions: 0,
|
||||
completedTaskTags: new Set(),
|
||||
},
|
||||
@@ -43,4 +45,27 @@ describe('PromotionRequirementSpecification', () => {
|
||||
'task:fixture.trial',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses lifetime contribution after the spendable balance is consumed', () => {
|
||||
const specification = new PromotionRequirementSpecification();
|
||||
expect(
|
||||
specification.violations(
|
||||
{
|
||||
realm: '筑基',
|
||||
stage: '初期',
|
||||
contribution: 0,
|
||||
lifetimeContribution: 500,
|
||||
dailyCompletions: 3,
|
||||
completedTaskTags: new Set(['fixture.trial']),
|
||||
},
|
||||
{
|
||||
rank: 'inner',
|
||||
minRealm: '筑基',
|
||||
contribution: 500,
|
||||
dailyCompletions: 3,
|
||||
requiredTaskTags: [{ tag: 'fixture.trial', label: '完成夹具试炼' }],
|
||||
},
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ export interface PromotionCandidateFacts {
|
||||
realm: RealmType;
|
||||
stage: RealmStage;
|
||||
contribution: number;
|
||||
lifetimeContribution?: number;
|
||||
dailyCompletions: number;
|
||||
completedTaskTags: ReadonlySet<string>;
|
||||
}
|
||||
@@ -45,10 +46,12 @@ export class PromotionRequirementSpecification {
|
||||
code: 'realm',
|
||||
message: `境界达到${requirement.minRealm}`,
|
||||
});
|
||||
if (candidate.contribution < requirement.contribution)
|
||||
const lifetimeContribution =
|
||||
candidate.lifetimeContribution ?? candidate.contribution;
|
||||
if (lifetimeContribution < requirement.contribution)
|
||||
violations.push({
|
||||
code: 'contribution',
|
||||
message: `当前贡献达到${requirement.contribution}`,
|
||||
message: `累计贡献达到${requirement.contribution}`,
|
||||
});
|
||||
if (
|
||||
requirement.dailyCompletions &&
|
||||
|
||||
Reference in new issue
Block a user