fix: auto generate lucky gift tier ids
This commit is contained in:
parent
282c9f1734
commit
3900866df0
@ -20,7 +20,12 @@ import {
|
|||||||
tierRTPContribution,
|
tierRTPContribution,
|
||||||
updateStageTier,
|
updateStageTier,
|
||||||
} from "@/features/lucky-gift/configModel.js";
|
} from "@/features/lucky-gift/configModel.js";
|
||||||
import { broadcastLevelOptions, rewardSourceOptions, stageOptions, optionLabel } from "@/features/lucky-gift/constants.js";
|
import {
|
||||||
|
broadcastLevelOptions,
|
||||||
|
rewardSourceOptions,
|
||||||
|
stageOptions,
|
||||||
|
optionLabel,
|
||||||
|
} from "@/features/lucky-gift/constants.js";
|
||||||
import styles from "@/features/lucky-gift/lucky-gift.module.css";
|
import styles from "@/features/lucky-gift/lucky-gift.module.css";
|
||||||
|
|
||||||
const helpText = {
|
const helpText = {
|
||||||
@ -210,7 +215,12 @@ export function LuckyGiftConfigDrawer({
|
|||||||
<Button disabled={configSaving} type="button" onClick={onClose}>
|
<Button disabled={configSaving} type="button" onClick={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={disabled} startIcon={<SaveOutlined fontSize="small" />} type="submit" variant="primary">
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
startIcon={<SaveOutlined fontSize="small" />}
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -229,7 +239,10 @@ function StageTierEditor({ disabled, form, setForm }) {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.stageStack}>
|
<div className={styles.stageStack}>
|
||||||
{stageOptions.map(([stageKey, stageLabel]) => {
|
{stageOptions.map(([stageKey, stageLabel]) => {
|
||||||
const stage = (form.stages || []).find((item) => item.stage === stageKey) || { stage: stageKey, tiers: [] };
|
const stage = (form.stages || []).find((item) => item.stage === stageKey) || {
|
||||||
|
stage: stageKey,
|
||||||
|
tiers: [],
|
||||||
|
};
|
||||||
const expectedRTP = stageExpectedRTP(stage);
|
const expectedRTP = stageExpectedRTP(stage);
|
||||||
const lowerRTP = Number(form.targetRtpPercent || 0) - Number(form.controlBandPercent || 0);
|
const lowerRTP = Number(form.targetRtpPercent || 0) - Number(form.controlBandPercent || 0);
|
||||||
const upperRTP = Number(form.targetRtpPercent || 0) + Number(form.controlBandPercent || 0);
|
const upperRTP = Number(form.targetRtpPercent || 0) + Number(form.controlBandPercent || 0);
|
||||||
@ -239,7 +252,9 @@ function StageTierEditor({ disabled, form, setForm }) {
|
|||||||
<div className={styles.stageHeader}>
|
<div className={styles.stageHeader}>
|
||||||
<FieldLabel help={helpText.stages} text={stageLabel} />
|
<FieldLabel help={helpText.stages} text={stageLabel} />
|
||||||
<div className={styles.stageMetrics}>
|
<div className={styles.stageMetrics}>
|
||||||
<span className={styles.stageTotal}>概率 {formatPercent(stageProbabilityTotal(stage))}</span>
|
<span className={styles.stageTotal}>
|
||||||
|
概率 {formatPercent(stageProbabilityTotal(stage))}
|
||||||
|
</span>
|
||||||
<span className={rtpInBand ? styles.stageTotal : styles.stageRTPError}>
|
<span className={rtpInBand ? styles.stageTotal : styles.stageRTPError}>
|
||||||
期望 RTP {formatPercent(expectedRTP)}
|
期望 RTP {formatPercent(expectedRTP)}
|
||||||
</span>
|
</span>
|
||||||
@ -261,7 +276,7 @@ function StageTierEditor({ disabled, form, setForm }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateStages(setForm, (stages) =>
|
updateStages(setForm, (stages) =>
|
||||||
correctStageTierProbabilities(stages, stageKey, stageTargetRTP[stageKey])
|
correctStageTierProbabilities(stages, stageKey, stageTargetRTP[stageKey]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -303,16 +318,10 @@ function defaultStageTargetRTP(targetRTPPercent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TierRow({ disabled, setForm, stageKey, stageLabel, tier, tierCount, tierIndex }) {
|
function TierRow({ disabled, setForm, stageKey, stageLabel, tier, tierCount, tierIndex }) {
|
||||||
const updateTier = (patch) => updateStages(setForm, (stages) => updateStageTier(stages, stageKey, tierIndex, patch));
|
const updateTier = (patch) =>
|
||||||
|
updateStages(setForm, (stages) => updateStageTier(stages, stageKey, tierIndex, patch));
|
||||||
return (
|
return (
|
||||||
<div className={styles.tierRow}>
|
<div className={styles.tierRow}>
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
disabled={disabled}
|
|
||||||
label="奖档 ID"
|
|
||||||
value={tier.tierId}
|
|
||||||
onChange={(event) => updateTier({ tierId: event.target.value })}
|
|
||||||
/>
|
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -382,7 +391,13 @@ function TierRow({ disabled, setForm, stageKey, stageLabel, tier, tierCount, tie
|
|||||||
|
|
||||||
function SelectField({ disabled, label, onChange, options, value }) {
|
function SelectField({ disabled, label, onChange, options, value }) {
|
||||||
return (
|
return (
|
||||||
<TextField select disabled={disabled} label={label} value={value} onChange={(event) => onChange(event.target.value)}>
|
<TextField
|
||||||
|
select
|
||||||
|
disabled={disabled}
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
>
|
||||||
{options.map(([optionValue, optionLabelText]) => (
|
{options.map(([optionValue, optionLabelText]) => (
|
||||||
<MenuItem key={optionValue} value={optionValue}>
|
<MenuItem key={optionValue} value={optionValue}>
|
||||||
{optionLabelText || optionLabel(options, optionValue)}
|
{optionLabelText || optionLabel(options, optionValue)}
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export function formFromConfig(config) {
|
|||||||
export function payloadFromLuckyGiftForm(form) {
|
export function payloadFromLuckyGiftForm(form) {
|
||||||
// 第一阶段的新契约明确要求后台只提交百分比和倍数;ppm 是 server 的持久化细节,
|
// 第一阶段的新契约明确要求后台只提交百分比和倍数;ppm 是 server 的持久化细节,
|
||||||
// 这里保留业务单位可以避免再次出现 target_rtp_ppm=95 这类单位错配。
|
// 这里保留业务单位可以避免再次出现 target_rtp_ppm=95 这类单位错配。
|
||||||
|
const stages = assignGeneratedTierIDs(normalizeStages(form.stages));
|
||||||
return {
|
return {
|
||||||
control_band_percent: decimal(form.controlBandPercent),
|
control_band_percent: decimal(form.controlBandPercent),
|
||||||
effective_from_ms: integer(form.effectiveFromMs),
|
effective_from_ms: integer(form.effectiveFromMs),
|
||||||
@ -92,7 +93,7 @@ export function payloadFromLuckyGiftForm(form) {
|
|||||||
pool_id: String(form.poolId || "").trim(),
|
pool_id: String(form.poolId || "").trim(),
|
||||||
pool_rate_percent: decimal(form.poolRatePercent),
|
pool_rate_percent: decimal(form.poolRatePercent),
|
||||||
settlement_window_wager: integer(form.settlementWindowWager),
|
settlement_window_wager: integer(form.settlementWindowWager),
|
||||||
stages: normalizeStages(form.stages).map((stage) => ({
|
stages: stages.map((stage) => ({
|
||||||
stage: stage.stage,
|
stage: stage.stage,
|
||||||
tiers: stage.tiers.map((item) => ({
|
tiers: stage.tiers.map((item) => ({
|
||||||
broadcast_level: item.broadcastLevel,
|
broadcast_level: item.broadcastLevel,
|
||||||
@ -126,23 +127,25 @@ export function previewForForm(form) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addTierToStage(stages, stageKey) {
|
export function addTierToStage(stages, stageKey) {
|
||||||
return stages.map((stage) => {
|
return assignGeneratedTierIDs(
|
||||||
if (stage.stage !== stageKey) {
|
stages.map((stage) => {
|
||||||
return stage;
|
if (stage.stage !== stageKey) {
|
||||||
}
|
return stage;
|
||||||
const nextMultiplier = nextSuggestedTierMultiplier(stage.tiers);
|
}
|
||||||
return {
|
const nextMultiplier = nextSuggestedTierMultiplier(stage.tiers);
|
||||||
...stage,
|
return {
|
||||||
tiers: [
|
...stage,
|
||||||
...stage.tiers,
|
tiers: [
|
||||||
tier(tierIDFromMultiplier(stage.stage, nextMultiplier), nextMultiplier, "", {
|
...stage.tiers,
|
||||||
broadcastLevel: "none",
|
tier(tierIDFromMultiplier(stage.stage, nextMultiplier), nextMultiplier, "", {
|
||||||
highWaterOnly: nextMultiplier >= 5,
|
broadcastLevel: "none",
|
||||||
rewardSource: "base_rtp",
|
highWaterOnly: nextMultiplier >= 5,
|
||||||
}),
|
rewardSource: "base_rtp",
|
||||||
],
|
}),
|
||||||
};
|
],
|
||||||
});
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTierFromStage(stages, stageKey, tierIndex) {
|
export function removeTierFromStage(stages, stageKey, tierIndex) {
|
||||||
@ -175,8 +178,12 @@ export function correctStageTierProbabilities(stages, stageKey, targetRTPPercent
|
|||||||
const enabledTiers = stage.tiers
|
const enabledTiers = stage.tiers
|
||||||
.map((item, index) => ({ index, multiplier: decimal(item.multiplier), tier: item }))
|
.map((item, index) => ({ index, multiplier: decimal(item.multiplier), tier: item }))
|
||||||
.filter((item) => item.tier.enabled !== false);
|
.filter((item) => item.tier.enabled !== false);
|
||||||
const baseTiers = enabledTiers.filter((item) => item.tier.rewardSource === "base_rtp" && Number.isFinite(item.multiplier));
|
const baseTiers = enabledTiers.filter(
|
||||||
const positiveTiers = baseTiers.filter((item) => item.multiplier > 0).sort((left, right) => left.multiplier - right.multiplier);
|
(item) => item.tier.rewardSource === "base_rtp" && Number.isFinite(item.multiplier),
|
||||||
|
);
|
||||||
|
const positiveTiers = baseTiers
|
||||||
|
.filter((item) => item.multiplier > 0)
|
||||||
|
.sort((left, right) => left.multiplier - right.multiplier);
|
||||||
const zeroTiers = baseTiers.filter((item) => item.multiplier === 0);
|
const zeroTiers = baseTiers.filter((item) => item.multiplier === 0);
|
||||||
if (enabledTiers.length === 0 || positiveTiers.length === 0) {
|
if (enabledTiers.length === 0 || positiveTiers.length === 0) {
|
||||||
return stage;
|
return stage;
|
||||||
@ -188,7 +195,7 @@ export function correctStageTierProbabilities(stages, stageKey, targetRTPPercent
|
|||||||
}
|
}
|
||||||
const fixedExpectedRTP = positiveTiers.reduce(
|
const fixedExpectedRTP = positiveTiers.reduce(
|
||||||
(sum, item) => sum + item.multiplier * (probabilityByIndex.get(item.index) || 0),
|
(sum, item) => sum + item.multiplier * (probabilityByIndex.get(item.index) || 0),
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
const remainingProbability = 100 - fixedProbability;
|
const remainingProbability = 100 - fixedProbability;
|
||||||
const targetExtraRTP = targetRTP - fixedExpectedRTP;
|
const targetExtraRTP = targetRTP - fixedExpectedRTP;
|
||||||
@ -231,6 +238,22 @@ export function tierRTPContribution(tier) {
|
|||||||
return expectedContributionPercent(tier.multiplier, tier.probabilityPercent);
|
return expectedContributionPercent(tier.multiplier, tier.probabilityPercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assignGeneratedTierIDs(stages) {
|
||||||
|
return (stages || []).map((stage) => {
|
||||||
|
const used = new Map();
|
||||||
|
const tiers = (stage.tiers || []).map((item) => {
|
||||||
|
const baseID = tierIDFromMultiplier(stage.stage, item.multiplier);
|
||||||
|
const usedCount = used.get(baseID) || 0;
|
||||||
|
used.set(baseID, usedCount + 1);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
tierId: usedCount === 0 ? baseID : `${baseID}_${usedCount + 1}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...stage, tiers };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function defaultStages() {
|
function defaultStages() {
|
||||||
return stageOptions.map(([stage]) => ({ stage, tiers: defaultStageTiers[stage].map((item) => ({ ...item })) }));
|
return stageOptions.map(([stage]) => ({ stage, tiers: defaultStageTiers[stage].map((item) => ({ ...item })) }));
|
||||||
}
|
}
|
||||||
@ -253,7 +276,11 @@ function nextSuggestedTierMultiplier(tiers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tierIDFromMultiplier(stage, multiplier) {
|
function tierIDFromMultiplier(stage, multiplier) {
|
||||||
const multiplierText = String(multiplier).replace(".", "_");
|
const value = decimal(multiplier);
|
||||||
|
if (value <= 0) {
|
||||||
|
return `${stage}_none`;
|
||||||
|
}
|
||||||
|
const multiplierText = formatDecimal(value).replace(".", "_");
|
||||||
return `${stage}_${multiplierText}x`;
|
return `${stage}_${multiplierText}x`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,22 +288,25 @@ function normalizeStages(stages) {
|
|||||||
// server 返回缺阶段时仍用默认三阶段补齐表单,保证运营看到的是完整发布形态;
|
// server 返回缺阶段时仍用默认三阶段补齐表单,保证运营看到的是完整发布形态;
|
||||||
// 保存时 schema 会再次校验概率合计和 0x 档,不让半成品配置进入发布请求。
|
// 保存时 schema 会再次校验概率合计和 0x 档,不让半成品配置进入发布请求。
|
||||||
const byStage = new Map((stages || []).map((stage) => [stage.stage, stage]));
|
const byStage = new Map((stages || []).map((stage) => [stage.stage, stage]));
|
||||||
return stageOptions.map(([stage]) => {
|
return assignGeneratedTierIDs(
|
||||||
const source = byStage.get(stage);
|
stageOptions.map(([stage]) => {
|
||||||
const tiers = Array.isArray(source?.tiers) && source.tiers.length > 0 ? source.tiers : defaultStageTiers[stage];
|
const source = byStage.get(stage);
|
||||||
return {
|
const tiers =
|
||||||
stage,
|
Array.isArray(source?.tiers) && source.tiers.length > 0 ? source.tiers : defaultStageTiers[stage];
|
||||||
tiers: tiers.map((item) => ({
|
return {
|
||||||
broadcastLevel: item.broadcastLevel || "none",
|
stage,
|
||||||
enabled: item.enabled !== false,
|
tiers: tiers.map((item) => ({
|
||||||
highWaterOnly: Boolean(item.highWaterOnly),
|
broadcastLevel: item.broadcastLevel || "none",
|
||||||
multiplier: formatDecimal(item.multiplier),
|
enabled: item.enabled !== false,
|
||||||
probabilityPercent: formatDecimal(item.probabilityPercent),
|
highWaterOnly: Boolean(item.highWaterOnly),
|
||||||
rewardSource: item.rewardSource || "base_rtp",
|
multiplier: formatDecimal(item.multiplier),
|
||||||
tierId: item.tierId || "",
|
probabilityPercent: formatDecimal(item.probabilityPercent),
|
||||||
})),
|
rewardSource: item.rewardSource || "base_rtp",
|
||||||
};
|
tierId: item.tierId || "",
|
||||||
});
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneStages(stages) {
|
function cloneStages(stages) {
|
||||||
@ -285,7 +315,10 @@ function cloneStages(stages) {
|
|||||||
|
|
||||||
function positiveBaselineProbabilities(enabled) {
|
function positiveBaselineProbabilities(enabled) {
|
||||||
const probabilities = new Map();
|
const probabilities = new Map();
|
||||||
const floor = enabled.length * minimumCorrectedProbabilityPercent <= 100 ? minimumCorrectedProbabilityPercent : 100 / enabled.length;
|
const floor =
|
||||||
|
enabled.length * minimumCorrectedProbabilityPercent <= 100
|
||||||
|
? minimumCorrectedProbabilityPercent
|
||||||
|
: 100 / enabled.length;
|
||||||
for (const item of enabled) {
|
for (const item of enabled) {
|
||||||
probabilities.set(item.index, roundPercent(floor));
|
probabilities.set(item.index, roundPercent(floor));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,31 @@ describe("lucky gift config payload", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("regenerates unique tier ids when building submit payload", () => {
|
||||||
|
const form = emptyLuckyGiftForm();
|
||||||
|
const advanced = form.stages.find((stage) => stage.stage === "advanced");
|
||||||
|
advanced.tiers = [
|
||||||
|
...advanced.tiers,
|
||||||
|
{
|
||||||
|
...advanced.tiers[advanced.tiers.length - 1],
|
||||||
|
multiplier: 5,
|
||||||
|
probabilityPercent: 0,
|
||||||
|
tierId: "manual_duplicate",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const payload = payloadFromLuckyGiftForm(form);
|
||||||
|
const advancedPayload = payload.stages.find((stage) => stage.stage === "advanced");
|
||||||
|
|
||||||
|
expect(advancedPayload.tiers.map((tier) => tier.tier_id)).toEqual([
|
||||||
|
"advanced_none",
|
||||||
|
"advanced_1x",
|
||||||
|
"advanced_2x",
|
||||||
|
"advanced_5x",
|
||||||
|
"advanced_5x_2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("corrects a stage probability to requested RTP without zeroing enabled tiers", () => {
|
test("corrects a stage probability to requested RTP without zeroing enabled tiers", () => {
|
||||||
const form = emptyLuckyGiftForm();
|
const form = emptyLuckyGiftForm();
|
||||||
const nextStages = correctStageTierProbabilities(form.stages, "normal", 95);
|
const nextStages = correctStageTierProbabilities(form.stages, "normal", 95);
|
||||||
|
|||||||
@ -19,16 +19,18 @@ describe("lucky gift config schema", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects duplicated tier id inside the same stage before submit", () => {
|
test("does not require manually maintained tier ids before submit", () => {
|
||||||
const form = emptyLuckyGiftForm();
|
const form = emptyLuckyGiftForm();
|
||||||
const advanced = form.stages.find((stage: { stage: string }) => stage.stage === "advanced");
|
const advanced = form.stages.find((stage: { stage: string }) => stage.stage === "advanced");
|
||||||
if (!advanced) {
|
if (!advanced) {
|
||||||
throw new Error("advanced stage missing");
|
throw new Error("advanced stage missing");
|
||||||
}
|
}
|
||||||
|
advanced.tiers[0].tierId = "";
|
||||||
advanced.tiers = [
|
advanced.tiers = [
|
||||||
...advanced.tiers,
|
...advanced.tiers,
|
||||||
{
|
{
|
||||||
...advanced.tiers[advanced.tiers.length - 1],
|
...advanced.tiers[advanced.tiers.length - 1],
|
||||||
|
enabled: false,
|
||||||
multiplier: 10,
|
multiplier: 10,
|
||||||
probabilityPercent: 0,
|
probabilityPercent: 0,
|
||||||
tierId: "advanced_5x",
|
tierId: "advanced_5x",
|
||||||
@ -37,9 +39,6 @@ describe("lucky gift config schema", () => {
|
|||||||
|
|
||||||
const result = luckyGiftConfigFormSchema.safeParse(form);
|
const result = luckyGiftConfigFormSchema.safeParse(form);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(true);
|
||||||
if (!result.success) {
|
|
||||||
expect(result.error.issues.map((issue) => issue.message)).toContain("同一阶段内奖档 ID 不能重复");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const stageTierSchema = z.object({
|
|||||||
multiplier: z.coerce.number().min(0, "倍率不能小于 0"),
|
multiplier: z.coerce.number().min(0, "倍率不能小于 0"),
|
||||||
probabilityPercent: z.coerce.number().min(0, "概率不能小于 0").max(100, "概率不能超过 100%"),
|
probabilityPercent: z.coerce.number().min(0, "概率不能小于 0").max(100, "概率不能超过 100%"),
|
||||||
rewardSource: z.enum(rewardSources),
|
rewardSource: z.enum(rewardSources),
|
||||||
tierId: z.string().trim().min(1, "奖档 ID 不能为空").max(96, "奖档 ID 不能超过 96 个字符"),
|
tierId: z.string().trim().max(96, "奖档 ID 不能超过 96 个字符").optional().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const stageSchema = z.object({
|
const stageSchema = z.object({
|
||||||
@ -31,8 +31,14 @@ export const luckyGiftConfigFormSchema = z
|
|||||||
effectiveFromMs: z.coerce.number().int("生效时间必须是毫秒整数").min(0, "生效时间不能小于 0"),
|
effectiveFromMs: z.coerce.number().int("生效时间必须是毫秒整数").min(0, "生效时间不能小于 0"),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
giftPriceReference: z.coerce.number().int("参考价格必须是整数").gt(0, "参考价格必须大于 0"),
|
giftPriceReference: z.coerce.number().int("参考价格必须是整数").gt(0, "参考价格必须大于 0"),
|
||||||
noviceMaxEquivalentDraws: z.coerce.number().int("新手阶段等价抽数必须是整数").min(0, "新手阶段等价抽数不能小于 0"),
|
noviceMaxEquivalentDraws: z.coerce
|
||||||
normalMaxEquivalentDraws: z.coerce.number().int("正常阶段等价抽数必须是整数").min(0, "正常阶段等价抽数不能小于 0"),
|
.number()
|
||||||
|
.int("新手阶段等价抽数必须是整数")
|
||||||
|
.min(0, "新手阶段等价抽数不能小于 0"),
|
||||||
|
normalMaxEquivalentDraws: z.coerce
|
||||||
|
.number()
|
||||||
|
.int("正常阶段等价抽数必须是整数")
|
||||||
|
.min(0, "正常阶段等价抽数不能小于 0"),
|
||||||
maxSinglePayout: z.coerce.number().int("单次返奖上限必须是整数").gt(0, "单次返奖上限必须大于 0"),
|
maxSinglePayout: z.coerce.number().int("单次返奖上限必须是整数").gt(0, "单次返奖上限必须大于 0"),
|
||||||
userHourlyPayoutCap: z.coerce.number().int("用户小时上限必须是整数").gt(0, "用户小时上限必须大于 0"),
|
userHourlyPayoutCap: z.coerce.number().int("用户小时上限必须是整数").gt(0, "用户小时上限必须大于 0"),
|
||||||
userDailyPayoutCap: z.coerce.number().int("用户每日上限必须是整数").gt(0, "用户每日上限必须大于 0"),
|
userDailyPayoutCap: z.coerce.number().int("用户每日上限必须是整数").gt(0, "用户每日上限必须大于 0"),
|
||||||
@ -93,23 +99,14 @@ export const luckyGiftConfigFormSchema = z
|
|||||||
path: ["stages", stageIndex, "tiers"],
|
path: ["stages", stageIndex, "tiers"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const tierIDSet = new Set<string>();
|
|
||||||
stage.tiers.forEach((tier, tierIndex) => {
|
|
||||||
const tierID = String(tier.tierId || "").trim();
|
|
||||||
if (tierIDSet.has(tierID)) {
|
|
||||||
context.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
message: "同一阶段内奖档 ID 不能重复",
|
|
||||||
path: ["stages", stageIndex, "tiers", tierIndex, "tierId"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
tierIDSet.add(tierID);
|
|
||||||
});
|
|
||||||
const expectedRTPPPM = stage.tiers.reduce((sum, tier) => {
|
const expectedRTPPPM = stage.tiers.reduce((sum, tier) => {
|
||||||
if (!tier.enabled || tier.rewardSource !== "base_rtp") {
|
if (!tier.enabled || tier.rewardSource !== "base_rtp") {
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
return sum + Math.trunc((multiplierToPPM(tier.multiplier) * percentToPPM(tier.probabilityPercent)) / ppmScale);
|
return (
|
||||||
|
sum +
|
||||||
|
Math.trunc((multiplierToPPM(tier.multiplier) * percentToPPM(tier.probabilityPercent)) / ppmScale)
|
||||||
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
const expectedRTP = expectedRTPPPM / 10_000;
|
const expectedRTP = expectedRTPPPM / 10_000;
|
||||||
const lowerRTPPPM = percentToPPM(value.targetRtpPercent - value.controlBandPercent);
|
const lowerRTPPPM = percentToPPM(value.targetRtpPercent - value.controlBandPercent);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user