243 lines
17 KiB
React
243 lines
17 KiB
React
/* eslint-disable */
|
|
// ─────────────────────────────────────────────────────────────
|
|
// searchgg · 클랜 데이터 + 엠블럼 렌더러
|
|
// window.ClanDB(name) → 클랜 상세, window.Emblem, window.EMBLEM_LIB
|
|
// ─────────────────────────────────────────────────────────────
|
|
(function () {
|
|
const r = (n) => Math.floor(Math.random() * n);
|
|
const pick = (a) => a[r(a.length)];
|
|
|
|
// ── 엠블럼 도형 (viewBox 100x100) ──
|
|
const SHAPES = {
|
|
shield: 'M50 6 L88 22 V52 C88 78 70 88 50 96 C30 88 12 78 12 52 V22 Z',
|
|
circle: null, // 원은 <circle>로 별도 처리
|
|
hexagon: 'M50 5 L88 27 V73 L50 95 L12 73 V27 Z',
|
|
diamond: 'M50 5 L95 50 L50 95 L5 50 Z',
|
|
};
|
|
const SHAPE_KEYS = ['shield', 'circle', 'hexagon', 'diamond'];
|
|
|
|
// ── 엠블럼 심볼 (중심 50,50 기준) ──
|
|
const SYM = {
|
|
crosshair: (c) => (<g fill="none" stroke={c} strokeWidth="5" strokeLinecap="round">
|
|
<circle cx="50" cy="50" r="17" /><line x1="50" y1="22" x2="50" y2="34" /><line x1="50" y1="66" x2="50" y2="78" />
|
|
<line x1="22" y1="50" x2="34" y2="50" /><line x1="66" y1="50" x2="78" y2="50" /><circle cx="50" cy="50" r="3" fill={c} stroke="none" /></g>),
|
|
star: (c) => (<path fill={c} d="M50 26 L56.5 43 L74 44 L60 55 L65 72 L50 62 L35 72 L40 55 L26 44 L43.5 43 Z" />),
|
|
skull: (c) => (<g fill={c}><path d="M50 28 C36 28 28 38 28 50 C28 58 32 62 36 65 L36 72 L64 72 L64 65 C68 62 72 58 72 50 C72 38 64 28 50 28 Z" />
|
|
<circle cx="41" cy="50" r="5" fill="#fff" /><circle cx="59" cy="50" r="5" fill="#fff" /><path d="M47 60 L50 56 L53 60 Z" fill="#fff" /></g>),
|
|
wolf: (c) => (<g fill={c}><ellipse cx="50" cy="58" rx="9" ry="11" /><ellipse cx="35" cy="44" rx="5" ry="7" /><ellipse cx="65" cy="44" rx="5" ry="7" /><ellipse cx="42" cy="34" rx="4.5" ry="6" /><ellipse cx="58" cy="34" rx="4.5" ry="6" /></g>),
|
|
sword: (c) => (<g stroke={c} strokeWidth="5" strokeLinecap="round" fill="none"><line x1="30" y1="30" x2="68" y2="68" /><line x1="70" y1="30" x2="32" y2="68" /><line x1="26" y1="40" x2="40" y2="26" /><line x1="60" y1="74" x2="74" y2="60" /></g>),
|
|
crown: (c) => (<path fill={c} d="M28 64 L24 36 L37 48 L50 30 L63 48 L76 36 L72 64 Z" />),
|
|
flame: (c) => (<path fill={c} d="M50 26 C58 38 66 44 66 56 C66 67 58 74 50 74 C42 74 34 67 34 56 C34 50 38 46 42 50 C42 40 46 32 50 26 Z" />),
|
|
bolt: (c) => (<path fill={c} d="M54 24 L34 54 L47 54 L44 76 L66 44 L52 44 Z" />),
|
|
shieldcheck: (c) => (<g><path fill={c} d="M50 26 L70 33 V52 C70 64 61 70 50 75 C39 70 30 64 30 52 V33 Z" /><path d="M42 51 L48 57 L60 43" fill="none" stroke="#fff" strokeWidth="5" strokeLinecap="round" strokeLinejoin="round" /></g>),
|
|
anchor: (c) => (<g fill="none" stroke={c} strokeWidth="5" strokeLinecap="round"><circle cx="50" cy="32" r="5" /><line x1="50" y1="37" x2="50" y2="72" /><line x1="38" y1="48" x2="62" y2="48" /><path d="M32 60 C32 70 42 74 50 74 C58 74 68 70 68 60" /></g>),
|
|
};
|
|
const SYM_KEYS = Object.keys(SYM);
|
|
|
|
const BG_COLORS = ['#48515f', '#2f6fed', '#c0392b', '#1f8a5b', '#8e44ad', '#d98a1f', '#16708a', '#33373d'];
|
|
const SYMBOL_COLORS = ['#ffffff', '#ffe08a', '#ffd1d1', '#bfe3ff', '#c9f0d8'];
|
|
const THEMES = ['#48515f', '#2f6fed', '#c0392b', '#1f8a5b', '#8e44ad', '#b8860b'];
|
|
|
|
function shade(hex, amt) {
|
|
const n = parseInt(hex.slice(1), 16);
|
|
let r0 = (n >> 16) + amt, g0 = ((n >> 8) & 255) + amt, b0 = (n & 255) + amt;
|
|
r0 = Math.max(0, Math.min(255, r0)); g0 = Math.max(0, Math.min(255, g0)); b0 = Math.max(0, Math.min(255, b0));
|
|
return '#' + (r0 * 65536 + g0 * 256 + b0).toString(16).padStart(6, '0');
|
|
}
|
|
|
|
function Emblem({ cfg, size = 88, ring = true }) {
|
|
const { shape = 'shield', bg = '#48515f', symbol = 'crosshair', symbolColor = '#ffffff' } = cfg || {};
|
|
const grad = `eg-${shape}-${bg.slice(1)}-${Math.round(size)}`;
|
|
return (
|
|
<svg width={size} height={size} viewBox="0 0 100 100" style={{ flexShrink: 0, filter: 'drop-shadow(0 2px 4px rgba(0,0,0,.18))' }}>
|
|
<defs>
|
|
<linearGradient id={grad} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor={shade(bg, 26)} /><stop offset="100%" stopColor={shade(bg, -22)} />
|
|
</linearGradient>
|
|
</defs>
|
|
{shape === 'circle'
|
|
? <circle cx="50" cy="50" r="45" fill={`url(#${grad})`} stroke={ring ? '#ffffff' : 'none'} strokeWidth={ring ? 3 : 0} strokeOpacity="0.7" />
|
|
: <path d={SHAPES[shape]} fill={`url(#${grad})`} stroke={ring ? '#ffffff' : 'none'} strokeWidth={ring ? 3 : 0} strokeOpacity="0.7" strokeLinejoin="round" />}
|
|
{SYM[symbol] ? SYM[symbol](symbolColor) : null}
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
// ── 클랜 멤버 생성 ──
|
|
const MEMBER_NICKS = ['불곰대장', '꿀밤한방', '연어초밥', '심야의총잡이', '각잡는곰', '리코일마스터', '플래시박사', '스팀팩중독', '한발의미학', '엄폐중독자', '연막의신', '돌격앞으로', '백발백중', '수비의화신', '클러치메이커', '에임핵아님', '곰발바닥', '겨울잠전문', '북극곰', '꿀단지수호', '재장전중', '헤드샷셰프', '각성한곰', '연승기원', '막내곰', '신입곰돌이'];
|
|
|
|
function genMembers(count, captain) {
|
|
const out = []; const used = new Set();
|
|
// 군 계급(rank) 풀 — 낮은 tier 숫자가 높은 계급
|
|
const ranks = (window.SA && window.SA.allPlayers ? window.SA.allPlayers.map((p) => p.rank).filter(Boolean) : []);
|
|
const uniq = []; const seenT = new Set();
|
|
ranks.forEach((rk) => { if (!seenT.has(rk.tier)) { seenT.add(rk.tier); uniq.push(rk); } });
|
|
uniq.sort((a, b) => a.tier - b.tier); // 높은 계급(작은 tier)부터
|
|
const rankByRole = (role) => {
|
|
if (!uniq.length) return null;
|
|
const top = uniq.length;
|
|
let lo, hi;
|
|
if (role === '클랜장') { lo = 0; hi = Math.max(1, Math.floor(top * 0.18)); }
|
|
else if (role === '운영진') { lo = 0; hi = Math.max(2, Math.floor(top * 0.35)); }
|
|
else if (role === '정예') { lo = Math.floor(top * 0.2); hi = Math.floor(top * 0.6); }
|
|
else { lo = Math.floor(top * 0.4); hi = top; }
|
|
const idx = Math.min(top - 1, lo + r(Math.max(1, hi - lo)));
|
|
return uniq[idx];
|
|
};
|
|
const plan = [];
|
|
plan.push('클랜장');
|
|
for (let i = 0; i < 3 && plan.length < count; i++) plan.push('운영진');
|
|
for (let i = 0; i < 7 && plan.length < count; i++) plan.push('정예');
|
|
while (plan.length < count) plan.push('일반');
|
|
plan.forEach((role, i) => {
|
|
let name;
|
|
if (i === 0 && captain) name = captain;
|
|
else { do { name = pick(MEMBER_NICKS); } while (used.has(name)); }
|
|
used.add(name);
|
|
out.push({
|
|
name, role,
|
|
rank: rankByRole(role),
|
|
contrib: role === '클랜장' ? 1800 + r(900) : role === '운영진' ? 1100 + r(800) : role === '정예' ? 600 + r(700) : 60 + r(560),
|
|
});
|
|
});
|
|
return out.sort((a, b) => b.contrib - a.contrib);
|
|
}
|
|
|
|
// 클랜별 고정 시드(이름 기반)로 엠블럼/테마 결정 → 재방문 시 동일
|
|
function seedFromName(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return h; }
|
|
|
|
const BASE = {
|
|
'불곰부대': { tag: 'BEAR', members: 52, wr: 68, rank: 2, captain: '서든황제김씨', emblem: { shape: 'shield', bg: '#8a5a2b', symbol: 'wolf', symbolColor: '#ffe08a' }, theme: '#8a5a2b',
|
|
slogan: '겨울잠은 시즌 끝나고. 지금은 사냥철.', intro: '러시아워 정규전 위주로 빡겜과 친목을 동시에 챙기는 곰 군단입니다. 매너 좋은 복귀 유저, 성장하고 싶은 뉴비 모두 환영해요.' },
|
|
'서울제일': { tag: 'SEL', members: 48, wr: 71, rank: 1, captain: 'GLOCK_여신', emblem: { shape: 'hexagon', bg: '#2f6fed', symbol: 'crown', symbolColor: '#ffe08a' }, theme: '#2f6fed',
|
|
slogan: '제일이 아니면 의미가 없다.', intro: '시즌2 1위 수성 중인 경쟁 지향 클랜. 정예 위주 스크림, 대회 출전 클랜입니다.' },
|
|
'야간전투': { tag: 'NGT', members: 41, wr: 66, rank: 3, captain: '컨테이너지박령', emblem: { shape: 'circle', bg: '#33373d', symbol: 'bolt', symbolColor: '#bfe3ff' }, theme: '#33373d',
|
|
slogan: '해가 지면, 우리의 시간.', intro: '심야 스크림 전문. 새벽까지 함께 달릴 올빼미 환영.' },
|
|
'한강수비대': { tag: 'HAN', members: 45, wr: 64, rank: 4, captain: '데드폴터줏대', emblem: { shape: 'shield', bg: '#16708a', symbol: 'shieldcheck', symbolColor: '#ffffff' }, theme: '#16708a',
|
|
slogan: '뚫리지 않는다. 우리가 강이니까.', intro: '주말 정모 클랜전 중심의 안정적인 운영. 꾸준한 활동러 환영.' },
|
|
'벚꽃클랜': { tag: 'SAK', members: 38, wr: 62, rank: 5, captain: '오로라공주', emblem: { shape: 'diamond', bg: '#b14d7a', symbol: 'flame', symbolColor: '#ffd1d1' }, theme: '#b14d7a',
|
|
slogan: '피고 지는 건 벚꽃, 지지 않는 건 우리.', intro: '뉴비 멘토링 ◎ 친목과 즐겜 위주의 따뜻한 클랜이에요.' },
|
|
'강철군화': { tag: 'STL', members: 50, wr: 61, rank: 6, captain: '원킬원샷', emblem: { shape: 'hexagon', bg: '#48515f', symbol: 'sword', symbolColor: '#ffffff' }, theme: '#48515f',
|
|
slogan: '매일 21시, 군화 끈을 조여라.', intro: '매일 정기 클랜전 운영. 규율 있는 활동을 선호하는 분께 추천.' },
|
|
};
|
|
|
|
const _cache = {};
|
|
function ClanDB(name) {
|
|
const key = name || '불곰부대';
|
|
if (_cache[key]) return _cache[key];
|
|
const b = BASE[key] || {
|
|
tag: (key.slice(0, 3) || 'CLN').toUpperCase(), members: 30 + r(25), wr: 55 + r(16), rank: 7 + r(40), captain: pick(MEMBER_NICKS),
|
|
emblem: { shape: pick(SHAPE_KEYS), bg: pick(BG_COLORS), symbol: pick(SYM_KEYS), symbolColor: pick(SYMBOL_COLORS) }, theme: pick(THEMES),
|
|
slogan: '함께라서 강하다.', intro: '함께 성장할 클랜원을 찾고 있어요. 분위기 좋은 우리 클랜에 놀러오세요!',
|
|
};
|
|
const seed = seedFromName(key);
|
|
const members = genMembers(Math.min(b.members, 24), b.captain);
|
|
const data = {
|
|
name: key, tag: b.tag, slogan: b.slogan, intro: b.intro,
|
|
emblem: { ...b.emblem }, theme: b.theme,
|
|
level: 12 + (seed % 20), exp: 40 + (seed % 55), expMax: 100,
|
|
expTotal: 80000 + (seed % 90000), expWeek: 1200 + (seed % 4200),
|
|
founded: `${2021 + (seed % 4)}.${String(1 + (seed % 11)).padStart(2, '0')}.${String(1 + (seed % 27)).padStart(2, '0')}`,
|
|
members, memberMax: b.members, totalMembers: b.members,
|
|
weeklyWr: b.wr, rank: b.rank,
|
|
notices: [
|
|
{ pin: true, title: '이번 주 정기전 일정 안내 — 목/토 21시', author: b.captain, time: '2시간 전' },
|
|
{ pin: false, title: '신규 클랜원 환영합니다! 인사 남겨주세요', author: pick(MEMBER_NICKS), time: '1일 전' },
|
|
{ pin: false, title: '클랜 보이스 채널 새로 팠어요 (디스코드)', author: pick(MEMBER_NICKS), time: '2일 전' },
|
|
{ pin: false, title: '클랜전 출전 명단 신청 받습니다 (이번 주)', author: b.captain, time: '3일 전' },
|
|
{ pin: false, title: '주말 내전 결과 정리 및 하이라이트', author: pick(MEMBER_NICKS), time: '4일 전' },
|
|
{ pin: false, title: '장기 미접속 클랜원 정리 예고 안내', author: b.captain, time: '5일 전' },
|
|
{ pin: false, title: '클랜 규칙 일부 개정 — 필독 부탁드려요', author: b.captain, time: '1주 전' },
|
|
],
|
|
feed: [
|
|
{ author: members[0].name, role: members[0].role, text: '오늘 정기전 5승 0패! 다들 폼 미쳤다 🔥', time: '38분 전', likes: 24 },
|
|
{ author: members[2] ? members[2].name : '정예곰', role: '정예', text: '신맵 데드폴 수비 루트 정리해서 공지에 올려둠. 한번씩 보기!', time: '3시간 전', likes: 17 },
|
|
{ author: members[4] ? members[4].name : '막내곰', role: '일반', text: '가입한 지 일주일 됐는데 다들 너무 친절하세요 ㅎㅎ 잘부탁드립니다', time: '6시간 전', likes: 31 },
|
|
{ author: members[1] ? members[1].name : '운영곰', role: members[1] ? members[1].role : '운영진', text: '오늘 저녁 9시 보이스 모여요. 클랜전 전략 회의 있습니다.', time: '9시간 전', likes: 12 },
|
|
{ author: members[3] ? members[3].name : '정예곰2', role: '정예', text: '어제 매치 하이라이트 클립 올렸어요. 마지막 1대3 클러치 꼭 보세요', time: '1일 전', likes: 28 },
|
|
{ author: members[5] ? members[5].name : '일반곰', role: '일반', text: '이번 주 출석 이벤트 다들 참여하셨나요? 마감 임박!', time: '1일 전', likes: 9 },
|
|
{ author: members[0].name, role: members[0].role, text: '지난 주 MVP 투표 결과 공유합니다. 축하해주세요 👏', time: '2일 전', likes: 35 },
|
|
],
|
|
guestbook: [
|
|
{ author: '지나가던행인', text: '클랜 분위기 좋아보여요 👍', time: '1일 전', replies: [{ author: b.captain, text: '좋게 봐주셔서 감사합니다! 언제든 놀러오세요 🙂', time: '1일 전' }] },
|
|
{ author: '스카우터', text: '혹시 정기전 용병 한 자리 가능할까요?', time: '2일 전', replies: [] },
|
|
{ author: '복귀유저A', text: '오랜만에 복귀했는데 가입 문의는 어디로 하면 되나요?', time: '3일 전', replies: [{ author: b.captain, text: '모집 광장의 저희 모집글에서 디스코드로 문의 주세요!', time: '3일 전' }] },
|
|
{ author: '옆클랜원', text: '지난 클랜전 잘 봤습니다. 다음에 또 붙어요!', time: '4일 전', replies: [] },
|
|
{ author: '구경꾼', text: '엠블럼 멋지네요 ㅎㅎ', time: '5일 전', replies: [] },
|
|
{ author: '뉴비방문자', text: '초보도 받아주시나요? 열심히 할 자신 있습니다!', time: '6일 전', replies: [] },
|
|
],
|
|
schedule: [
|
|
{ day: '월', time: '21:00', title: '자유 스크림', on: false },
|
|
{ day: '화', time: '—', title: '휴식', on: false },
|
|
{ day: '수', time: '21:00', title: '내전 데이', on: true },
|
|
{ day: '목', time: '21:00', title: '정기 클랜전', on: true },
|
|
{ day: '금', time: '22:00', title: '친선전', on: false },
|
|
{ day: '토', time: '21:00', title: '정기 클랜전', on: true },
|
|
{ day: '일', time: '20:00', title: '주간 결산', on: true },
|
|
],
|
|
attendance: Array.from({ length: 7 }, (_, i) => 55 + ((seed >> i) % 42)),
|
|
activity: Array.from({ length: 7 }, (_, i) => {
|
|
const s2 = (seed * (i + 3)) % 9973;
|
|
const wk = i >= 5 ? 1.6 : 1; // 주말 가중
|
|
return {
|
|
clanWar: Math.round((3 + (s2 % 8)) * wk),
|
|
clanRanked: Math.round((2 + ((s2 >> 1) % 7)) * wk),
|
|
soloRanked: Math.round((4 + ((s2 >> 2) % 10)) * wk),
|
|
partyRanked: Math.round((3 + ((s2 >> 4) % 8)) * wk),
|
|
};
|
|
}),
|
|
isMine: key === '불곰부대',
|
|
};
|
|
_cache[key] = data;
|
|
return data;
|
|
}
|
|
|
|
window.Emblem = Emblem;
|
|
window.ClanDB = ClanDB;
|
|
window.__SYM_PREVIEW = SYM;
|
|
window.EMBLEM_LIB = { SHAPE_KEYS, SYM_KEYS, BG_COLORS, SYMBOL_COLORS, THEMES };
|
|
|
|
// ── 모집 광장 데이터 ──
|
|
const RECRUIT_NAMES = ['불곰부대', '서울제일', '야간전투', '한강수비대', '벚꽃클랜', '강철군화', '한밤의늑대', '새벽기습대', '데드샷연합', '폭풍전야', '은하수비대', '정예타격대', '코리아게이밍', '불꽃군단', '심야의사냥꾼'];
|
|
const RECRUIT_TYPES = ['클랜전', 'A보급', '3보급', '랭크전', '상관없음'];
|
|
window.RECRUIT_TYPES = RECRUIT_TYPES;
|
|
const _recruits = [];
|
|
function buildRecruits() {
|
|
if (_recruits.length) return _recruits;
|
|
RECRUIT_NAMES.forEach((nm) => {
|
|
const c = ClanDB(nm);
|
|
const seed = seedFromName(nm);
|
|
const max = c.memberMax;
|
|
const cur = Math.max(8, max - (3 + (seed % 12)));
|
|
const openings = max - cur;
|
|
const matches = 120 + (seed % 380);
|
|
const w = Math.round(matches * c.weeklyWr / 100);
|
|
_recruits.push({
|
|
name: nm, emblem: c.emblem, intro: c.intro,
|
|
weeklyWr: c.weeklyWr, rank: c.rank,
|
|
recruitTypes: (() => { const n = 1 + (seed % 3); const out = []; for (let i = 0; i < n; i++) { const t = RECRUIT_TYPES[(seed + i * 2) % RECRUIT_TYPES.length]; if (!out.includes(t)) out.push(t); } return out; })(),
|
|
record: { matches, w, l: matches - w },
|
|
conditions: (() => {
|
|
const ageMin = [null, 20, 25, 30][seed % 4];
|
|
const ageMax = (seed >> 2) % 3 === 0 ? (ageMin ? ageMin + 10 + (seed % 6) : 35) : null;
|
|
return {
|
|
ageMin, ageMax,
|
|
voice: seed % 4 !== 0,
|
|
nickChange: seed % 3 === 0,
|
|
noDualClan: seed % 2 === 0,
|
|
friendJoin: seed % 3 !== 1,
|
|
};
|
|
})(),
|
|
contacts: [
|
|
{ type: '디스코드', label: '디스코드로 문의' },
|
|
...(seed % 3 !== 0 ? [{ type: '오픈카톡', label: '오픈카톡으로 문의' }] : []),
|
|
],
|
|
cur, max, openings,
|
|
hot: 40 + (seed % 220), createdAgo: 1 + (seed % 46),
|
|
});
|
|
});
|
|
return _recruits;
|
|
}
|
|
window.ClanRecruits = buildRecruits;
|
|
})();
|