feat: complete placement analysis workflow

This commit is contained in:
2026-08-18 14:22:04 +09:00
parent dd3ea911f6
commit 7b713bffe3
452 changed files with 97115 additions and 24 deletions
+51
View File
@@ -0,0 +1,51 @@
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);
}