export type SkillConditionMember = { type: string; generation: string; }; export function plainSkillText(value: string): string { return value.replace(/\[[^\]]+\]/g, '').replace(/\s+/g, ' ').trim(); } function normalizedGroup(value: string): string { return value .replace(/^hololive\s+/i, '') .replace(/\(이\)가$/, '') .replace(/\s*타입$/, '') .trim() .toLowerCase(); } function memberBelongsTo(member: SkillConditionMember, group: string): boolean { const normalized = normalizedGroup(group); const type = normalizedGroup(member.type); const generation = normalizedGroup(member.generation); const generations = member.generation .split('/') .map((value) => normalizedGroup(value)) .filter(Boolean); return type === normalized || generation === normalized || generation.endsWith(normalized) || generations.some((value) => value === normalized || value.endsWith(normalized)); } export function skillConditionSatisfied( condition: string, members: readonly SkillConditionMember[] ): boolean { const description = plainSkillText(condition); if (!description) return true; // 콤보와 라이프는 분석에서 최적 플레이를 전제로 항상 충족한다. if (/\d+콤보 이상이면/.test(description) || /라이프 \d+ 이상이면/.test(description)) return true; const deckCondition = description.match(/^(.+?)\s*(\d+)명 이상이면$/); if (!deckCondition) return true; const requiredCount = Number(deckCondition[2]); return members.filter((member) => memberBelongsTo(member, deckCondition[1])).length >= requiredCount; } export function skillTargetBelongsTo(member: SkillConditionMember, group: string): boolean { return memberBelongsTo(member, group); }