import { z } from "zod"; const rewardTypes = ["coin", "gift", "prop", "dress"] as const; const propRewardTypes = new Set(["prop", "dress"]); const wheelTierCounts: Record = { advanced: 12, classic: 9, luxury: 8, }; const wheelTierSchema = z.object({ displayName: z.string().trim().min(1, "奖品名称不能为空").max(80, "奖品名称不能超过 80 个字符"), enabled: z.boolean(), metadataJson: z.string().trim().default("{}"), probabilityPercent: z.coerce.number().min(0, "概率不能小于 0").max(100, "概率不能超过 100%"), rewardCoins: z.coerce.number().int("金币数量必须是整数").min(0, "金币数量不能小于 0"), rewardCount: z.coerce.number().int("奖励数量必须是整数").min(0, "奖励数量不能小于 0"), rewardId: z.string().trim().default(""), rewardType: z.enum(rewardTypes), rtpValueCoins: z.coerce.number().int("RTP 价值必须是整数").min(0, "RTP 价值不能小于 0"), tierId: z.string().trim().min(1, "档位 ID 不能为空").max(64, "档位 ID 不能超过 64 个字符"), }); export const wheelConfigFormSchema = z .object({ drawPriceCoins: z.coerce.number().int("单抽价格必须是整数").gt(0, "单抽价格必须大于 0"), effectiveFromMs: z.coerce.number().int("生效时间必须是毫秒整数").min(0, "生效时间不能小于 0"), enabled: z.boolean(), initialPoolCoins: z.coerce.number().int("初始奖池必须是整数").min(0, "初始奖池不能小于 0"), maxSingleRTPPayout: z.coerce.number().int("单次 RTP 上限必须是整数").min(0, "单次 RTP 上限不能小于 0"), poolRatePercent: z.coerce.number().min(0, "入池比例不能小于 0").max(100, "入池比例不能超过 100%"), poolReserveCoins: z.coerce.number().int("奖池保底必须是整数").min(0, "奖池保底不能小于 0"), settlementWindowDraws: z.coerce.number().int("结算窗口必须是整数").gt(0, "结算窗口必须大于 0"), targetRTPPercent: z.coerce.number().min(0, "RTP 不能小于 0").max(100, "RTP 不能超过 100%"), tiers: z.array(wheelTierSchema).min(1, "必须配置奖品档位"), wheelId: z.string().trim().min(1, "转盘 ID 不能为空").max(64, "转盘 ID 不能超过 64 个字符"), }) .superRefine((value, context) => { const expectedCount = wheelTierCounts[value.wheelId] || wheelTierCounts.classic; if (value.tiers.length !== expectedCount) { context.addIssue({ code: "custom", message: `${value.wheelId} 转盘必须配置 ${expectedCount} 个奖品档位`, path: ["tiers"], }); } const totalPPM = value.tiers.reduce((sum, tier) => sum + Math.round(tier.probabilityPercent * 10_000), 0); if (totalPPM !== 1_000_000) { context.addIssue({ code: "custom", message: "奖品档位概率合计必须等于 100%", path: ["tiers"], }); } value.tiers.forEach((tier, index) => { if (tier.enabled && tier.probabilityPercent <= 0) { context.addIssue({ code: "custom", message: "启用奖品的概率必须大于 0", path: ["tiers", index, "probabilityPercent"], }); } if (tier.rewardType === "coin" && tier.rewardCoins <= 0) { context.addIssue({ code: "custom", message: "金币档 reward_coins 必须大于 0", path: ["tiers", index, "rewardCoins"], }); } if (tier.rewardType === "gift" && !tier.rewardId) { context.addIssue({ code: "custom", message: "礼物档 reward_id 不能为空", path: ["tiers", index, "rewardId"], }); } if (propRewardTypes.has(tier.rewardType) && !tier.rewardId) { context.addIssue({ code: "custom", message: "道具或装扮档 reward_id 不能为空", path: ["tiers", index, "rewardId"], }); } if (propRewardTypes.has(tier.rewardType) && tier.rtpValueCoins !== 0) { context.addIssue({ code: "custom", message: "道具或装扮不计入 RTP,RTP 价值必须为 0", path: ["tiers", index, "rtpValueCoins"], }); } if (tier.metadataJson) { try { JSON.parse(tier.metadataJson); } catch { context.addIssue({ code: "custom", message: "扩展 JSON 格式不正确", path: ["tiers", index, "metadataJson"], }); } } }); }); export type WheelConfigForm = z.infer;