feat: port scenario 903 select-pool flow

This commit is contained in:
2026-07-30 23:27:59 +00:00
parent 9bd057456b
commit 115218ded8
80 changed files with 6859 additions and 48 deletions
+1
View File
@@ -34,6 +34,7 @@
"@fastify/cors": "^11.2.0",
"@fastify/static": "^9.0.0",
"@sammo-ts/common": "workspace:*",
"@sammo-ts/game-engine": "workspace:*",
"@sammo-ts/infra": "workspace:*",
"@sammo-ts/logic": "workspace:*",
"@trpc/server": "^11.8.1",
+3 -1
View File
@@ -41,7 +41,9 @@ const parsePayload = (value: unknown): GameSessionTokenPayload | null => {
typeof user.id !== 'string' ||
typeof user.username !== 'string' ||
typeof user.displayName !== 'string' ||
!Array.isArray(user.roles)
!Array.isArray(user.roles) ||
(user.legacyMemberNo !== undefined &&
(!Number.isSafeInteger(user.legacyMemberNo) || user.legacyMemberNo <= 0))
) {
return null;
}
+7 -2
View File
@@ -19,7 +19,9 @@ import {
createTraitCatalog,
createOfficerLevelActionModules,
DOMESTIC_TRAIT_KEYS,
EVENT_DOMESTIC_TRAIT_KEYS,
loadDomesticTraitModules,
loadEventDomesticTraitModules,
loadNationTraitModules,
loadPersonalityTraitModules,
loadWarTraitModules,
@@ -52,7 +54,10 @@ const itemWarModules: WarActionModule[] = createItemActionModules(
).war;
const crewTypeWarTriggerRegistry = createCrewTypeWarTriggerRegistry();
const traitCatalog = createTraitCatalog({
domestic: await loadDomesticTraitModules([...DOMESTIC_TRAIT_KEYS]),
domestic: [
...(await loadDomesticTraitModules([...DOMESTIC_TRAIT_KEYS])),
...(await loadEventDomesticTraitModules([...EVENT_DOMESTIC_TRAIT_KEYS])),
],
war: await loadWarTraitModules([...WAR_TRAIT_KEYS]),
personality: await loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
nation: await loadNationTraitModules([...NATION_TRAIT_KEYS]),
@@ -145,7 +150,7 @@ const mapGeneralPayload = (payload: BattleSimJobPayload['attackerGeneral']): Gen
officerLevel: payload.officer_level,
role: {
personality: payload.personal,
specialDomestic: null,
specialDomestic: payload.special ?? null,
specialWar: payload.special2,
items: {
horse: normalizeItemCode(payload.horse),
+1
View File
@@ -8,6 +8,7 @@ export const zBattleSimGeneral = z.object({
nation: z.number().int().positive(),
turntime: z.string().min(1),
personal: z.string().nullable(),
special: z.string().nullable().optional(),
special2: z.string().nullable(),
crew: z.number().int().min(0),
crewtype: z.number().int().positive(),
@@ -1,5 +1,7 @@
import {
ITEM_KEYS,
EVENT_DOMESTIC_TRAIT_KEYS,
loadEventDomesticTraitModules,
loadItemModules,
loadNationTraitModules,
loadPersonalityTraitModules,
@@ -95,22 +97,26 @@ const toTraitOption = (module: TraitModule): BattleSimTraitOption => ({
let cachedTraitOptions: Promise<{
nationTypes: BattleSimTraitOption[];
eventDomesticTraits: BattleSimTraitOption[];
warTraits: BattleSimTraitOption[];
personalities: BattleSimTraitOption[];
}> | null = null;
export const loadBattleSimTraitOptions = async (): Promise<{
nationTypes: BattleSimTraitOption[];
eventDomesticTraits: BattleSimTraitOption[];
warTraits: BattleSimTraitOption[];
personalities: BattleSimTraitOption[];
}> => {
if (!cachedTraitOptions) {
cachedTraitOptions = Promise.all([
loadNationTraitModules([...NATION_TRAIT_KEYS]),
loadEventDomesticTraitModules([...EVENT_DOMESTIC_TRAIT_KEYS]),
loadWarTraitModules([...WAR_TRAIT_KEYS]),
loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
]).then(([nationTraits, warTraits, personalities]) => ({
]).then(([nationTraits, eventDomesticTraits, warTraits, personalities]) => ({
nationTypes: nationTraits.map(toTraitOption),
eventDomesticTraits: eventDomesticTraits.map(toTraitOption),
warTraits: warTraits.map(toTraitOption),
personalities: personalities.map(toTraitOption),
}));
+1
View File
@@ -10,6 +10,7 @@ export interface BattleSimGeneralPayload {
nation: number;
turntime: string;
personal: string | null;
special?: string | null;
special2: string | null;
crew: number;
crewtype: number;
+16 -2
View File
@@ -29,6 +29,16 @@ export class ConflictingTurnDaemonCommandError extends Error {
}
}
export class FailedTurnDaemonCommandError extends Error {
constructor(
readonly requestId: string,
readonly storedError: string | null
) {
super(storedError ?? `Engine input event ${requestId} failed.`);
this.name = 'FailedTurnDaemonCommandError';
}
}
export class DatabaseTurnDaemonTransport implements TurnDaemonTransport {
constructor(
private readonly db: DatabaseClient,
@@ -45,6 +55,10 @@ export class DatabaseTurnDaemonTransport implements TurnDaemonTransport {
target: 'ENGINE',
eventType: command.type,
payload: asJson(durableCommand),
actorUserId:
'userId' in command && typeof command.userId === 'string'
? command.userId
: null,
},
});
} catch (error) {
@@ -80,13 +94,13 @@ export class DatabaseTurnDaemonTransport implements TurnDaemonTransport {
while (Date.now() < deadline) {
const event = await this.db.inputEvent.findUnique({
where: { requestId },
select: { status: true, result: true },
select: { status: true, result: true, error: true },
});
if (event?.status === 'SUCCEEDED') {
return event.result as T;
}
if (event?.status === 'FAILED') {
return null;
throw new FailedTurnDaemonCommandError(requestId, event.error);
}
await delay(Math.min(50, Math.max(1, deadline - Date.now())));
}
+8 -1
View File
@@ -5,7 +5,7 @@ import { isAfter, isValid, parseISO } from 'date-fns';
import { z } from 'zod';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import { procedure, router } from '../../trpc.js';
import { authedProcedure, procedure, router } from '../../trpc.js';
const parseDate = (value: string): Date | null => {
const parsed = parseISO(value);
@@ -45,6 +45,13 @@ const verifyGatewayToken = (
};
export const authRouter = router({
status: authedProcedure.query(({ ctx }) => {
const userId = ctx.auth?.user.id;
if (!userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return { userId };
}),
exchangeGatewayToken: procedure
.input(z.object({ gatewayToken: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
+3
View File
@@ -112,6 +112,7 @@ export const battleRouter = router({
crewTypes,
},
nationTypes: traits.nationTypes,
eventDomesticTraits: traits.eventDomesticTraits,
warTraits: traits.warTraits,
personalities: traits.personalities,
items,
@@ -207,6 +208,7 @@ export const battleRouter = router({
bookCode: true,
itemCode: true,
personalCode: true,
specialCode: true,
special2Code: true,
meta: true,
},
@@ -241,6 +243,7 @@ export const battleRouter = router({
injury: general.injury,
rice: general.rice,
personal: normalizeOptionalKey(general.personalCode),
special: normalizeOptionalKey(general.specialCode),
special2: normalizeOptionalKey(general.special2Code),
crew: general.crew,
crewtype: general.crewTypeId,
+165 -3
View File
@@ -2,8 +2,8 @@ import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { randomBytes } from 'node:crypto';
import type { DatabaseClient, WorldStateRow } from '../../context.js';
import { authedProcedure, router } from '../../trpc.js';
import type { DatabaseClient, GameApiContext, WorldStateRow } from '../../context.js';
import { authedProcedure, engineAuthedProcedure, router } from '../../trpc.js';
import { asNumber, asRecord, asStringArray, LiteHashDRBG } from '@sammo-ts/common';
import {
isPersonalityTraitKey,
@@ -21,6 +21,58 @@ import {
resolveInheritConstants,
setInheritancePoint,
} from '../../services/inheritance.js';
import {
getSelectionPoolStatus,
reserveSelectionPool,
resolveSelectionMaxGeneral,
} from '../../services/selectPool.js';
const resolveSelectionCommandResult = (
result:
| Awaited<ReturnType<GameApiContext['turnDaemon']['requestCommand']>>
| null,
expectedType: 'selectPoolCreate' | 'selectPoolReselect'
): { ok: true; generalId: number } => {
if (!result) {
throw new TRPCError({
code: 'TIMEOUT',
message:
'장수 선택 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.',
});
}
if (result.type !== expectedType) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: '턴 데몬이 올바르지 않은 장수 선택 결과를 반환했습니다.',
});
}
if (!result.ok) {
throw new TRPCError({
code: result.code,
message: result.reason,
});
}
return { ok: true, generalId: result.generalId };
};
const resolveSelectionRequestId = (
contextRequestId: string | undefined,
userId: string,
clientRequestId: string | undefined,
operation: 'create' | 'reselect'
): string | undefined => {
if (clientRequestId) {
return `select-pool:${userId}:${clientRequestId}:${operation}`;
}
if (!contextRequestId) {
return undefined;
}
const path =
operation === 'create'
? 'join.selectPoolGeneral'
: 'join.reselectPoolGeneral';
return `${contextRequestId}:${path}`;
};
const DEFAULT_JOIN_STAT = {
total: 165,
@@ -181,10 +233,12 @@ export const joinRouter = router({
const availableSpecialWar = asStringArray(configConst.availableSpecialWar);
const warKeys = availableSpecialWar.length > 0 ? availableSpecialWar : [...WAR_TRAIT_KEYS];
const [personalities, warSpecials, nationRows] = await Promise.all([
const [personalities, warSpecials, nationRows, userGeneralCount, npcGeneralCount] =
await Promise.all([
loadPersonalityOptions(),
loadWarOptions(warKeys),
ctx.db.nation.findMany({
where: { id: { gt: 0 } },
select: {
id: true,
name: true,
@@ -193,6 +247,8 @@ export const joinRouter = router({
},
orderBy: { id: 'asc' },
}),
ctx.db.general.count({ where: { npcState: { lt: 2 } } }),
ctx.db.general.count({ where: { npcState: { gte: 2 } } }),
]);
const nations = nationRows.map((nation) => {
@@ -209,7 +265,13 @@ export const joinRouter = router({
const inheritTotalPoint = ctx.auth?.user.id
? await readInheritancePoint(ctx.db, ctx.auth.user.id, 'previous')
: 0;
const selectionPool = await getSelectionPoolStatus(
ctx.db,
worldState,
ctx.auth?.user.id ?? ''
);
const tickMinutes = Math.max(1, Math.round(worldState.tickSeconds / 60));
const maxGeneral = resolveSelectionMaxGeneral(worldState);
const inheritCitiesRaw = await ctx.db.city.findMany({
where: { level: { in: [5, 6] }, nationId: 0 },
select: { id: true, name: true, level: true, region: true },
@@ -239,6 +301,14 @@ export const joinRouter = router({
],
warSpecials,
nations,
serverInfo: {
currentYear: worldState.currentYear,
currentMonth: worldState.currentMonth,
tickMinutes,
maxGeneral,
userGeneralCount,
npcGeneralCount,
},
inherit: {
totalPoint: inheritTotalPoint,
costs: {
@@ -251,8 +321,93 @@ export const joinRouter = router({
turnTimeZones: buildTurnTimeZones(tickMinutes),
availableSpecialWar: warSpecials,
},
selectionPool,
};
}),
getSelectionPool: authedProcedure.mutation(async ({ ctx }) => {
const userId = ctx.auth?.user.id;
if (!userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
const worldState = await ctx.db.worldState.findFirst();
if (!worldState) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'World state is not initialized.',
});
}
return reserveSelectionPool({
db: ctx.db,
worldState,
userId,
seedOwnerIdentity: ctx.auth?.user.legacyMemberNo ?? userId,
});
}),
selectPoolGeneral: engineAuthedProcedure
.input(
z.object({
uniqueName: z.string().min(1).max(20),
personality: z.string().min(1),
clientRequestId: z.string().uuid().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const auth = ctx.auth;
if (!auth) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
const userId = auth.user.id;
if (auth.identity?.canCreateGeneral === false) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '이 서버에서는 카카오 인증을 완료해야 장수를 생성할 수 있습니다.',
});
}
const commandRequestId = resolveSelectionRequestId(
ctx.requestId,
userId,
input.clientRequestId,
'create'
);
const result = await ctx.turnDaemon.requestCommand({
type: 'selectPoolCreate',
...(commandRequestId ? { requestId: commandRequestId } : {}),
userId,
ownerDisplayName: auth.user.displayName,
uniqueName: input.uniqueName,
personality: input.personality,
seedOwnerIdentity: auth.user.legacyMemberNo ?? userId,
});
return resolveSelectionCommandResult(result, 'selectPoolCreate');
}),
reselectPoolGeneral: engineAuthedProcedure
.input(
z.object({
uniqueName: z.string().min(1).max(20),
clientRequestId: z.string().uuid().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const auth = ctx.auth;
if (!auth) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
const userId = auth.user.id;
const commandRequestId = resolveSelectionRequestId(
ctx.requestId,
userId,
input.clientRequestId,
'reselect'
);
const result = await ctx.turnDaemon.requestCommand({
type: 'selectPoolReselect',
...(commandRequestId ? { requestId: commandRequestId } : {}),
userId,
ownerDisplayName: auth.user.displayName,
uniqueName: input.uniqueName,
});
return resolveSelectionCommandResult(result, 'selectPoolReselect');
}),
createGeneral: authedProcedure
.input(
z.object({
@@ -287,6 +442,13 @@ export const joinRouter = router({
message: 'World state is not initialized.',
});
}
const selectionPool = await getSelectionPoolStatus(ctx.db, worldState, userId);
if (selectionPool.enabled) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: '장수 선택 목록에서 장수를 골라 주세요.',
});
}
const joinPolicy = resolveJoinPolicy(worldState);
if (joinPolicy.blockGeneralCreate === 1) {
+4 -1
View File
@@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
import { isSelectionPoolWorld } from '../../services/selectPool.js';
import { procedure, router } from '../../trpc.js';
export const lobbyRouter = router({
@@ -27,12 +28,13 @@ export const lobbyRouter = router({
if (ctx.auth?.user.id) {
const general = await ctx.db.general.findFirst({
where: { userId: ctx.auth.user.id },
select: { name: true, picture: true },
select: { name: true, picture: true, imageServer: true },
});
if (general) {
myGeneral = {
name: general.name,
picture: general.picture,
imageServer: general.imageServer,
};
}
}
@@ -51,6 +53,7 @@ export const lobbyRouter = router({
turntime: worldState.meta.turntime ?? '',
otherTextInfo: worldState.meta.otherTextInfo ?? '',
isUnited: worldState.meta.isUnited ?? 0,
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
myGeneral,
};
}),
+13
View File
@@ -5,11 +5,14 @@ import { asNumber, asRecord } from '@sammo-ts/common';
import {
createIncomeActionContext,
DomesticTraitLoader,
EventDomesticTraitLoader,
isDomesticTraitKey,
isEventDomesticTraitKey,
isNationTraitKey,
isPersonalityTraitKey,
isWarTraitKey,
loadDomesticTraitModules,
loadEventDomesticTraitModules,
loadNationTraitModules,
loadPersonalityTraitModules,
loadWarTraitModules,
@@ -301,12 +304,22 @@ export const loadTraitNames = async (keys: Array<string | null>, kind: keyof Tra
if (kind === 'domestic') {
const filtered = missing.filter((key) => isDomesticTraitKey(key));
const eventFiltered = missing.filter((key) => isEventDomesticTraitKey(key));
if (filtered.length) {
const modules = await loadDomesticTraitModules(filtered, new DomesticTraitLoader());
for (const module of modules) {
cache.set(module.key, { name: module.name, info: module.info ?? '' });
}
}
if (eventFiltered.length) {
const modules = await loadEventDomesticTraitModules(
eventFiltered,
new EventDomesticTraitLoader()
);
for (const module of modules) {
cache.set(module.key, { name: module.name, info: module.info ?? '' });
}
}
} else if (kind === 'war') {
const filtered = missing.filter((key) => isWarTraitKey(key));
if (filtered.length) {
+12
View File
@@ -0,0 +1,12 @@
export {
buildSelectPoolSeed,
claimWeightedSelectionCandidates,
getSelectionPoolStatus,
isSelectionPoolWorld,
reserveSelectionPool,
resolveSelectionMaxGeneral,
SelectPoolError,
type SelectPoolCandidateDto,
type SelectPoolCandidateInfo,
type SelectPoolReservationDto,
} from '@sammo-ts/game-engine';
+5
View File
@@ -71,6 +71,11 @@ export const router = t.router;
export const procedure = t.procedure.use(inputEventMiddleware);
export const authedProcedure: typeof procedure = procedure.use(requireAuthMiddleware);
// 턴 데몬이 ENGINE input_event와 world/DB 변경을 자체 transaction으로
// 커밋하는 mutation에 사용한다. API input-event transaction으로 한 번 더
// 감싸면 daemon이 아직 commit되지 않은 command를 볼 수 없어 교착된다.
export const engineAuthedProcedure: typeof procedure = t.procedure.use(requireAuthMiddleware);
// 페이지 조회 계측처럼 game state/input-event 원장과 무관한 세션 보조
// mutation에 사용한다. gameplay state 변경에는 사용하지 않는다.
export const sessionActivityProcedure = t.procedure;
@@ -300,6 +300,16 @@ describe('battle sim processor', () => {
expect(strong.killed).toBeGreaterThan(baseline.killed ?? 0);
});
it('applies the event-domestic trait carried by an imported general', () => {
const baseline = processBattleSimJob(buildPayload('battle'));
const payload = buildPayload('battle');
payload.attackerGeneral.special = 'che_event_무쌍';
const eventDomestic = processBattleSimJob(payload);
expect(eventDomestic.killed).toBeGreaterThan(baseline.killed ?? 0);
expect(eventDomestic).not.toEqual(baseline);
});
it('keeps StrongAttacker city combat identical but applies MoreEffect to it', () => {
const baselinePayload = buildPayload('battle');
baselinePayload.defenderGenerals = [];
@@ -427,6 +427,7 @@ describe('battle simulator general import permissions', () => {
bookCode: null,
itemCode: null,
personalCode: null,
specialCode: null,
special2Code: null,
meta: {},
...overrides,
@@ -447,6 +448,7 @@ describe('battle simulator general import permissions', () => {
weaponCode: 'che_의천검',
bookCode: 'che_손자병법',
itemCode: 'che_옥새',
specialCode: 'che_event_신산',
meta: {
dex1: 10000,
rank_warnum: 33,
@@ -483,6 +485,7 @@ describe('battle simulator general import permissions', () => {
warnum: 33,
killnum: 22,
killcrew: 1111,
special: 'che_event_신산',
});
const redacted = await foreign.battle.getGeneralDetail({ generalId: ally.id });
@@ -499,6 +502,7 @@ describe('battle simulator general import permissions', () => {
warnum: 0,
killnum: 0,
killcrew: 0,
special: 'che_event_신산',
});
});
@@ -2,7 +2,11 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
import { DuplicateInputEventError, executeInputEvent } from '../src/inputEventBoundary.js';
import { ConflictingTurnDaemonCommandError, DatabaseTurnDaemonTransport } from '../src/daemon/databaseTransport.js';
import {
ConflictingTurnDaemonCommandError,
DatabaseTurnDaemonTransport,
FailedTurnDaemonCommandError,
} from '../src/daemon/databaseTransport.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
const integration = describe.skipIf(!databaseUrl);
@@ -144,4 +148,25 @@ integration('API input event boundary', () => {
ConflictingTurnDaemonCommandError
);
});
it('distinguishes a stored terminal engine failure from a result timeout', async () => {
const transport = new DatabaseTurnDaemonTransport(db, 100);
const requestId = 'integration:api:engine-failed';
const command = { type: 'vacation' as const, requestId, generalId: 7 };
await transport.sendCommand(command);
await db.inputEvent.update({
where: { requestId },
data: {
status: 'FAILED',
error: 'injected terminal engine failure',
completedAt: new Date(),
},
});
await expect(transport.requestCommand(command)).rejects.toMatchObject({
name: FailedTurnDaemonCommandError.name,
requestId,
storedError: 'injected terminal engine failure',
});
});
});
+23
View File
@@ -274,6 +274,29 @@ describe('appRouter', () => {
});
});
it('reports the authenticated game access-token identity', async () => {
const caller = appRouter.createCaller(buildContext({ auth: buildAuth() }));
await expect(caller.auth.status()).resolves.toEqual({ userId: 'user-1' });
});
it('rejects unauthenticated or game-blocked auth status checks', async () => {
await expect(
appRouter.createCaller(buildContext({ auth: null })).auth.status()
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
await expect(
appRouter
.createCaller(
buildContext({
auth: buildAuth({
suspendedUntil: '2099-01-01T00:00:00.000Z',
}),
})
)
.auth.status()
).rejects.toMatchObject({ code: 'FORBIDDEN' });
});
it('does not apply an expired or message-only restriction to other game APIs', async () => {
const caller = appRouter.createCaller(
buildContext({
@@ -0,0 +1,582 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { RANK_DATA_TYPES } from '@sammo-ts/common';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import {
createTurnDaemonRuntime,
seedScenarioToDatabase,
type TurnDaemonRuntime,
} from '@sammo-ts/game-engine';
import {
createGamePostgresConnector,
type GamePrisma,
type GamePrismaClient,
type RedisConnector,
} from '@sammo-ts/infra';
import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
import type { GameApiContext } from '../src/context.js';
import { DatabaseTurnDaemonTransport } from '../src/daemon/databaseTransport.js';
import type { TurnDaemonTransport } from '../src/daemon/transport.js';
import { appRouter } from '../src/router.js';
const databaseUrl = process.env.SELECT_POOL_DATABASE_URL;
const integration = describe.skipIf(!databaseUrl);
const userId = 'select-pool-integration-user';
const otherUserId = 'select-pool-integration-other-user';
const foreignUserId = 'select-pool-integration-foreign-user';
const failureUserId = 'select-pool-integration-failure-user';
const profile = 'hwe:903';
const schemaName = databaseUrl ? new URL(databaseUrl).searchParams.get('schema') ?? '' : '';
const assertDedicatedDatabase = (rawUrl: string): void => {
const schema = new URL(rawUrl).searchParams.get('schema');
if (!schema?.endsWith('select_pool_integration')) {
throw new Error(`Refusing to mutate non-dedicated schema: ${schema ?? '(missing)'}`);
}
if (!/^[a-z0-9_]+$/.test(schema)) {
throw new Error(`Refusing unsafe schema name: ${schema}`);
}
};
const auth: GameSessionTokenPayload = {
version: 1,
profile,
issuedAt: '2026-07-30T00:00:00.000Z',
expiresAt: '2026-08-30T00:00:00.000Z',
sessionId: 'select-pool-integration-session',
user: {
id: userId,
username: 'select-pool-user',
displayName: '선택사용자',
roles: ['user'],
},
sanctions: {},
};
const otherAuth: GameSessionTokenPayload = {
...auth,
sessionId: 'select-pool-integration-other-session',
user: {
...auth.user,
id: otherUserId,
username: 'select-pool-other',
displayName: '다른사용자',
},
};
const foreignAuth: GameSessionTokenPayload = {
...auth,
sessionId: 'select-pool-integration-foreign-session',
user: {
...auth.user,
id: foreignUserId,
username: 'select-pool-foreign',
displayName: '외국사용자',
},
};
const failureAuth: GameSessionTokenPayload = {
...auth,
sessionId: 'select-pool-integration-failure-session',
user: {
...auth.user,
id: failureUserId,
username: 'select-pool-failure',
displayName: '실패사용자',
},
};
integration('scenario 903 select pool through the durable turn daemon', () => {
let db: GamePrismaClient;
let closeDb: (() => Promise<void>) | undefined;
let runtime: TurnDaemonRuntime | undefined;
let daemonLoop: Promise<void> | undefined;
let turnDaemon: TurnDaemonTransport;
let worldStateId: number;
const buildContext = (
requestId: string,
actorAuth: GameSessionTokenPayload = auth
): GameApiContext => {
const redisClient = {
get: async () => null,
set: async () => null,
};
return {
requestId,
db,
redis: redisClient as unknown as RedisConnector['client'],
turnDaemon,
battleSim: new InMemoryBattleSimTransport(),
profile: { id: 'hwe', scenario: '903', name: profile },
uploadDir: 'uploads',
uploadPath: '/uploads',
uploadPublicUrl: null,
auth: actorAuth,
accessTokenStore: new RedisAccessTokenStore(redisClient, profile),
flushStore: new InMemoryFlushStore(),
gameTokenSecret: 'select-pool-test-secret',
};
};
beforeAll(async () => {
assertDedicatedDatabase(databaseUrl!);
const previousSeed = process.env.INTEGRATION_WORLD_SEED;
process.env.INTEGRATION_WORLD_SEED = 'select-pool-integration-seed';
try {
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl: databaseUrl!,
now: new Date('2099-07-30T12:00:00.000Z'),
installOptions: {
turnTermMinutes: 5,
npcMode: 2,
showImgLevel: 3,
serverId: profile,
season: 1,
},
});
} finally {
if (previousSeed === undefined) {
delete process.env.INTEGRATION_WORLD_SEED;
} else {
process.env.INTEGRATION_WORLD_SEED = previousSeed;
}
}
const connector = createGamePostgresConnector({ url: databaseUrl! });
await connector.connect();
db = connector.prisma;
closeDb = () => connector.disconnect();
await db.inputEvent.deleteMany();
await db.logEntry.deleteMany();
worldStateId = (await db.worldState.findFirstOrThrow()).id;
runtime = await createTurnDaemonRuntime({
profile,
databaseUrl: databaseUrl!,
enableDatabaseFlush: true,
enableLeaseHeartbeat: false,
leaseOwnerId: 'select-pool-integration-daemon',
});
turnDaemon = new DatabaseTurnDaemonTransport(db, 10_000);
daemonLoop = runtime.lifecycle.start();
await expect(turnDaemon.requestStatus(10_000)).resolves.toMatchObject({
state: expect.any(String),
});
}, 60_000);
afterAll(async () => {
if (runtime) {
await runtime.lifecycle.stop('select-pool integration complete');
await daemonLoop;
await runtime.close();
}
await closeDb?.();
}, 30_000);
it('creates and reselects in one durable DB and in-memory command boundary', async () => {
await expect(
appRouter.createCaller(buildContext('select-pool-public-lobby')).lobby.info()
).resolves.toMatchObject({ selectionPoolEnabled: true });
await expect(
appRouter.createCaller(buildContext('select-pool-config')).join.getConfig()
).resolves.toMatchObject({
serverInfo: {
currentYear: 180,
currentMonth: 1,
tickMinutes: 5,
maxGeneral: 500,
},
});
const [firstReservation, concurrentReservation] = await Promise.all([
appRouter.createCaller(buildContext('select-pool-reserve-a')).join.getSelectionPool(),
appRouter.createCaller(buildContext('select-pool-reserve-b')).join.getSelectionPool(),
]);
expect(concurrentReservation).toEqual(firstReservation);
expect(firstReservation.candidates).toHaveLength(14);
expect(await db.selectPoolEntry.count({ where: { ownerUserId: userId } })).toBe(14);
const attempts = await Promise.allSettled([
appRouter.createCaller(buildContext('select-pool-create-a')).join.selectPoolGeneral({
uniqueName: firstReservation.candidates[0]!.uniqueName,
personality: 'che_안전',
}),
appRouter.createCaller(buildContext('select-pool-create-b')).join.selectPoolGeneral({
uniqueName: firstReservation.candidates[1]!.uniqueName,
personality: 'che_유지',
}),
]);
expect(attempts.filter((attempt) => attempt.status === 'fulfilled')).toHaveLength(1);
expect(attempts.filter((attempt) => attempt.status === 'rejected')).toHaveLength(1);
const initial = await db.general.findFirstOrThrow({ where: { userId } });
const initialRuntime = runtime!.world.getGeneralById(initial.id);
expect(initialRuntime).toMatchObject({
id: initial.id,
userId,
name: initial.name,
imageServer: initial.imageServer,
stats: {
leadership: initial.leadership,
strength: initial.strength,
intelligence: initial.intel,
},
});
expect(initial.id).toBeGreaterThan(
Math.max(
...runtime!.world
.listGenerals()
.filter((general) => general.id !== initial.id)
.map((general) => general.id)
)
);
expect(
await db.generalTurn.findMany({
where: { generalId: initial.id },
orderBy: { turnIdx: 'asc' },
select: { turnIdx: true, actionCode: true, arg: true },
})
).toEqual(
Array.from({ length: 30 }, (_, turnIdx) => ({
turnIdx,
actionCode: '휴식',
arg: {},
}))
);
await expect(
db.generalTurnRevision.findUniqueOrThrow({ where: { generalId: initial.id } })
).resolves.toMatchObject({ revision: 0, leaseOwner: null, leaseExpiresAt: null });
const initialRankRows = await db.rankData.findMany({
where: { generalId: initial.id },
orderBy: { type: 'asc' },
select: { nationId: true, type: true, value: true },
});
expect(initialRankRows).toHaveLength(RANK_DATA_TYPES.length);
expect(initialRankRows.map(({ type }) => type).sort()).toEqual(
[...RANK_DATA_TYPES].sort()
);
expect(initialRankRows.every(({ nationId, value }) => nationId === 0 && value === 0)).toBe(
true
);
expect(await db.selectPoolEntry.count({ where: { generalId: initial.id } })).toBe(1);
expect(await db.selectPoolEntry.count({ where: { ownerUserId: userId } })).toBe(0);
expect(
await db.logEntry.count({
where: { meta: { path: ['ownerUserId'], equals: userId } },
})
).toBe(2);
await expect(
appRouter.createCaller(buildContext('select-pool-cooldown')).join.getSelectionPool()
).rejects.toMatchObject({ message: '아직 다시 고를 수 없습니다' });
const cooledAt = '2026-07-29T00:00:00.000Z';
await expect(
turnDaemon.requestCommand({
type: 'patchGeneral',
requestId: 'select-pool-cooldown-patch',
generalId: initial.id,
patch: {
meta: {
next_change: cooledAt,
nextChangeAt: cooledAt,
},
},
})
).resolves.toMatchObject({ type: 'patchGeneral', ok: true, generalId: initial.id });
const reselection = await appRouter
.createCaller(buildContext('select-pool-reserve-reselection'))
.join.getSelectionPool();
const target = reselection.candidates.find(
(candidate) => candidate.generalName !== initial.name
)!;
await expect(
appRouter
.createCaller(buildContext('select-pool-reselect'))
.join.reselectPoolGeneral({ uniqueName: target.uniqueName })
).resolves.toEqual({ ok: true, generalId: initial.id });
const updated = await db.general.findUniqueOrThrow({ where: { id: initial.id } });
expect(updated).toMatchObject({
id: initial.id,
userId,
name: target.generalName,
leadership: target.leadership,
strength: target.strength,
intel: target.intel,
personalCode: initial.personalCode,
specialCode: target.specialDomestic,
imageServer: target.imageServer,
picture: target.picture,
});
expect(runtime!.world.getGeneralById(initial.id)).toMatchObject({
id: initial.id,
userId,
name: target.generalName,
imageServer: target.imageServer,
picture: target.picture,
stats: {
leadership: target.leadership,
strength: target.strength,
intelligence: target.intel,
},
});
expect(await db.selectPoolEntry.count({ where: { generalId: initial.id } })).toBe(1);
expect(
await db.selectPoolEntry.findUniqueOrThrow({ where: { uniqueName: target.uniqueName } })
).toMatchObject({ generalId: initial.id, ownerUserId: null, reservedUntil: null });
expect(
await db.logEntry.count({
where: { meta: { path: ['ownerUserId'], equals: userId } },
})
).toBe(4);
await expect(
turnDaemon.requestCommand({
type: 'patchGeneral',
requestId: 'select-pool-post-reselection-flush',
generalId: initial.id,
patch: { meta: { postReselectionFlush: 1 } },
})
).resolves.toMatchObject({ type: 'patchGeneral', ok: true });
await expect(
db.general.findUniqueOrThrow({ where: { id: initial.id } })
).resolves.toMatchObject({
name: target.generalName,
leadership: target.leadership,
strength: target.strength,
intel: target.intel,
});
const fullWorld = await db.worldState.findUniqueOrThrow({ where: { id: worldStateId } });
const fullConfig = fullWorld.config as Record<string, unknown>;
await db.worldState.update({
where: { id: worldStateId },
data: { config: { ...fullConfig, maxGeneral: 1 } as GamePrisma.InputJsonValue },
});
const secondCooledAt = '2026-07-28T00:00:00.000Z';
await turnDaemon.requestCommand({
type: 'patchGeneral',
requestId: 'select-pool-full-cooldown-patch',
generalId: initial.id,
patch: {
meta: {
next_change: secondCooledAt,
nextChangeAt: secondCooledAt,
},
},
});
const fullReselection = await appRouter
.createCaller(buildContext('select-pool-full-reselection-reserve'))
.join.getSelectionPool();
await expect(
appRouter
.createCaller(buildContext('select-pool-full-reselection'))
.join.reselectPoolGeneral({
uniqueName: fullReselection.candidates[0]!.uniqueName,
})
).resolves.toEqual({ ok: true, generalId: initial.id });
const otherReservation = await appRouter
.createCaller(buildContext('select-pool-full-new-user-reserve', otherAuth))
.join.getSelectionPool();
await expect(
appRouter
.createCaller(buildContext('select-pool-full-new-user-create', otherAuth))
.join.selectPoolGeneral({
uniqueName: otherReservation.candidates[0]!.uniqueName,
personality: 'che_안전',
})
).rejects.toMatchObject({ message: '더 이상 등록 할 수 없습니다.' });
expect(await db.general.count({ where: { userId: otherUserId } })).toBe(0);
await db.worldState.update({
where: { id: worldStateId },
data: { config: fullConfig as GamePrisma.InputJsonValue },
});
}, 30_000);
it('keeps a stable ENGINE event for retries and rejects reservation bypasses', async () => {
const reservation = await appRouter
.createCaller(buildContext('select-pool-other-reserve', otherAuth))
.join.getSelectionPool();
const candidate = reservation.candidates[0]!;
await expect(
appRouter
.createCaller(buildContext('select-pool-foreign-token', foreignAuth))
.join.selectPoolGeneral({
uniqueName: candidate.uniqueName,
personality: 'che_안전',
})
).rejects.toMatchObject({ message: '유효한 장수 목록이 없습니다.' });
expect(await db.general.count({ where: { userId: foreignUserId } })).toBe(0);
await db.selectPoolEntry.update({
where: { uniqueName: candidate.uniqueName },
data: { reservedUntil: new Date(Date.now() - 60_000) },
});
await expect(
appRouter
.createCaller(buildContext('select-pool-expired-token', otherAuth))
.join.selectPoolGeneral({
uniqueName: candidate.uniqueName,
personality: 'che_안전',
})
).rejects.toMatchObject({ message: '유효한 장수 목록이 없습니다.' });
expect(await db.general.count({ where: { userId: otherUserId } })).toBe(0);
await expect(
appRouter
.createCaller(buildContext('select-pool-generic-bypass', otherAuth))
.join.createGeneral({
name: '우회장수',
leadership: 55,
strength: 55,
intel: 55,
character: 'che_안전',
})
).rejects.toMatchObject({ message: '장수 선택 목록에서 장수를 골라 주세요.' });
const input = {
uniqueName: reservation.candidates[1]!.uniqueName,
personality: 'che_안전',
};
await db.selectPoolEntry.updateMany({
where: { ownerUserId: otherUserId, generalId: null },
data: { reservedUntil: new Date(Date.now() + 60_000) },
});
const runtimeAllocatorBefore = runtime!.world.getState().meta.lastGeneralId;
const persistedAllocatorBefore = (
(await db.worldState.findUniqueOrThrow({ where: { id: worldStateId } }))
.meta as Record<string, unknown>
).lastGeneralId;
await expect(
appRouter
.createCaller(buildContext('select-pool-invalid-personality', otherAuth))
.join.selectPoolGeneral({
...input,
personality: 'not-a-personality',
clientRequestId: '11111111-1111-4111-8111-111111111111',
})
).rejects.toMatchObject({ message: '올바르지 않은 성격입니다.' });
expect(runtime!.world.getState().meta.lastGeneralId).toBe(runtimeAllocatorBefore);
expect(
(
(await db.worldState.findUniqueOrThrow({ where: { id: worldStateId } }))
.meta as Record<string, unknown>
).lastGeneralId
).toBe(persistedAllocatorBefore);
expect(await db.general.count({ where: { userId: otherUserId } })).toBe(0);
const stableClientRequestId = '22222222-2222-4222-8222-222222222222';
const stableInput = { ...input, clientRequestId: stableClientRequestId };
const first = await appRouter
.createCaller(buildContext('select-pool-http-attempt-a', otherAuth))
.join.selectPoolGeneral(stableInput);
const retried = await appRouter
.createCaller(buildContext('select-pool-http-attempt-b', otherAuth))
.join.selectPoolGeneral(stableInput);
expect(retried).toEqual(first);
expect(await db.general.count({ where: { userId: otherUserId } })).toBe(1);
await expect(
db.inputEvent.findUniqueOrThrow({
where: {
requestId: `select-pool:${otherUserId}:${stableClientRequestId}:create`,
},
})
).resolves.toMatchObject({
status: 'SUCCEEDED',
attempts: 1,
actorUserId: otherUserId,
});
}, 30_000);
it('rolls back a hard failure and retries the same ENGINE event exactly once', async () => {
const reservation = await appRouter
.createCaller(buildContext('select-pool-failure-reserve', failureAuth))
.join.getSelectionPool();
const candidate = reservation.candidates[0]!;
const requestUuid = '33333333-3333-4333-8333-333333333333';
const requestId = `select-pool:${failureUserId}:${requestUuid}:create`;
const triggerName = 'select_pool_fail_first_log';
const functionName = 'select_pool_fail_first_log_fn';
await db.$executeRawUnsafe(`
CREATE OR REPLACE FUNCTION "${schemaName}"."${functionName}"()
RETURNS trigger AS $$
BEGIN
IF NEW.meta ->> 'ownerUserId' = '${failureUserId}'
AND EXISTS (
SELECT 1
FROM "${schemaName}"."input_event"
WHERE "request_id" = '${requestId}'
AND "status" = 'PROCESSING'
AND "attempts" = 1
)
THEN
RAISE EXCEPTION 'injected first selection log failure';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
`);
await db.$executeRawUnsafe(`
CREATE TRIGGER "${triggerName}"
BEFORE INSERT ON "${schemaName}"."log_entry"
FOR EACH ROW EXECUTE FUNCTION "${schemaName}"."${functionName}"()
`);
try {
await expect(
appRouter
.createCaller(buildContext('select-pool-failure-http', failureAuth))
.join.selectPoolGeneral({
uniqueName: candidate.uniqueName,
personality: 'che_안전',
clientRequestId: requestUuid,
})
).resolves.toMatchObject({ ok: true, generalId: expect.any(Number) });
} finally {
await db.$executeRawUnsafe(
`DROP TRIGGER IF EXISTS "${triggerName}" ON "${schemaName}"."log_entry"`
);
await db.$executeRawUnsafe(
`DROP FUNCTION IF EXISTS "${schemaName}"."${functionName}"()`
);
}
const created = await db.general.findFirstOrThrow({ where: { userId: failureUserId } });
expect(runtime!.world.getGeneralById(created.id)).toMatchObject({
id: created.id,
userId: failureUserId,
name: created.name,
});
expect(await db.general.count({ where: { userId: failureUserId } })).toBe(1);
expect(await db.generalTurn.count({ where: { generalId: created.id } })).toBe(30);
expect(await db.generalTurnRevision.count({ where: { generalId: created.id } })).toBe(1);
expect(await db.rankData.count({ where: { generalId: created.id } })).toBe(
RANK_DATA_TYPES.length
);
expect(await db.generalAccessLog.count({ where: { generalId: created.id } })).toBe(1);
expect(await db.selectPoolEntry.count({ where: { generalId: created.id } })).toBe(1);
expect(
await db.logEntry.count({
where: { meta: { path: ['ownerUserId'], equals: failureUserId } },
})
).toBe(2);
await expect(
db.inputEvent.findUniqueOrThrow({ where: { requestId } })
).resolves.toMatchObject({
status: 'SUCCEEDED',
attempts: 2,
actorUserId: failureUserId,
error: null,
});
}, 30_000);
});
+75
View File
@@ -0,0 +1,75 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import {
buildSelectPoolSeed,
claimWeightedSelectionCandidates,
} from '../src/services/selectPool.js';
interface PoolResource {
data: Array<
[
string,
number,
number,
number,
string,
[number, number, number, number, number],
0 | 1,
string,
]
>;
}
const loadWeightedRows = async (): Promise<Array<[{ id: number }, number]>> => {
const filePath = path.resolve(
import.meta.dirname,
'../../../resources/general-pool/SPoolUnderU30.json'
);
const resource = JSON.parse(await fs.readFile(filePath, 'utf8')) as PoolResource;
return resource.data.map((row, index) => [
{ id: index + 1 },
row[5].reduce((sum, value) => sum + value, 0),
]);
};
const drawVector = async (
hiddenSeed: string
): Promise<{ selected: number[]; draws: number[] }> => {
const weighted = await loadWeightedRows();
const now = new Date('2026-07-30T03:34:56.000Z');
const draws: number[] = [];
const selected = await claimWeightedSelectionCandidates({
weighted,
rng: new RandUtil(new LiteHashDRBG(buildSelectPoolSeed(hiddenSeed, 42, now))),
count: 14,
claim: async () => true,
onDraw: (candidate) => draws.push(candidate.id),
});
return { selected: selected.map((candidate) => candidate.id), draws };
};
describe('select pool Ref RNG parity', () => {
it('uses the legacy seed serialization and fixed UnderS30 draw vector', async () => {
const now = new Date('2026-07-30T03:34:56.000Z');
expect(buildSelectPoolSeed('vector-hidden', 42, now)).toBe(
'str(13,vector-hidden)|str(10,selectPool)|int(42)|str(19,2026-07-30 12:34:56)'
);
await expect(drawVector('vector-hidden')).resolves.toEqual({
selected: [72, 1283, 110, 1659, 608, 1408, 1543, 1573, 1096, 1081, 278, 1256, 872, 1369],
draws: [72, 1283, 110, 1659, 608, 1408, 1543, 1573, 1096, 1081, 278, 1256, 872, 1369],
});
});
it('consumes duplicate draws without removing the candidate from the weighted pool', async () => {
await expect(drawVector('vector-hidden-28')).resolves.toEqual({
selected: [314, 865, 1485, 1382, 110, 550, 27, 368, 399, 1298, 152, 39, 189, 760],
draws: [314, 865, 1485, 1382, 110, 550, 27, 368, 399, 1298, 27, 152, 39, 189, 760],
});
});
});
+10 -1
View File
@@ -11,6 +11,12 @@
"@sammo-ts/common/*": [
"../../packages/common/src/*"
],
"@sammo-ts/game-engine": [
"../../app/game-engine/src/index.ts"
],
"@sammo-ts/game-engine/*": [
"../../app/game-engine/src/*"
],
"@sammo-ts/infra": [
"../../packages/infra/src/index.ts"
],
@@ -39,6 +45,9 @@
},
{
"path": "../../packages/logic"
},
{
"path": "../game-engine"
}
]
}
}
+2
View File
@@ -12,6 +12,7 @@ export * from './lifecycle/turnDaemonLifecycle.js';
export * from './lifecycle/getNextTickTime.js';
export * from './scenario/scenarioLoader.js';
export * from './scenario/scenarioComposition.js';
export * from './scenario/generalPoolLoader.js';
export * from './scenario/databaseUrl.js';
export * from './scenario/mapLoader.js';
export * from './scenario/scenarioSeeder.js';
@@ -22,6 +23,7 @@ export * from './turn/engineStateManager.js';
export * from './turn/inMemoryStateStore.js';
export * from './turn/inMemoryTurnProcessor.js';
export * from './turn/databaseHooks.js';
export * from './turn/selectPoolService.js';
export * from './turn/turnDaemon.js';
export * from './turn/cli.js';
@@ -17,6 +17,7 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
private readonly localQueue: TurnDaemonCommand[] = [];
private readonly workerId = randomUUID();
private readonly leaseDurationMs = 60_000;
private readonly maxAttempts = 3;
constructor(private readonly db: GamePrismaClient) {}
@@ -62,6 +63,48 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
await this.complete(requestId, result);
}
async publishCommandError(requestId: string, error: unknown): Promise<void> {
const message = error instanceof Error ? error.message : 'Unknown command error.';
await this.db.$transaction(async (transaction) => {
const event = await transaction.inputEvent.findUnique({
where: { requestId },
select: {
status: true,
target: true,
lockedBy: true,
attempts: true,
},
});
if (
!event ||
event.target !== 'ENGINE' ||
event.status !== 'PROCESSING' ||
event.lockedBy !== this.workerId
) {
return;
}
const terminal = event.attempts >= this.maxAttempts;
await transaction.inputEvent.updateMany({
where: {
requestId,
target: 'ENGINE',
status: 'PROCESSING',
lockedBy: this.workerId,
attempts: event.attempts,
},
data: {
status: terminal ? 'FAILED' : 'PENDING',
processingAt: null,
lockedBy: null,
leaseUntil: null,
completedAt: terminal ? new Date() : null,
result: GamePrisma.DbNull,
error: message,
},
});
});
}
private async claimPending(limit = 100): Promise<TurnDaemonCommand[]> {
await this.recoverExpiredLeases();
return this.db.$transaction(async (transaction) => {
@@ -132,8 +175,13 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
}
private async complete(requestId: string, result: unknown): Promise<void> {
await this.db.inputEvent.update({
where: { requestId },
const completed = await this.db.inputEvent.updateMany({
where: {
requestId,
target: 'ENGINE',
status: 'PROCESSING',
lockedBy: this.workerId,
},
data: {
status: 'SUCCEEDED',
result: asJson(result),
@@ -143,6 +191,24 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
leaseUntil: null,
},
});
if (completed.count > 0) {
return;
}
// Database hooks commit mutation results atomically with game state and
// may already have set SUCCEEDED. Only the worker that still owns the
// lease may clear that committed row's claim metadata.
await this.db.inputEvent.updateMany({
where: {
requestId,
target: 'ENGINE',
status: 'SUCCEEDED',
lockedBy: this.workerId,
},
data: {
lockedBy: null,
leaseUntil: null,
},
});
}
private async recoverExpiredLeases(): Promise<void> {
@@ -310,6 +310,17 @@ export class TurnDaemonLifecycle {
this.status.paused = true;
this.errorPaused = true;
this.status.lastError = error instanceof Error ? error.message : 'Unknown command error.';
if (command.requestId && this.commandResponder?.publishCommandError) {
try {
await this.commandResponder.publishCommandError(command.requestId, error);
} catch (reportError) {
const reportMessage =
reportError instanceof Error
? reportError.message
: 'Unknown command failure reporting error.';
this.status.lastError = `${this.status.lastError} (failure report: ${reportMessage})`;
}
}
await this.hooks?.onRunError?.(error);
return;
}
+1
View File
@@ -33,6 +33,7 @@ export interface TurnDaemonCommandExecutionContext {
export interface TurnDaemonCommandResponder {
publishStatus(requestId: string, status: TurnDaemonStatus): Promise<void>;
publishCommandResult(requestId: string, result: TurnDaemonCommandResult): Promise<void>;
publishCommandError?(requestId: string, error: unknown): Promise<void>;
}
export type { Clock } from '@sammo-ts/common';
@@ -0,0 +1,93 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { isRecord } from '@sammo-ts/common';
import { isEventDomesticTraitKey } from '@sammo-ts/logic';
import { resolveWorkspaceRoot } from '../paths.js';
const DEFAULT_GENERAL_POOL_ROOT = path.resolve(resolveWorkspaceRoot(), 'resources', 'general-pool');
const SUPPORTED_POOL = 'SPoolUnderU30';
const EXPECTED_COLUMNS = [
'generalName',
'leadership',
'strength',
'intel',
'specialDomestic',
'dex',
'imgsvr',
'picture',
] as const;
export interface GeneralPoolSeedEntry {
uniqueName: string;
info: Record<string, unknown>;
}
export interface GeneralPoolLoaderOptions {
generalPoolRoot?: string;
}
const readPoolResource = async (filePath: string): Promise<unknown> =>
JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
const normalizePoolRow = (row: unknown, index: number): GeneralPoolSeedEntry => {
if (!Array.isArray(row) || row.length !== EXPECTED_COLUMNS.length) {
throw new Error(`General pool row ${index} does not match the expected ${EXPECTED_COLUMNS.length} columns.`);
}
const info = Object.fromEntries(EXPECTED_COLUMNS.map((column, columnIndex) => [column, row[columnIndex]]));
const uniqueName = info.generalName;
if (typeof uniqueName !== 'string' || uniqueName.length === 0) {
throw new Error(`General pool row ${index} has no generalName.`);
}
if (uniqueName.length > 20) {
throw new Error(`General pool row ${index} has a generalName longer than the select_pool key.`);
}
if (
!Number.isInteger(info.leadership) ||
!Number.isInteger(info.strength) ||
!Number.isInteger(info.intel) ||
typeof info.specialDomestic !== 'string' ||
!isEventDomesticTraitKey(info.specialDomestic) ||
!Array.isArray(info.dex) ||
info.dex.length !== 5 ||
info.dex.some((value) => typeof value !== 'number' || !Number.isInteger(value) || value < 0) ||
info.dex.reduce((sum, value) => sum + Number(value), 0) <= 0 ||
(info.imgsvr !== 0 && info.imgsvr !== 1) ||
typeof info.picture !== 'string'
) {
throw new Error(`General pool row ${index} contains invalid candidate data.`);
}
return {
uniqueName,
info: {
...info,
uniqueName,
},
};
};
export const loadGeneralPoolEntries = async (
poolName: string,
options?: GeneralPoolLoaderOptions
): Promise<GeneralPoolSeedEntry[]> => {
if (poolName !== SUPPORTED_POOL) {
throw new Error(`Unsupported general pool: ${poolName}.`);
}
const root = path.resolve(options?.generalPoolRoot ?? DEFAULT_GENERAL_POOL_ROOT);
const raw = await readPoolResource(path.resolve(root, `${poolName}.json`));
if (!isRecord(raw) || !Array.isArray(raw.columns) || !Array.isArray(raw.data)) {
throw new Error(`General pool ${poolName} is not a valid resource.`);
}
if (
raw.columns.length !== EXPECTED_COLUMNS.length ||
raw.columns.some((column, index) => column !== EXPECTED_COLUMNS[index])
) {
throw new Error(`General pool ${poolName} has an unexpected column contract.`);
}
const entries = raw.data.map(normalizePoolRow);
if (new Set(entries.map((entry) => entry.uniqueName)).size !== entries.length) {
throw new Error(`General pool ${poolName} contains duplicate unique names.`);
}
return entries;
};
+33 -6
View File
@@ -1,3 +1,5 @@
import { randomBytes } from 'node:crypto';
import { createGamePostgresConnector, type InputJsonValue, type TurnEngineEventCreateManyInput } from '@sammo-ts/infra';
import { asRecord } from '@sammo-ts/common';
import {
@@ -14,6 +16,8 @@ import type { ScenarioLoaderOptions } from './scenarioLoader.js';
import { loadScenarioDefinitionById } from './scenarioLoader.js';
import type { UnitSetLoaderOptions } from './unitSetLoader.js';
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
import type { GeneralPoolLoaderOptions } from './generalPoolLoader.js';
import { loadGeneralPoolEntries } from './generalPoolLoader.js';
import { applyInitialChangeCityEvents } from '../turn/monthlyChangeCityAction.js';
const DEFAULT_TICK_SECONDS = 120 * 60;
@@ -51,6 +55,7 @@ export interface ScenarioSeedOptions {
scenarioOptions?: ScenarioLoaderOptions;
mapOptions?: MapLoaderOptions;
unitSetOptions?: UnitSetLoaderOptions;
generalPoolOptions?: GeneralPoolLoaderOptions;
resetTables?: boolean;
now?: Date;
tickSeconds?: number;
@@ -209,6 +214,11 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
const scenarioDefinition = includeExtendedGeneral ? scenario : { ...scenario, generalsEx: [] };
const map = await loadMapDefinitionByName(scenario.config.environment.mapName, options.mapOptions);
const unitSet = await loadUnitSetDefinitionByName(scenario.config.environment.unitSet, options.unitSetOptions);
const targetGeneralPool =
typeof scenario.config.map.targetGeneralPool === 'string' ? scenario.config.map.targetGeneralPool : null;
const generalPoolEntries = targetGeneralPool
? await loadGeneralPoolEntries(targetGeneralPool, options.generalPoolOptions)
: [];
const { seed, warnings } = buildScenarioBootstrap({
scenario: scenarioDefinition,
@@ -271,10 +281,11 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
worldMeta.serverId = install.serverId.trim();
}
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV];
if (typeof integrationSeed === 'string' && integrationSeed.trim().length > 0) {
worldMeta.hiddenSeed = integrationSeed.trim();
}
const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV]?.trim();
worldMeta.hiddenSeed =
integrationSeed && integrationSeed.length > 0
? integrationSeed
: randomBytes(16).toString('hex');
if (install?.preopenAt) {
worldMeta.preopenAt = formatDateTime(install.preopenAt);
@@ -286,6 +297,8 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
options: install.autorunUser.options,
};
}
const archivedWorldMeta = { ...worldMeta };
delete archivedWorldMeta.hiddenSeed;
await connector.connect();
try {
@@ -297,6 +310,11 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
if (eventTableReady) {
await prisma.event.deleteMany();
}
await prisma.selectPoolEntry.deleteMany();
await prisma.generalTurn.deleteMany();
await prisma.generalTurnRevision.deleteMany();
await prisma.rankData.deleteMany();
await prisma.generalAccessLog.deleteMany();
await prisma.diplomacy.deleteMany();
await prisma.general.deleteMany();
await prisma.troop.deleteMany();
@@ -316,6 +334,15 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
},
});
if (generalPoolEntries.length > 0) {
await prisma.selectPoolEntry.createMany({
data: generalPoolEntries.map((entry) => ({
uniqueName: entry.uniqueName,
info: asJson(entry.info),
})),
});
}
if (typeof worldMeta.serverId === 'string' && worldMeta.serverId) {
await prisma.gameHistory.upsert({
where: { serverId: worldMeta.serverId },
@@ -332,7 +359,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
scenarioName: String(seed.scenarioMeta?.title ?? ''),
env: asJson({
config: scenarioConfig,
meta: worldMeta,
meta: archivedWorldMeta,
}),
},
update: {
@@ -347,7 +374,7 @@ export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Prom
scenarioName: String(seed.scenarioMeta?.title ?? ''),
env: asJson({
config: scenarioConfig,
meta: worldMeta,
meta: archivedWorldMeta,
}),
},
});
@@ -243,6 +243,28 @@ const zPatchGeneral = z.object({
}),
});
const zSelectPoolCreate = z
.object({
type: z.literal('selectPoolCreate'),
requestId: z.string().optional(),
userId: z.string().min(1),
ownerDisplayName: z.string().min(1),
uniqueName: z.string().min(1).max(20),
personality: z.string().min(1),
seedOwnerIdentity: z.union([z.string().min(1), zFiniteNumber]),
})
.strict();
const zSelectPoolReselect = z
.object({
type: z.literal('selectPoolReselect'),
requestId: z.string().optional(),
userId: z.string().min(1),
ownerDisplayName: z.string().min(1),
uniqueName: z.string().min(1).max(20),
})
.strict();
const zGetStatus = z.object({
type: z.literal('getStatus'),
requestId: z.string().optional(),
@@ -484,6 +506,22 @@ const normalizePatchGeneral: CommandNormalizer<'patchGeneral'> = (envelope) => {
return { ...command, requestId: envelope.requestId };
};
const normalizeSelectPoolCreate: CommandNormalizer<'selectPoolCreate'> = (envelope) => {
const command = parseWith(zSelectPoolCreate, envelope.command);
if (!command) {
return null;
}
return { ...command, requestId: envelope.requestId };
};
const normalizeSelectPoolReselect: CommandNormalizer<'selectPoolReselect'> = (envelope) => {
const command = parseWith(zSelectPoolReselect, envelope.command);
if (!command) {
return null;
}
return { ...command, requestId: envelope.requestId };
};
const normalizeGetStatus: CommandNormalizer<'getStatus'> = (envelope) => {
const command = parseWith(zGetStatus, envelope.command);
if (!command) {
@@ -547,6 +585,8 @@ const normalizers: CommandNormalizerMap = {
adjustGeneralMeta: normalizeAdjustGeneralMeta,
tournamentMatchResult: normalizeTournamentMatchResult,
patchGeneral: normalizePatchGeneral,
selectPoolCreate: normalizeSelectPoolCreate,
selectPoolReselect: normalizeSelectPoolReselect,
getStatus: normalizeGetStatus,
run: normalizeRun,
pause: normalizePause,
+15
View File
@@ -351,6 +351,8 @@ const buildGeneralUpdate = (
bornYear: general.bornYear,
deadYear: general.deadYear,
picture: general.picture ?? null,
imageServer: general.imageServer ?? 0,
startAge: general.startAge ?? general.age,
npcState: general.npcState,
horseCode: toCode(general.role.items.horse),
weaponCode: toCode(general.role.items.weapon),
@@ -369,6 +371,7 @@ const buildGeneralCreate = (
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
): TurnEngineGeneralCreateManyInput => ({
id: general.id,
userId: general.userId ?? null,
name: general.name,
nationId: general.nationId,
cityId: general.cityId,
@@ -392,6 +395,8 @@ const buildGeneralCreate = (
bornYear: general.bornYear,
deadYear: general.deadYear,
picture: general.picture ?? null,
imageServer: general.imageServer ?? 0,
startAge: general.startAge ?? general.age,
horseCode: toCode(general.role.items.horse),
weaponCode: toCode(general.role.items.weapon),
bookCode: toCode(general.role.items.book),
@@ -799,6 +804,14 @@ export const createDatabaseTurnHooks = async (
}
if (deletedGenerals.length > 0) {
await prisma.selectPoolEntry.updateMany({
where: { generalId: { in: deletedGenerals } },
data: {
generalId: null,
ownerUserId: null,
reservedUntil: null,
},
});
if (prisma.generalTurnRevision) {
await prisma.generalTurnRevision.deleteMany({
where: { generalId: { in: deletedGenerals } },
@@ -969,6 +982,8 @@ export const createDatabaseTurnHooks = async (
result: asJson(commandCompletion.result),
completedAt: new Date(),
error: null,
lockedBy: null,
leaseUntil: null,
},
});
}
+3 -4
View File
@@ -881,10 +881,9 @@ export class InMemoryTurnWorld {
getNextGeneralId(): number {
const meta = this.state.meta as Record<string, unknown>;
let lastId = (meta.lastGeneralId as number | undefined) ?? 0;
if (lastId === 0) {
const currentIds = Array.from(this.generals.keys());
lastId = currentIds.length > 0 ? Math.max(...currentIds) : 0;
}
const currentIds = Array.from(this.generals.keys());
const currentMaxId = currentIds.length > 0 ? Math.max(...currentIds) : 0;
lastId = Math.max(lastId, currentMaxId);
const nextId = lastId + 1;
this.state = {
@@ -0,0 +1,889 @@
import { z } from 'zod';
import {
asNumber,
asRecord,
JosaUtil,
LiteHashDRBG,
RandUtil,
} from '@sammo-ts/common';
import { GamePrisma, LogCategory, LogScope } from '@sammo-ts/infra';
import {
EventDomesticTraitLoader,
isEventDomesticTraitKey,
isPersonalityTraitKey,
PERSONALITY_TRAIT_KEYS,
simpleSerialize,
} from '@sammo-ts/logic';
import type { DatabaseClient, GamePrisma as GamePrismaTypes } from '@sammo-ts/infra';
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
import type { TurnGeneral } from './types.js';
type WorldStateRow = GamePrismaTypes.WorldStateGetPayload<Record<string, never>>;
export type SelectPoolErrorCode =
| 'BAD_REQUEST'
| 'PRECONDITION_FAILED'
| 'CONFLICT'
| 'INTERNAL_SERVER_ERROR';
export class SelectPoolError extends Error {
constructor(
readonly code: SelectPoolErrorCode,
message: string
) {
super(message);
this.name = 'SelectPoolError';
}
}
const SUPPORTED_POOL = 'SPoolUnderU30';
const RESERVATION_COUNT = 14;
const RESERVATION_TURN_MULTIPLIER = 2;
const RESELECTION_TURN_MULTIPLIER = 12;
const DEFAULT_MAX_GENERAL = 500;
const DEFAULT_CREW_TYPE_ID = 1100;
const MAX_GENERAL_TURNS = 30;
const DEFAULT_TURN_ACTION = '휴식';
const LEGACY_TIMEZONE_OFFSET_MS = 9 * 60 * 60 * 1000;
const zCandidateInfo = z.object({
uniqueName: z.string().min(1),
generalName: z.string().min(1),
leadership: z.number().int(),
strength: z.number().int(),
intel: z.number().int(),
specialDomestic: z.string().min(1),
specialWar: z.string().min(1).optional(),
ego: z.string().min(1).optional(),
experience: z.number().int().optional(),
dedication: z.number().int().optional(),
dex: z.tuple([z.number(), z.number(), z.number(), z.number(), z.number()]),
imgsvr: z.union([z.literal(0), z.literal(1)]),
picture: z.string(),
});
export type SelectPoolCandidateInfo = z.infer<typeof zCandidateInfo>;
interface SelectPoolRow {
id: number;
uniqueName: string;
ownerUserId: string | null;
generalId: number | null;
reservedUntil: Date | null;
info: unknown;
}
export interface SelectPoolCandidateDto {
uniqueName: string;
generalName: string;
leadership: number;
strength: number;
intel: number;
specialDomestic: string;
specialDomesticName: string;
specialDomesticInfo: string;
specialWar: string | null;
ego: string | null;
dex: [number, number, number, number, number];
imageServer: 0 | 1;
picture: string;
}
export interface SelectPoolReservationDto {
poolName: typeof SUPPORTED_POOL;
hasGeneral: boolean;
validUntil: string;
candidates: SelectPoolCandidateDto[];
}
const fail = (
code: SelectPoolErrorCode,
message: string
): never => {
throw new SelectPoolError(code, message);
};
const resolvePoolName = (worldState: WorldStateRow): string | null => {
const config = asRecord(worldState.config);
const map = asRecord(config.map);
return typeof map.targetGeneralPool === 'string' ? map.targetGeneralPool : null;
};
const resolvePoolAllowOptions = (worldState: WorldStateRow): string[] => {
const map = asRecord(asRecord(worldState.config).map);
return Array.isArray(map.generalPoolAllowOption)
? map.generalPoolAllowOption.filter((value): value is string => typeof value === 'string')
: [];
};
const resolveTurnTermMinutes = (worldState: WorldStateRow): number => {
const config = asRecord(worldState.config);
const configured = asNumber(config.turnTermMinutes, Math.round(worldState.tickSeconds / 60));
return Math.max(1, Math.abs(Math.trunc(configured)));
};
export const isSelectionPoolWorld = (worldState: WorldStateRow): boolean => {
const config = asRecord(worldState.config);
return asNumber(config.npcMode, 0) === 2 && resolvePoolName(worldState) === SUPPORTED_POOL;
};
export const resolveSelectionMaxGeneral = (worldState: WorldStateRow): number => {
const config = asRecord(worldState.config);
const configConst = asRecord(config.const);
return Math.max(
0,
Math.floor(
asNumber(
config.maxGeneral ??
configConst.defaultMaxGeneral ??
configConst.maxGeneral,
DEFAULT_MAX_GENERAL
)
)
);
};
const requirePoolWorld = (worldState: WorldStateRow): void => {
if (!isSelectionPoolWorld(worldState)) {
fail('PRECONDITION_FAILED', '선택 가능한 서버가 아닙니다');
}
};
const parseCandidate = (row: Pick<SelectPoolRow, 'uniqueName' | 'info'>): SelectPoolCandidateInfo => {
const info = zCandidateInfo.safeParse(row.info);
if (!info.success || !info.data) {
throw new SelectPoolError(
'INTERNAL_SERVER_ERROR',
`장수 선택 후보 정보가 올바르지 않습니다: ${row.uniqueName}`
);
}
const candidate = info.data;
if (candidate.uniqueName !== row.uniqueName) {
throw new SelectPoolError(
'INTERNAL_SERVER_ERROR',
`장수 선택 후보 정보가 올바르지 않습니다: ${row.uniqueName}`
);
}
return candidate;
};
const candidateWeight = (candidate: SelectPoolCandidateInfo): number =>
candidate.dex.reduce((sum, value) => sum + value, 0);
const eventDomesticTraitLoader = new EventDomesticTraitLoader();
const toCandidateDto = async (
candidate: SelectPoolCandidateInfo
): Promise<SelectPoolCandidateDto> => {
const trait = isEventDomesticTraitKey(candidate.specialDomestic)
? await eventDomesticTraitLoader.load(candidate.specialDomestic)
: null;
return {
uniqueName: candidate.uniqueName,
generalName: candidate.generalName,
leadership: candidate.leadership,
strength: candidate.strength,
intel: candidate.intel,
specialDomestic: candidate.specialDomestic,
specialDomesticName:
trait?.name ?? candidate.specialDomestic.replace(/^che_event_/, ''),
specialDomesticInfo: trait?.info ?? '',
specialWar: candidate.specialWar ?? null,
ego: candidate.ego ?? null,
dex: candidate.dex,
imageServer: candidate.imgsvr,
picture: candidate.picture,
};
};
const toReservationDto = (
rows: Array<Pick<SelectPoolRow, 'id' | 'uniqueName' | 'reservedUntil' | 'info'>>,
hasGeneral: boolean
): Promise<SelectPoolReservationDto> => {
const validUntil = rows[0]?.reservedUntil;
if (!validUntil) {
throw new SelectPoolError(
'INTERNAL_SERVER_ERROR',
'장수 선택 후보의 유효기간이 없습니다.'
);
}
const expiresAt = validUntil;
const sorted = rows
.map((row) => ({ id: row.id, info: parseCandidate(row) }))
.sort(
(left, right) =>
candidateWeight(left.info) - candidateWeight(right.info) || left.id - right.id
);
return Promise.all(sorted.map((entry) => toCandidateDto(entry.info))).then((candidates) => ({
poolName: SUPPORTED_POOL,
hasGeneral,
validUntil: expiresAt.toISOString(),
candidates,
}));
};
const formatLegacySeedTime = (value: Date): string => {
const pad = (part: number): string => String(part).padStart(2, '0');
const koreaTime = new Date(value.getTime() + LEGACY_TIMEZONE_OFFSET_MS);
return `${koreaTime.getUTCFullYear()}-${pad(koreaTime.getUTCMonth() + 1)}-${pad(
koreaTime.getUTCDate()
)} ${pad(koreaTime.getUTCHours())}:${pad(koreaTime.getUTCMinutes())}:${pad(
koreaTime.getUTCSeconds()
)}`;
};
export const buildSelectPoolSeed = (
hiddenSeed: string | number,
ownerIdentity: string | number,
now: Date
): string => simpleSerialize(hiddenSeed, 'selectPool', ownerIdentity, formatLegacySeedTime(now));
export const claimWeightedSelectionCandidates = async <T extends { id: number }>(options: {
weighted: [T, number][];
rng: RandUtil;
count: number;
claim(candidate: T): Promise<boolean>;
onDraw?(candidate: T): void;
maxAttempts?: number;
}): Promise<T[]> => {
const claimed: T[] = [];
const claimedIds = new Set<number>();
const maxAttempts = options.maxAttempts ?? Math.max(options.weighted.length * 8, 1000);
let attempts = 0;
while (claimed.length < options.count && attempts < maxAttempts) {
attempts += 1;
const candidate = options.rng.choiceUsingWeightPair(options.weighted);
options.onDraw?.(candidate);
if (claimedIds.has(candidate.id) || !(await options.claim(candidate))) {
continue;
}
claimedIds.add(candidate.id);
claimed.push(candidate);
}
return claimed;
};
const readNextChangeAt = (generalMeta: unknown): Date | null => {
const meta = asRecord(generalMeta);
const raw = meta.next_change ?? meta.nextChangeAt;
if (typeof raw !== 'string') {
return null;
}
const parsed = new Date(raw);
return Number.isNaN(parsed.getTime()) ? null : parsed;
};
const getWorldHiddenSeed = (worldState: WorldStateRow): string | number => {
const meta = asRecord(worldState.meta);
const value = meta.hiddenSeed ?? meta.seed;
return typeof value === 'string' || typeof value === 'number'
? value
: fail('INTERNAL_SERVER_ERROR', '장수 선택 비밀 seed가 설정되지 않았습니다.');
};
const lockSelectionUser = async (db: DatabaseClient, userId: string): Promise<void> => {
await db.$executeRaw(
GamePrisma.sql`SELECT pg_advisory_xact_lock(hashtextextended(${`select_pool:${userId}`}, 903))`
);
};
const requireSelectionToken = async (
db: DatabaseClient,
userId: string,
uniqueName: string,
now: Date
): Promise<SelectPoolRow> => {
const token = await db.selectPoolEntry.findFirst({
where: {
ownerUserId: userId,
uniqueName,
reservedUntil: { gte: now },
generalId: null,
},
});
if (!token) {
fail('PRECONDITION_FAILED', '유효한 장수 목록이 없습니다.');
}
return token as SelectPoolRow;
};
export const reserveSelectionPool = async (options: {
db: DatabaseClient;
worldState: WorldStateRow;
userId: string;
now?: Date;
seedOwnerIdentity?: string | number;
}): Promise<SelectPoolReservationDto> => {
const { db, worldState, userId } = options;
requirePoolWorld(worldState);
const now = options.now ?? new Date();
await lockSelectionUser(db, userId);
const general = await db.general.findFirst({
where: { userId },
select: { id: true, meta: true },
});
const nextChangeAt = general ? readNextChangeAt(general.meta) : null;
if (nextChangeAt && nextChangeAt.getTime() > now.getTime()) {
fail('PRECONDITION_FAILED', '아직 다시 고를 수 없습니다');
}
const existing = await db.selectPoolEntry.findMany({
where: {
ownerUserId: userId,
reservedUntil: { gte: now },
generalId: null,
},
orderBy: { id: 'asc' },
});
if (existing.length > 0) {
return toReservationDto(existing as SelectPoolRow[], Boolean(general));
}
await db.selectPoolEntry.updateMany({
where: {
reservedUntil: { lt: now },
generalId: null,
},
data: {
ownerUserId: null,
reservedUntil: null,
},
});
const available = (await db.selectPoolEntry.findMany({
where: {
ownerUserId: null,
reservedUntil: null,
generalId: null,
},
orderBy: { id: 'asc' },
})) as SelectPoolRow[];
if (available.length < RESERVATION_COUNT) {
fail('PRECONDITION_FAILED', 'pool 부족');
}
const rng = new RandUtil(
new LiteHashDRBG(
buildSelectPoolSeed(
getWorldHiddenSeed(worldState),
options.seedOwnerIdentity ?? userId,
now
)
)
);
const weighted = available.map((row) => [row, candidateWeight(parseCandidate(row))] as [SelectPoolRow, number]);
const reservedUntil = new Date(
now.getTime() + resolveTurnTermMinutes(worldState) * RESERVATION_TURN_MULTIPLIER * 60_000
);
const selected = await claimWeightedSelectionCandidates({
weighted,
rng,
count: RESERVATION_COUNT,
claim: async (candidate) => {
const claimed = await db.selectPoolEntry.updateMany({
where: {
id: candidate.id,
ownerUserId: null,
reservedUntil: null,
generalId: null,
},
data: {
ownerUserId: userId,
reservedUntil,
},
});
return claimed.count > 0;
},
});
const reserved = selected.map((candidate) => ({
...candidate,
ownerUserId: userId,
reservedUntil,
}));
if (reserved.length !== RESERVATION_COUNT) {
fail('CONFLICT', '장수 선택 후보를 예약하지 못했습니다. 다시 시도해 주세요.');
}
return toReservationDto(reserved, Boolean(general));
};
const lockSelectionMutationTables = async (db: DatabaseClient): Promise<void> => {
await db.$executeRaw(GamePrisma.sql`LOCK TABLE "general" IN SHARE ROW EXCLUSIVE MODE`);
await db.$executeRaw(GamePrisma.sql`LOCK TABLE "select_pool" IN SHARE ROW EXCLUSIVE MODE`);
};
const assertGeneralIdSnapshotMatches = async (
db: DatabaseClient,
world: InMemoryTurnWorld
): Promise<void> => {
const persistedIds = (
await db.general.findMany({
select: { id: true },
orderBy: { id: 'asc' },
})
).map(({ id }) => id);
const runtimeIds = world
.listGenerals()
.map(({ id }) => id)
.sort((left, right) => left - right);
if (
persistedIds.length !== runtimeIds.length ||
persistedIds.some((id, index) => id !== runtimeIds[index])
) {
throw new Error(
'DB와 턴 데몬의 장수 번호 목록이 일치하지 않아 장수를 생성할 수 없습니다.'
);
}
};
const clearUnusedReservations = async (db: DatabaseClient, userId: string, now: Date): Promise<void> => {
await db.selectPoolEntry.updateMany({
where: {
generalId: null,
OR: [{ ownerUserId: userId }, { reservedUntil: { lt: now } }],
},
data: {
ownerUserId: null,
reservedUntil: null,
},
});
};
const resolveSpecialityAges = (worldState: WorldStateRow, age: number): { domestic: number; war: number } => {
const configConst = asRecord(asRecord(worldState.config).const);
const retirementYear = asNumber(configConst.retirementYear, 80);
const scenarioMeta = asRecord(asRecord(worldState.meta).scenarioMeta);
const startYear = asNumber(scenarioMeta.startYear, worldState.currentYear);
const relativeYear = Math.max(worldState.currentYear - startYear, 0);
const build = (divisor: number): number =>
Math.max(Math.round((retirementYear - age) / divisor - relativeYear / 2), 3) + age;
return { domestic: build(12), war: build(6) };
};
const resolveRandomPersonality = (
worldState: WorldStateRow,
ownerIdentity: string | number,
uniqueName: string
): string =>
new RandUtil(
new LiteHashDRBG(
simpleSerialize(
getWorldHiddenSeed(worldState),
'selectPickedGeneralPersonality',
ownerIdentity,
uniqueName
)
)
).choice([...PERSONALITY_TRAIT_KEYS]);
const resolveSelectedPersonality = (
worldState: WorldStateRow,
ownerIdentity: string | number,
uniqueName: string,
requested: string
): string => {
if (!resolvePoolAllowOptions(worldState).includes('ego')) {
return 'None';
}
if (requested === 'Random') {
return resolveRandomPersonality(worldState, ownerIdentity, uniqueName);
}
if (!isPersonalityTraitKey(requested)) {
fail('BAD_REQUEST', '올바르지 않은 성격입니다.');
}
return requested;
};
const resolvePoolRng = (
worldState: WorldStateRow,
ownerIdentity: string | number,
uniqueName: string
): RandUtil =>
new RandUtil(
new LiteHashDRBG(
simpleSerialize(
getWorldHiddenSeed(worldState),
'selectPickedGeneral',
ownerIdentity,
uniqueName
)
)
);
const resolveTurnTimeBase = (worldState: WorldStateRow, now: Date): Date => {
const raw = asRecord(worldState.meta).turntime;
if (typeof raw === 'string') {
const parsed = new Date(raw);
if (!Number.isNaN(parsed.getTime())) {
return parsed;
}
}
return now;
};
const buildInitialTurnTime = (rng: RandUtil, worldState: WorldStateRow, now: Date): Date => {
const termSeconds = resolveTurnTermMinutes(worldState) * 60;
const seconds = rng.nextRangeInt(0, termSeconds - 1);
const microseconds = rng.nextRangeInt(0, 999_999);
return new Date(resolveTurnTimeBase(worldState, now).getTime() + seconds * 1000 + microseconds / 1000);
};
const appendSelectionLogs = async (options: {
db: DatabaseClient;
worldState: WorldStateRow;
generalId: number;
ownerUserId: string;
generalText: string;
globalText: string;
}): Promise<void> => {
const common = {
year: options.worldState.currentYear,
month: options.worldState.currentMonth,
nationId: null,
userId: null,
meta: { ownerUserId: options.ownerUserId },
};
await options.db.logEntry.createMany({
data: [
{
...common,
scope: LogScope.GENERAL,
category: LogCategory.HISTORY,
generalId: options.generalId,
text: options.generalText,
},
{
...common,
scope: LogScope.SYSTEM,
category: LogCategory.ACTION,
generalId: null,
text: options.globalText,
},
],
});
};
export const createGeneralFromSelectionPool = async (options: {
db: DatabaseClient;
world: InMemoryTurnWorld;
worldState: WorldStateRow;
userId: string;
ownerDisplayName: string;
uniqueName: string;
personality: string;
now?: Date;
seedOwnerIdentity?: string | number;
}): Promise<{ ok: true; generalId: number }> => {
const { db, world, worldState, userId, ownerDisplayName, uniqueName } = options;
requirePoolWorld(worldState);
const now = options.now ?? new Date();
await lockSelectionUser(db, userId);
await lockSelectionMutationTables(db);
await assertGeneralIdSnapshotMatches(db, world);
if (
world.listGenerals().some((general) => general.userId === userId) ||
(await db.general.findFirst({ where: { userId }, select: { id: true } }))
) {
fail('PRECONDITION_FAILED', '이미 장수를 생성했습니다.');
}
const token = await requireSelectionToken(db, userId, uniqueName, now);
const info = parseCandidate(token);
const config = asRecord(worldState.config);
const configConst = asRecord(config.const);
const maxGeneral = resolveSelectionMaxGeneral(worldState);
const activeCount = await db.general.count({ where: { npcState: { lt: 2 } } });
if (activeCount >= maxGeneral) {
fail('PRECONDITION_FAILED', '더 이상 등록 할 수 없습니다.');
}
const seedOwnerIdentity = options.seedOwnerIdentity ?? userId;
const rng = resolvePoolRng(worldState, seedOwnerIdentity, uniqueName);
const affinity = rng.nextRangeInt(1, 150);
const cities = await db.city.findMany({ select: { id: true, name: true }, orderBy: { id: 'asc' } });
if (cities.length === 0) {
fail('PRECONDITION_FAILED', '생성 가능한 도시가 없습니다.');
}
const city = rng.choice(cities);
const turnTime = buildInitialTurnTime(rng, worldState, now);
const age = 20;
const specialityAges = resolveSpecialityAges(worldState, age);
const nextChangeAt = new Date(
now.getTime() + resolveTurnTermMinutes(worldState) * RESELECTION_TURN_MULTIPLIER * 60_000
);
const showImgLevel = asNumber(config.showImgLevel, 0);
const picture = showImgLevel >= 3 ? info.picture : 'default.jpg';
const defaultSpecialWar =
typeof configConst.defaultSpecialWar === 'string' ? configConst.defaultSpecialWar : 'None';
const personality = resolveSelectedPersonality(
worldState,
seedOwnerIdentity,
uniqueName,
options.personality
);
// 모든 사용자 입력과 DB 선조건을 검증한 뒤에만 allocator를 변경한다.
// SelectPoolError는 정상 command 결과로 commit되므로 이보다 먼저
// getNextGeneralId()를 호출하면 실패한 요청도 lastGeneralId를 소비한다.
const generalId = world.getNextGeneralId();
const general: TurnGeneral = {
id: generalId,
userId,
name: info.generalName,
nationId: 0,
cityId: city.id,
troopId: 0,
npcState: 0,
affinity,
bornYear: worldState.currentYear - age,
deadYear: worldState.currentYear + 60,
picture,
imageServer: info.imgsvr,
stats: {
leadership: info.leadership,
strength: info.strength,
intelligence: info.intel,
},
experience: info.experience ?? age * 100,
dedication: info.dedication ?? age * 100,
officerLevel: 0,
injury: 0,
gold: 1000,
rice: 1000,
crew: 0,
crewTypeId: DEFAULT_CREW_TYPE_ID,
train: 0,
atmos: 0,
turnTime,
age,
startAge: age,
role: {
personality,
specialDomestic: info.specialDomestic,
specialWar: info.specialWar ?? defaultSpecialWar,
items: {
horse: null,
weapon: null,
book: null,
item: null,
},
},
triggerState: {
flags: {},
counters: {},
modifiers: {},
meta: {},
},
lastTurn: { command: DEFAULT_TURN_ACTION },
penalty: {},
refreshScoreTotal: 0,
meta: {
createdBy: 'select_pool',
ownerName: ownerDisplayName,
owner_name: ownerDisplayName,
killturn: 5,
specage: specialityAges.domestic,
specage2: specialityAges.war,
dex1: info.dex[0],
dex2: info.dex[1],
dex3: info.dex[2],
dex4: info.dex[3],
dex5: info.dex[4],
next_change: nextChangeAt.toISOString(),
nextChangeAt: nextChangeAt.toISOString(),
npc_org: 0,
},
};
if (!world.addGeneral(general)) {
throw new Error(`장수 번호 ${generalId}를 할당할 수 없습니다.`);
}
await db.generalTurn.createMany({
data: Array.from({ length: MAX_GENERAL_TURNS }, (_, turnIdx) => ({
generalId,
turnIdx,
actionCode: DEFAULT_TURN_ACTION,
arg: {},
})),
});
await db.generalTurnRevision.create({
data: {
generalId,
revision: 0,
},
});
const occupied = await db.selectPoolEntry.updateMany({
where: {
id: token.id,
ownerUserId: userId,
reservedUntil: { gte: now },
generalId: null,
},
data: {
generalId,
ownerUserId: null,
reservedUntil: null,
},
});
if (occupied.count === 0) {
throw new Error('장수 등록 중 선택 후보 점유에 실패했습니다.');
}
await db.generalAccessLog.upsert({
where: { generalId },
update: { userId, lastRefresh: now },
create: { generalId, userId, lastRefresh: now },
});
await clearUnusedReservations(db, userId, now);
const ownerJosaYi = JosaUtil.pick(ownerDisplayName, '이');
const generalJosaRo = JosaUtil.pick(info.generalName, '로');
await appendSelectionLogs({
db,
worldState,
generalId,
ownerUserId: userId,
generalText: `<Y>${info.generalName}</>, <G>${city.name}</>에서 등장`,
globalText: `<G><b>${city.name}</b></>에서 <Y>${ownerDisplayName}</>${ownerJosaYi} <Y>${info.generalName}</>${generalJosaRo} 등장합니다.`,
});
return { ok: true, generalId };
};
export const reselectGeneralFromSelectionPool = async (options: {
db: DatabaseClient;
world: InMemoryTurnWorld;
worldState: WorldStateRow;
userId: string;
ownerDisplayName: string;
uniqueName: string;
now?: Date;
}): Promise<{ ok: true; generalId: number }> => {
const { db, world, worldState, userId, ownerDisplayName, uniqueName } = options;
requirePoolWorld(worldState);
const now = options.now ?? new Date();
await lockSelectionUser(db, userId);
await lockSelectionMutationTables(db);
const persistedGeneral = await db.general.findFirst({ where: { userId } });
const general = world.listGenerals().find((candidate) => candidate.userId === userId);
if (!persistedGeneral || !general) {
throw new SelectPoolError(
'PRECONDITION_FAILED',
'장수가 생성하지 않았습니다. 이미 사망하지 않았는지 확인해보세요.'
);
}
if (persistedGeneral.id !== general.id) {
fail('INTERNAL_SERVER_ERROR', 'DB와 턴 데몬의 장수 소유 정보가 일치하지 않습니다.');
}
const nextChangeAt = readNextChangeAt(general.meta);
if (nextChangeAt && nextChangeAt.getTime() > now.getTime()) {
fail('PRECONDITION_FAILED', '아직 다시 고를 수 없습니다');
}
const token = await requireSelectionToken(db, userId, uniqueName, now);
const info = parseCandidate(token);
const provisionalGeneralId = -general.id;
const claimed = await db.selectPoolEntry.updateMany({
where: {
id: token.id,
ownerUserId: userId,
reservedUntil: { gte: now },
generalId: null,
},
data: {
generalId: provisionalGeneralId,
ownerUserId: null,
reservedUntil: null,
},
});
if (claimed.count === 0) {
throw new Error('장수 재선택 중 선택 후보 점유에 실패했습니다.');
}
await db.selectPoolEntry.updateMany({
where: { generalId: general.id },
data: { generalId: null, ownerUserId: null, reservedUntil: null },
});
const finalized = await db.selectPoolEntry.updateMany({
where: {
id: token.id,
generalId: provisionalGeneralId,
},
data: {
generalId: general.id,
},
});
if (finalized.count === 0) {
throw new Error('장수 재선택 중 선택 후보 확정에 실패했습니다.');
}
const currentMeta = asRecord(general.meta);
const cooldown = new Date(
now.getTime() + resolveTurnTermMinutes(worldState) * RESELECTION_TURN_MULTIPLIER * 60_000
);
const updatedMeta = {
...currentMeta,
ownerName: ownerDisplayName,
owner_name: ownerDisplayName,
dex1: info.dex[0],
dex2: info.dex[1],
dex3: info.dex[2],
dex4: info.dex[3],
dex5: info.dex[4],
next_change: cooldown.toISOString(),
nextChangeAt: cooldown.toISOString(),
};
const updated = world.updateGeneral(general.id, {
name: info.generalName,
stats: {
leadership: info.leadership,
strength: info.strength,
intelligence: info.intel,
},
role: {
...general.role,
personality: info.ego ?? general.role.personality,
specialDomestic: info.specialDomestic,
specialWar: info.specialWar ?? general.role.specialWar,
},
picture: info.picture,
imageServer: info.imgsvr,
meta: updatedMeta as unknown as TurnGeneral['meta'],
});
if (!updated) {
throw new Error('턴 데몬에서 장수 정보를 갱신하지 못했습니다.');
}
await clearUnusedReservations(db, userId, now);
const ownerJosaYi = JosaUtil.pick(ownerDisplayName, '이');
const generalJosaRo = JosaUtil.pick(info.generalName, '로');
await appendSelectionLogs({
db,
worldState,
generalId: general.id,
ownerUserId: userId,
generalText: `장수를 <Y>${general.name}</>에서 <Y>${info.generalName}</>${generalJosaRo} 변경`,
globalText: `<Y>${ownerDisplayName}</>${ownerJosaYi} 장수를 <Y>${general.name}</>에서 <Y>${info.generalName}</>${generalJosaRo} 변경합니다.`,
});
return { ok: true, generalId: general.id };
};
export const getSelectionPoolStatus = async (
db: DatabaseClient,
worldState: WorldStateRow,
userId: string
): Promise<{
enabled: boolean;
poolName: string | null;
allowOptions: string[];
hasGeneral: boolean;
nextChangeAt: string | null;
}> => {
const poolName = resolvePoolName(worldState);
const enabled = isSelectionPoolWorld(worldState);
const general = await db.general.findFirst({ where: { userId }, select: { meta: true } });
return {
enabled,
poolName,
allowOptions: resolvePoolAllowOptions(worldState),
hasGeneral: Boolean(general),
nextChangeAt: general ? readNextChangeAt(general.meta)?.toISOString() ?? null : null,
};
};
+1
View File
@@ -27,6 +27,7 @@ export interface TurnGeneral extends General {
deadYear?: number;
affinity?: number | null;
picture?: string | null;
imageServer?: number;
turnTime: Date;
recentWarTime?: Date | null;
lastTurn?: GeneralLastTurn;
+118 -1
View File
@@ -4,7 +4,7 @@ import type {
TurnDaemonCommandExecutionContext,
TurnDaemonCommandResult,
} from '../lifecycle/types.js';
import { GamePrisma } from '@sammo-ts/infra';
import { GamePrisma, type DatabaseClient } from '@sammo-ts/infra';
import { asRecord, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import {
LogCategory,
@@ -40,6 +40,11 @@ import {
IMMEDIATE_TROOP_JOIN_MOVE_HANDLER,
LEGACY_TROOP_JOIN_EVENT,
} from './scenarioStaticEvents.js';
import {
createGeneralFromSelectionPool,
reselectGeneralFromSelectionPool,
SelectPoolError,
} from './selectPoolService.js';
let itemRegistryPromise: Promise<Map<string, ItemModule>> | null = null;
@@ -117,6 +122,108 @@ interface CommandHandlerContext {
tournamentRewardFinalizer?: TournamentRewardFinalizer;
}
const requireCommandDatabase = (ctx: CommandHandlerContext): DatabaseClient => {
if (!ctx.commandDb) {
throw new Error('ENGINE mutation transaction is required for selection-pool commands.');
}
return ctx.commandDb as unknown as DatabaseClient;
};
const resolveCommandAcceptedAt = async (
db: DatabaseClient,
command: Extract<TurnDaemonCommand, { type: 'selectPoolCreate' | 'selectPoolReselect' }>
): Promise<Date> => {
if (!command.requestId) {
throw new Error(`${command.type} requestId is required.`);
}
const event = await db.inputEvent.findUnique({
where: { requestId: command.requestId },
select: { createdAt: true },
});
if (!event) {
throw new Error(`ENGINE input event ${command.requestId} is missing.`);
}
return event.createdAt;
};
async function handleSelectPoolCreate(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'selectPoolCreate' }>
): Promise<TurnDaemonCommandResult> {
const db = requireCommandDatabase(ctx);
const worldState = await db.worldState.findUnique({
where: { id: ctx.world.getState().id },
});
if (!worldState) {
throw new Error('Selection-pool world state is missing.');
}
const acceptedAt = await resolveCommandAcceptedAt(db, command);
try {
return {
type: 'selectPoolCreate',
...(await createGeneralFromSelectionPool({
db,
world: ctx.world,
worldState,
userId: command.userId,
ownerDisplayName: command.ownerDisplayName,
uniqueName: command.uniqueName,
personality: command.personality,
seedOwnerIdentity: command.seedOwnerIdentity,
now: acceptedAt,
})),
};
} catch (error) {
if (error instanceof SelectPoolError) {
return {
type: 'selectPoolCreate',
ok: false,
code: error.code,
reason: error.message,
};
}
throw error;
}
}
async function handleSelectPoolReselect(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'selectPoolReselect' }>
): Promise<TurnDaemonCommandResult> {
const db = requireCommandDatabase(ctx);
const worldState = await db.worldState.findUnique({
where: { id: ctx.world.getState().id },
});
if (!worldState) {
throw new Error('Selection-pool world state is missing.');
}
const acceptedAt = await resolveCommandAcceptedAt(db, command);
try {
return {
type: 'selectPoolReselect',
...(await reselectGeneralFromSelectionPool({
db,
world: ctx.world,
worldState,
userId: command.userId,
ownerDisplayName: command.ownerDisplayName,
uniqueName: command.uniqueName,
now: acceptedAt,
})),
};
} catch (error) {
if (error instanceof SelectPoolError) {
return {
type: 'selectPoolReselect',
ok: false,
code: error.code,
reason: error.message,
};
}
throw error;
}
}
interface AuctionFinalizer {
finalize(auctionId: number, db?: GamePrisma.TransactionClient): Promise<TurnDaemonCommandResult>;
}
@@ -1845,6 +1952,16 @@ export const createTurnDaemonCommandHandler = (options: {
handleTournamentMatchResult(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentMatchResult' }>),
patchGeneral: (command) =>
handlePatchGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'patchGeneral' }>),
selectPoolCreate: (command) =>
handleSelectPoolCreate(
ctx,
command as Extract<TurnDaemonCommand, { type: 'selectPoolCreate' }>
),
selectPoolReselect: (command) =>
handleSelectPoolReselect(
ctx,
command as Extract<TurnDaemonCommand, { type: 'selectPoolReselect' }>
),
shiftSchedule: (command) =>
handleShiftSchedule(ctx, command as Extract<TurnDaemonCommand, { type: 'shiftSchedule' }>),
};
+1
View File
@@ -237,6 +237,7 @@ const mapGeneralRow = (
deadYear: row.deadYear,
affinity: row.affinity,
picture: row.picture,
imageServer: row.imageServer,
triggerState: {
flags: {},
counters: {},
@@ -93,4 +93,48 @@ integration('database command queue', () => {
lockedBy: 'active-worker',
});
});
it('retries an owned command twice and then records a terminal failure', async () => {
const requestId = 'integration:engine:bounded-failure';
await db.inputEvent.create({
data: {
requestId,
target: 'ENGINE',
eventType: 'vacation',
payload: {
type: 'vacation',
requestId,
generalId: 10,
} as GamePrisma.InputJsonValue,
},
});
const owner = new DatabaseTurnDaemonCommandQueue(db);
const stale = new DatabaseTurnDaemonCommandQueue(db);
for (const attempt of [1, 2, 3]) {
await expect(owner.drain()).resolves.toEqual([
{ type: 'vacation', requestId, generalId: 10 },
]);
await stale.publishCommandError(requestId, new Error('stale worker failure'));
await expect(
db.inputEvent.findUniqueOrThrow({ where: { requestId } })
).resolves.toMatchObject({
status: 'PROCESSING',
attempts: attempt,
});
await owner.publishCommandError(requestId, new Error('injected command failure'));
await expect(
db.inputEvent.findUniqueOrThrow({ where: { requestId } })
).resolves.toMatchObject({
status: attempt < 3 ? 'PENDING' : 'FAILED',
attempts: attempt,
error: 'injected command failure',
lockedBy: null,
leaseUntil: null,
});
}
await expect(owner.drain()).resolves.toEqual([]);
});
});
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { loadGeneralPoolEntries } from '../src/scenario/generalPoolLoader.js';
describe('SPoolUnderU30 resource', () => {
it('preserves the Ref UnderS30 row contract and ordering', async () => {
const entries = await loadGeneralPoolEntries('SPoolUnderU30');
const weights = entries.map((entry) => {
const dex = entry.info.dex as number[];
return dex.reduce((sum, value) => sum + value, 0);
});
expect(entries).toHaveLength(1844);
expect(new Set(entries.map((entry) => entry.uniqueName)).size).toBe(1844);
expect(Math.min(...weights)).toBe(100122);
expect(Math.max(...weights)).toBe(2582699);
const traitFrequencies = entries.reduce<Record<string, number>>((result, entry) => {
const key = String(entry.info.specialDomestic);
result[key] = (result[key] ?? 0) + 1;
return result;
}, {});
expect(traitFrequencies).toEqual({
che_event_격노: 152,
che_event_견고: 91,
che_event_공성: 8,
che_event_궁병: 12,
che_event_귀병: 38,
che_event_기병: 12,
che_event_돌격: 98,
che_event_무쌍: 100,
che_event_반계: 81,
che_event_보병: 10,
che_event_신산: 99,
che_event_신중: 106,
che_event_위압: 85,
che_event_의술: 37,
che_event_저격: 251,
che_event_집중: 125,
che_event_징병: 169,
che_event_척사: 166,
che_event_필살: 156,
che_event_환술: 48,
});
expect(entries[0]).toEqual({
uniqueName: '⑨탈곡기',
info: {
generalName: '⑨탈곡기',
leadership: 69,
strength: 12,
intel: 80,
specialDomestic: 'che_event_징병',
dex: [12066, 27302, 29463, 307356, 16448],
imgsvr: 1,
picture: '9ed8be6.gif?=20190417',
uniqueName: '⑨탈곡기',
},
});
expect(entries.at(-1)?.uniqueName).toBe('④야부키 나코');
});
it('rejects an unsupported pool instead of silently substituting data', async () => {
await expect(loadGeneralPoolEntries('SPoolUnknown')).rejects.toThrow('Unsupported general pool');
});
});
@@ -254,6 +254,7 @@ describe('input event atomicity', () => {
resolveError = resolve;
});
const publishCommandResult = vi.fn(async () => {});
const publishCommandError = vi.fn(async () => {});
let engineState = { value: 'before' };
const stateManager = new EngineStateManager();
stateManager.register('test', {
@@ -287,6 +288,7 @@ describe('input event atomicity', () => {
commandResponder: {
publishStatus: async () => {},
publishCommandResult,
publishCommandError,
},
},
{
@@ -305,6 +307,10 @@ describe('input event atomicity', () => {
lastError: 'injected commit failure',
});
expect(publishCommandResult).not.toHaveBeenCalled();
expect(publishCommandError).toHaveBeenCalledWith(
'event-2',
expect.objectContaining({ message: 'injected commit failure' })
);
expect(engineState).toEqual({ value: 'before' });
expect(stateManager.getRevision()).toBe(0);
@@ -320,6 +326,7 @@ describe('input event atomicity', () => {
});
const commitCommand = vi.fn(async () => {});
const publishCommandResult = vi.fn(async () => {});
const publishCommandError = vi.fn(async () => {});
let engineState = { value: 'before' };
const stateManager = new EngineStateManager();
stateManager.register('test', {
@@ -351,6 +358,7 @@ describe('input event atomicity', () => {
commandResponder: {
publishStatus: async () => {},
publishCommandResult,
publishCommandError,
},
},
{
@@ -370,6 +378,10 @@ describe('input event atomicity', () => {
});
expect(commitCommand).not.toHaveBeenCalled();
expect(publishCommandResult).not.toHaveBeenCalled();
expect(publishCommandError).toHaveBeenCalledWith(
'event-3',
expect.objectContaining({ message: 'injected handler failure' })
);
expect(engineState).toEqual({ value: 'before' });
expect(stateManager.getRevision()).toBe(0);
+171
View File
@@ -30,6 +30,12 @@ type ScenarioSeederPrismaClient = {
count(): Promise<number>;
findFirst(): Promise<{ age: number; startAge: number; meta: unknown } | null>;
};
selectPoolEntry: {
count(): Promise<number>;
findFirst(args: {
orderBy: { id: 'asc' | 'desc' };
}): Promise<{ uniqueName: string; info: unknown } | null>;
};
diplomacy: {
count(): Promise<number>;
findFirst(args: {
@@ -48,6 +54,10 @@ type ScenarioSeederPrismaClient = {
currentMonth: number;
} | null>;
};
gameHistory: {
findUnique(args: { where: { serverId: string } }): Promise<{ env: unknown } | null>;
deleteMany(args: { where: { serverId: string } }): Promise<{ count: number }>;
};
};
const requiredTables = ['world_state', 'nation', 'city', 'general', 'diplomacy', 'troop', 'event'];
@@ -254,6 +264,167 @@ describeDb('scenario database seed', () => {
await connector.disconnect();
}
});
test('seeds the exact scenario 903 UnderS30 selection pool', async () => {
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl,
});
const connector = createGamePostgresConnector({ url: databaseUrl });
await connector.connect();
try {
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
expect(await prisma.selectPoolEntry.count()).toBe(1844);
expect(await prisma.selectPoolEntry.findFirst({ orderBy: { id: 'asc' } })).toMatchObject({
uniqueName: '⑨탈곡기',
info: {
generalName: '⑨탈곡기',
specialDomestic: 'che_event_징병',
},
});
expect(await prisma.selectPoolEntry.findFirst({ orderBy: { id: 'desc' } })).toMatchObject({
uniqueName: '④야부키 나코',
});
} finally {
await connector.disconnect();
}
});
test('clears general lifecycle rows before reusing general ids on reseed', async () => {
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl,
});
const connector = createGamePostgresConnector({ url: databaseUrl });
await connector.connect();
try {
const prisma = connector.prisma;
const generalId = 990_903;
const createSelectedGeneral = async (): Promise<void> => {
await prisma.general.create({
data: {
id: generalId,
userId: 'scenario-reseed-user',
name: '재설치선택장수',
turnTime: new Date('2026-07-30T12:00:00.000Z'),
},
});
};
const createLifecycleRows = async (): Promise<void> => {
await prisma.generalTurn.create({
data: {
generalId,
turnIdx: 0,
actionCode: '휴식',
},
});
await prisma.generalTurnRevision.create({
data: {
generalId,
revision: 7,
},
});
await prisma.rankData.create({
data: {
nationId: 0,
generalId,
type: 'experience',
value: 123,
},
});
await prisma.generalAccessLog.create({
data: {
generalId,
userId: 'scenario-reseed-user',
},
});
};
const expectLifecycleRows = async (count: number): Promise<void> => {
await expect(
Promise.all([
prisma.generalTurn.count({ where: { generalId } }),
prisma.generalTurnRevision.count({ where: { generalId } }),
prisma.rankData.count({ where: { generalId } }),
prisma.generalAccessLog.count({ where: { generalId } }),
])
).resolves.toEqual([count, count, count, count]);
};
await createSelectedGeneral();
await createLifecycleRows();
await expectLifecycleRows(1);
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl,
});
await expectLifecycleRows(0);
await expect(prisma.general.findUnique({ where: { id: generalId } })).resolves.toBeNull();
await createSelectedGeneral();
await createLifecycleRows();
await expectLifecycleRows(1);
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl,
});
await expectLifecycleRows(0);
} finally {
await connector.disconnect();
}
});
test('persists a private hidden seed without copying it into game history', async () => {
const envName = 'INTEGRATION_WORLD_SEED';
const originalSeed = process.env[envName];
const serverId = 'scenario-seeder-hidden-seed-test';
const connector = createGamePostgresConnector({ url: databaseUrl });
try {
process.env[envName] = 'scenario-seeder-explicit-hidden-seed';
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl,
installOptions: { serverId },
});
await connector.connect();
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
const explicitWorld = await prisma.worldState.findFirst();
expect(explicitWorld?.meta).toMatchObject({
hiddenSeed: 'scenario-seeder-explicit-hidden-seed',
});
const explicitHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
expect((explicitHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty(
'hiddenSeed'
);
delete process.env[envName];
await seedScenarioToDatabase({
scenarioId: 903,
databaseUrl,
installOptions: { serverId },
});
const randomWorld = await prisma.worldState.findFirst();
const randomSeed = (randomWorld?.meta as Record<string, unknown>)?.hiddenSeed;
expect(randomSeed).toMatch(/^[0-9a-f]{32}$/);
expect(randomSeed).not.toBe('scenario-seeder-explicit-hidden-seed');
const randomHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
expect((randomHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty(
'hiddenSeed'
);
await prisma.gameHistory.deleteMany({ where: { serverId } });
} finally {
if (originalSeed === undefined) {
delete process.env[envName];
} else {
process.env[envName] = originalSeed;
}
await connector.disconnect();
}
});
});
describe('tracked scenario composition', () => {
@@ -0,0 +1,274 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import {
createGamePostgresConnector,
type GamePrisma,
type GamePrismaClient,
} from '@sammo-ts/infra';
import { createDatabaseTurnHooks } from '../src/turn/databaseHooks.js';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { loadTurnWorldFromDatabase } from '../src/turn/worldLoader.js';
const databaseUrl = process.env.SELECT_POOL_DATABASE_URL;
const integration = describe.skipIf(!databaseUrl);
const generalId = 990_904;
const cityId = 990_904;
const scenarioCode = 'select-pool-release-integration';
const assertDedicatedDatabase = (rawUrl: string): void => {
const schema = new URL(rawUrl).searchParams.get('schema');
if (!schema?.endsWith('select_pool_integration')) {
throw new Error(`Refusing to mutate non-dedicated schema: ${schema ?? '(missing)'}`);
}
};
integration('select pool release during general deletion', () => {
let db: GamePrismaClient;
let closeDb: (() => Promise<void>) | undefined;
beforeAll(async () => {
assertDedicatedDatabase(databaseUrl!);
const connector = createGamePostgresConnector({ url: databaseUrl! });
await connector.connect();
db = connector.prisma;
closeDb = () => connector.disconnect();
await db.$executeRawUnsafe('DROP TABLE IF EXISTS "select_pool_delete_blocker"');
await db.selectPoolEntry.deleteMany({
where: { uniqueName: 'release-candidate' },
});
await db.general.deleteMany({ where: { id: generalId } });
await db.city.deleteMany({ where: { id: cityId } });
await db.worldState.deleteMany({ where: { scenarioCode } });
await db.worldState.create({
data: {
scenarioCode,
currentYear: 180,
currentMonth: 1,
tickSeconds: 300,
config: {
npcMode: 2,
turnTermMinutes: 5,
stat: {
total: 165,
min: 15,
max: 80,
npcTotal: 165,
npcMax: 80,
npcMin: 15,
chiefMin: 40,
},
iconPath: '.',
map: {
targetGeneralPool: 'SPoolUnderU30',
generalPoolAllowOption: ['ego'],
},
const: {},
environment: {
mapName: 'che',
unitSet: 'che',
},
},
meta: {
hiddenSeed: 'select-pool-release-seed',
killturn: 5,
turntime: '2026-07-30T12:00:00.000Z',
},
},
});
await db.city.create({
data: {
id: cityId,
name: '해제성',
level: 5,
nationId: 0,
population: 10_000,
populationMax: 20_000,
agriculture: 1_000,
agricultureMax: 2_000,
commerce: 1_000,
commerceMax: 2_000,
security: 1_000,
securityMax: 2_000,
defence: 1_000,
defenceMax: 2_000,
wall: 1_000,
wallMax: 2_000,
region: 1,
},
});
await db.general.create({
data: {
id: generalId,
userId: 'select-pool-release-user',
name: '해제대상',
nationId: 0,
cityId,
troopId: 0,
npcState: 0,
affinity: 1,
bornYear: 160,
deadYear: 240,
picture: 'default.jpg',
imageServer: 0,
leadership: 50,
strength: 50,
intel: 50,
experience: 2_000,
dedication: 2_000,
officerLevel: 0,
turnTime: new Date('2026-07-30T12:01:00.000Z'),
age: 20,
startAge: 20,
personalCode: 'che_안전',
specialCode: 'che_event_신산',
special2Code: 'che_무쌍',
meta: {
killturn: 5,
dex1: 100_000,
dex2: 100_000,
dex3: 100_000,
dex4: 100_000,
dex5: 100_000,
},
},
});
await db.selectPoolEntry.create({
data: {
uniqueName: 'release-candidate',
ownerUserId: null,
generalId,
reservedUntil: null,
info: {
uniqueName: 'release-candidate',
generalName: '해제대상',
leadership: 50,
strength: 50,
intel: 50,
specialDomestic: 'che_event_신산',
dex: [100_000, 100_000, 100_000, 100_000, 100_000],
imgsvr: 0,
picture: 'default.jpg',
} as GamePrisma.InputJsonValue,
},
});
});
afterAll(async () => {
if (!db) {
await closeDb?.();
return;
}
await db.$executeRawUnsafe('DROP TABLE IF EXISTS "select_pool_delete_blocker"');
await db.selectPoolEntry.deleteMany({
where: { uniqueName: 'release-candidate' },
});
await db.general.deleteMany({ where: { id: generalId } });
await db.city.deleteMany({ where: { id: cityId } });
await db.worldState.deleteMany({ where: { scenarioCode } });
await closeDb?.();
});
it('round-trips independent event-domestic and war trait slots through a dirty flush', async () => {
const loaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
const world = new InMemoryTurnWorld(loaded.state, loaded.snapshot, {
schedule: { entries: [{ startMinute: 0, tickMinutes: 5 }] },
});
const before = world.getGeneralById(generalId);
expect(before?.role).toMatchObject({
specialDomestic: 'che_event_신산',
specialWar: 'che_무쌍',
});
expect(world.updateGeneral(generalId, { gold: (before?.gold ?? 0) + 1 })).not.toBeNull();
const hooks = await createDatabaseTurnHooks(databaseUrl!, world);
try {
await hooks.hooks.flushChanges?.({
lastTurnTime: loaded.state.lastTurnTime.toISOString(),
processedGenerals: 0,
processedTurns: 0,
durationMs: 0,
partial: false,
});
} finally {
await hooks.close();
}
await expect(db.general.findUniqueOrThrow({ where: { id: generalId } })).resolves.toMatchObject({
specialCode: 'che_event_신산',
special2Code: 'che_무쌍',
});
const reloaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
expect(
reloaded.snapshot.generals.find((general) => general.id === generalId)?.role
).toMatchObject({
specialDomestic: 'che_event_신산',
specialWar: 'che_무쌍',
});
});
it('rolls back a failed flush, then releases all Ref fields before deleting the general', async () => {
const loaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
const world = new InMemoryTurnWorld(loaded.state, loaded.snapshot, {
schedule: { entries: [{ startMinute: 0, tickMinutes: 5 }] },
});
expect(world.removeGeneral(generalId)).toBe(true);
const hooks = await createDatabaseTurnHooks(databaseUrl!, world);
try {
await db.$executeRawUnsafe(`
CREATE TABLE "select_pool_delete_blocker" (
"general_id" INTEGER PRIMARY KEY
REFERENCES "general"("id") ON DELETE RESTRICT
)
`);
await db.$executeRawUnsafe(
`INSERT INTO "select_pool_delete_blocker" ("general_id") VALUES (${generalId})`
);
await expect(
hooks.hooks.flushChanges?.({
lastTurnTime: loaded.state.lastTurnTime.toISOString(),
processedGenerals: 0,
processedTurns: 0,
durationMs: 0,
partial: false,
})
).rejects.toThrow();
await expect(db.general.findUnique({ where: { id: generalId } })).resolves.not.toBeNull();
await expect(
db.selectPoolEntry.findUniqueOrThrow({
where: { uniqueName: 'release-candidate' },
})
).resolves.toMatchObject({
generalId,
ownerUserId: null,
reservedUntil: null,
});
await db.$executeRawUnsafe('DROP TABLE "select_pool_delete_blocker"');
await hooks.hooks.flushChanges?.({
lastTurnTime: loaded.state.lastTurnTime.toISOString(),
processedGenerals: 0,
processedTurns: 0,
durationMs: 0,
partial: false,
});
await expect(db.general.findUnique({ where: { id: generalId } })).resolves.toBeNull();
await expect(
db.selectPoolEntry.findUniqueOrThrow({
where: { uniqueName: 'release-candidate' },
})
).resolves.toMatchObject({
generalId: null,
ownerUserId: null,
reservedUntil: null,
});
} finally {
await hooks.close();
}
});
});
+4 -1
View File
@@ -135,7 +135,7 @@ describe('InMemoryTurnProcessor ordering', () => {
currentMonth: 1,
tickSeconds: 3600,
lastTurnTime: baseTime,
meta: {},
meta: { lastGeneralId: 1 },
};
const world = new InMemoryTurnWorld(state, snapshot, {
@@ -157,5 +157,8 @@ describe('InMemoryTurnProcessor ordering', () => {
});
expect(executed).toEqual([2, 3, 1]);
expect(world.getNextGeneralId()).toBe(4);
expect(world.getNextGeneralId()).toBe(5);
expect(world.getState().meta).toMatchObject({ lastGeneralId: 5 });
});
});
+60 -2
View File
@@ -52,6 +52,7 @@ const simulatorOptions = {
],
},
nationTypes: [{ key: 'che_중립', name: '중립', info: '특별한 효과 없음' }],
eventDomesticTraits: [{ key: 'che_event_신산', name: '신산', info: '계략 강화' }],
warTraits: [{ key: 'che_필살', name: '필살', info: '필살 확률 증가' }],
personalities: [{ key: 'che_대담', name: '대담', info: '공격적인 성격' }],
items: { horse: [], weapon: [], book: [], item: [] },
@@ -163,6 +164,7 @@ type Fixture = {
queueFirst?: boolean;
pollingCount: number;
requests: string[];
simulationPayloads: unknown[];
};
const installImages = async (page: Page) => {
@@ -185,8 +187,14 @@ const installApi = async (page: Page, fixture: Fixture) => {
});
await page.route('**/che/api/trpc/**', async (route) => {
const operations = operationNames(route);
const results = operations.map((operation) => {
const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
const requestBody =
rawRequestBody && typeof rawRequestBody === 'object'
? (rawRequestBody as Record<string, unknown>)
: {};
const results = operations.map((operation, operationIndex) => {
fixture.requests.push(operation);
if (operation === 'auth.status') return response({ userId: 'battle-sim-user' });
if (operation === 'lobby.info') {
return response({
year: 205,
@@ -206,6 +214,18 @@ const installApi = async (page: Page, fixture: Fixture) => {
}
if (operation === 'battle.getGeneralDetail') return response(importedGeneral);
if (operation === 'battle.simulate') {
const rawPayload =
requestBody[String(operationIndex)] ?? (operations.length === 1 ? requestBody : undefined);
const payload =
rawPayload && typeof rawPayload === 'object'
? (rawPayload as {
json?: unknown;
input?: { json?: unknown };
})
: undefined;
fixture.simulationPayloads.push(
payload?.json ?? payload?.input?.json ?? rawPayload
);
if (fixture.failNextSimulation) {
fixture.failNextSimulation = false;
return errorResponse(operation, '시뮬레이터 입력 오류');
@@ -244,7 +264,13 @@ const gotoSimulator = async (page: Page) => {
};
test('operates independent/game presets, imports my general, and renders battle logs', async ({ page }) => {
const fixture: Fixture = { hasGeneral: true, queueFirst: true, pollingCount: 0, requests: [] };
const fixture: Fixture = {
hasGeneral: true,
queueFirst: true,
pollingCount: 0,
requests: [],
simulationPayloads: [],
};
await installApi(page, fixture);
await page.setViewportSize({ width: 1280, height: 900 });
await gotoSimulator(page);
@@ -265,6 +291,9 @@ test('operates independent/game presets, imports my general, and renders battle
await page.getByRole('button', { name: '내 장수를 출병자로' }).click();
await expect(page.getByLabel('이름').first()).toHaveValue('유비');
await expect(page.getByLabel('병사').first()).toHaveValue('4321');
const attackerDomesticTrait = page.getByLabel('내정특기').first();
await attackerDomesticTrait.selectOption('che_event_신산');
await expect(attackerDomesticTrait).toHaveValue('che_event_신산');
const battleButton = page.getByRole('button', { name: '전투', exact: true });
await battleButton.hover();
@@ -276,6 +305,34 @@ test('operates independent/game presets, imports my general, and renders battle
await expect(page.getByText('5', { exact: true })).toBeVisible();
expect(fixture.pollingCount).toBe(2);
expect(fixture.requests).toContain('battle.getSimulation');
expect(fixture.simulationPayloads[0]).toMatchObject({
attackerGeneral: { special: 'che_event_신산' },
});
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: '모두 저장' }).click();
const download = await downloadPromise;
const downloadPath = await download.path();
expect(downloadPath).not.toBeNull();
const exportedBattle = JSON.parse(await readFile(downloadPath!, 'utf8')) as {
objType: string;
data: { attackerGeneral: { special?: string | null } };
};
expect(exportedBattle).toMatchObject({
objType: 'battle',
data: { attackerGeneral: { special: 'che_event_신산' } },
});
await attackerDomesticTrait.selectOption({ label: '-' });
await expect(attackerDomesticTrait).toHaveValue('-');
await page.locator('.header-actions input[type="file"]').setInputFiles(downloadPath!);
await expect(attackerDomesticTrait).toHaveValue('che_event_신산');
await battleButton.click();
await expect.poll(() => fixture.simulationPayloads.length).toBe(2);
expect(fixture.simulationPayloads[1]).toMatchObject({
attackerGeneral: { special: 'che_event_신산' },
});
if (artifactRoot) {
await page.screenshot({
@@ -292,6 +349,7 @@ test('keeps simulation available without a game general and preserves input afte
failNextSimulation: true,
pollingCount: 0,
requests: [],
simulationPayloads: [],
};
await installApi(page, fixture);
await page.setViewportSize({ width: 500, height: 900 });
@@ -28,6 +28,7 @@ export default defineConfig({
'commandArguments.spec.ts',
'commandArgumentsLive.spec.ts',
'mainNavigation.spec.ts',
'session-auth.spec.ts',
],
fullyParallel: false,
workers: 1,
@@ -0,0 +1,40 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig, devices } from '@playwright/test';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
const port = Number(process.env.PLAYWRIGHT_FRONTEND_PORT ?? 15124);
const basePath = `/${(process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'hwe').replace(/^\/+|\/+$/g, '')}`;
const gameProfile = process.env.PLAYWRIGHT_GAME_PROFILE ?? 'hwe:903';
const baseURL = `http://127.0.0.1:${port}${basePath}/`;
const gameApiUrl = process.env.PLAYWRIGHT_GAME_API_URL ?? 'http://127.0.0.1:15125/trpc';
export default defineConfig({
testDir: '.',
testMatch: ['selectGeneralLive.spec.ts'],
fullyParallel: false,
workers: 1,
timeout: 60_000,
expect: {
timeout: 10_000,
},
reporter: [['list']],
outputDir: resolve(repositoryRoot, 'test-results/select-pool-live'),
use: {
baseURL,
...devices['Desktop Chrome'],
deviceScaleFactor: 1,
colorScheme: 'dark',
locale: 'ko-KR',
timezoneId: 'Asia/Seoul',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
webServer: {
command: `VITE_APP_BASE_PATH=${basePath} VITE_GAME_API_URL=${gameApiUrl} VITE_GAME_PROFILE=${gameProfile} VITE_GATEWAY_WEB_URL=/gateway/ pnpm --filter @sammo-ts/game-frontend dev --host 127.0.0.1 --port ${port}`,
cwd: repositoryRoot,
url: baseURL,
reuseExistingServer: false,
timeout: 120_000,
},
});
@@ -0,0 +1,519 @@
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { expect, test, type Page } from '@playwright/test';
import { encryptGameSessionToken } from '@sammo-ts/common/auth/gameToken';
import {
createGamePostgresConnector,
type GamePrisma,
type GamePrismaClient,
} from '@sammo-ts/infra';
const gameTokenSecret = process.env.SELECT_POOL_LIVE_GAME_SECRET;
const databaseUrl = process.env.SELECT_POOL_LIVE_DATABASE_URL;
const userId = process.env.SELECT_POOL_LIVE_USER_ID;
const profile = process.env.PLAYWRIGHT_GAME_PROFILE ?? 'hwe:903';
const hasLiveFixture = Boolean(gameTokenSecret && databaseUrl && userId);
const workspaceRoot =
process.env.SAMMO_WORKSPACE_ROOT ??
path.resolve(import.meta.dirname, '../../../../../sam_rebuild');
const defaultIcon = path.resolve(
process.env.SELECT_POOL_LIVE_DEFAULT_ICON ??
path.join(workspaceRoot, 'image/icons/default.jpg')
);
const walnutTexture = path.join(workspaceRoot, 'image/game/back_walnut.jpg');
const greenTexture = path.join(workspaceRoot, 'image/game/back_green.jpg');
const fixtureNationIds = [990_901, 990_902, 990_903];
interface AssetTracker {
userIconRequests: number;
}
const installSession = async (page: Page, tracker?: AssetTracker): Promise<void> => {
const now = new Date();
const gameToken = encryptGameSessionToken(
{
version: 1,
profile,
issuedAt: now.toISOString(),
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
sessionId: `select-pool-live-${randomUUID()}`,
user: {
id: userId!,
username: 'select-pool-live',
displayName: '선택실사용자',
roles: ['user'],
legacyMemberNo: 42,
},
sanctions: {},
identity: {
kakaoVerified: true,
canCreateGeneral: true,
requiresKakaoVerification: false,
graceEndsAt: null,
},
},
gameTokenSecret!
);
await page.addInitScript(
({ token, gameProfile }) => {
if (!window.localStorage.getItem('sammo-game-token')) {
window.localStorage.setItem('sammo-game-token', token);
}
window.localStorage.setItem('sammo-game-profile', gameProfile);
},
{ token: gameToken, gameProfile: profile }
);
await page.addInitScript(() => {
const values = [1, 0];
Object.defineProperty(window.crypto, 'getRandomValues', {
configurable: true,
value: <T extends ArrayBufferView | null>(array: T): T => {
if (array && (array as Uint32Array).length > 0) {
(array as Uint32Array)[0] = values.shift() ?? 0;
}
return array;
},
});
});
await page.route('**/image/icons/**', (route) =>
route.fulfill({ path: defaultIcon, contentType: 'image/jpeg' })
);
await page.route('**/gateway/api/user-icons/**', (route) => {
if (tracker) {
tracker.userIconRequests += 1;
}
return route.fulfill({ status: 404, body: '' });
});
await page.route('**/image/game/back_walnut.jpg', (route) =>
route.fulfill({ path: walnutTexture, contentType: 'image/jpeg' })
);
await page.route('**/image/game/back_green.jpg', (route) =>
route.fulfill({ path: greenTexture, contentType: 'image/jpeg' })
);
};
const waitForPool = async (page: Page): Promise<void> => {
await expect(page.locator('.card-holder > .general-card')).toHaveCount(14);
await page.evaluate(async () => {
await document.fonts.ready;
});
};
test.describe('scenario 903 live selection pool', () => {
test.skip(!hasLiveFixture, 'live selection-pool token and database are required');
let db: GamePrismaClient;
let closeDb: (() => Promise<void>) | undefined;
test.beforeAll(async () => {
const connector = createGamePostgresConnector({ url: databaseUrl! });
await connector.connect();
db = connector.prisma;
closeDb = () => connector.disconnect();
});
test.afterAll(async () => {
await closeDb?.();
});
test('renders Ref-width desktop/mobile cards, tooltip, focus, and expiration states', async ({
page,
}, testInfo) => {
await page.clock.install({ time: new Date() });
const assetTracker: AssetTracker = { userIconRequests: 0 };
await installSession(page, assetTracker);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('select-general');
await waitForPool(page);
await expect(page.locator('.server-info-table')).toContainText(
'현재 : 180年 1月 (5분 턴 서버)'
);
await expect(page.locator('.server-info-table')).toContainText(
'등록 장수 : 유저 0 / 500 명'
);
await expect(page.locator('.invitation-table')).toContainText('임관 권유 메시지');
const geometry = await page.evaluate(() => {
const root = document.querySelector<HTMLElement>('.select-pool-page')!;
const cards = Array.from(
document.querySelectorAll<HTMLElement>('.card-holder > .general-card')
);
const images = Array.from(
document.querySelectorAll<HTMLImageElement>('.card-holder .portrait img')
);
const selectionBody = document.querySelector<HTMLElement>('.selection-body')!;
const createSection = document.querySelector<HTMLElement>('.create-section')!;
const createBody = document.querySelector<HTMLElement>('.create-body')!;
const pageTitle = document.querySelector<HTMLElement>('.page-title')!;
const serverInfoTable =
document.querySelector<HTMLElement>('.server-info-table')!;
const invitationTable =
document.querySelector<HTMLElement>('.invitation-table')!;
const footerBack = document.querySelector<HTMLElement>('.footer-back')!;
const footerBanner = document.querySelector<HTMLElement>('.footer-banner')!;
const firstButton = document.querySelector<HTMLElement>(
'.card-holder .select-button'
)!;
return {
root: root.getBoundingClientRect().toJSON(),
pageTitle: pageTitle.getBoundingClientRect().toJSON(),
serverInfoTable: serverInfoTable.getBoundingClientRect().toJSON(),
invitationTable: invitationTable.getBoundingClientRect().toJSON(),
cards: cards.map((card) => card.getBoundingClientRect().toJSON()),
images: images.map((image) => ({
rect: image.getBoundingClientRect().toJSON(),
naturalWidth: image.naturalWidth,
naturalHeight: image.naturalHeight,
objectFit: getComputedStyle(image).objectFit,
})),
selectionBody: selectionBody.getBoundingClientRect().toJSON(),
createSection: createSection.getBoundingClientRect().toJSON(),
createBody: createBody.getBoundingClientRect().toJSON(),
footerBack: footerBack.getBoundingClientRect().toJSON(),
footerBanner: footerBanner.getBoundingClientRect().toJSON(),
firstButton: firstButton.getBoundingClientRect().toJSON(),
rootStyle: {
fontFamily: getComputedStyle(root).fontFamily,
fontSize: getComputedStyle(root).fontSize,
lineHeight: getComputedStyle(root).lineHeight,
backgroundImage: getComputedStyle(root).backgroundImage,
},
scrollWidth: document.documentElement.scrollWidth,
};
});
expect(geometry.root).toMatchObject({ x: 100, y: 8, width: 1000 });
expect(geometry.pageTitle).toMatchObject({ x: 100, y: 8, width: 1000 });
expect(Math.abs(geometry.pageTitle.height - 42.1875)).toBeLessThan(0.6);
expect(Math.abs(geometry.serverInfoTable.y - 50.1875)).toBeLessThan(0.6);
expect(Math.abs(geometry.serverInfoTable.height - 40.375)).toBeLessThan(0.6);
expect(Math.abs(geometry.invitationTable.x - 553)).toBeLessThan(0.6);
expect(Math.abs(geometry.invitationTable.y - 90.5625)).toBeLessThan(0.6);
expect(geometry.invitationTable.width).toBe(94);
expect(Math.abs(geometry.invitationTable.height - 20.1875)).toBeLessThan(0.1);
expect(geometry.rootStyle).toMatchObject({
fontSize: '14px',
lineHeight: '18.2px',
});
expect(geometry.rootStyle.fontFamily).toContain('Pretendard');
expect(geometry.rootStyle.backgroundImage).toContain('back_walnut.jpg');
expect(geometry.cards.every((card) => card.width === 127)).toBe(true);
expect(new Set(geometry.cards.map((card) => card.y)).size).toBe(2);
const shortestCard = Math.min(...geometry.cards.map((card) => card.height));
expect(Math.abs(shortestCard - 254.875)).toBeLessThan(0.6);
expect(
geometry.images.every(
(image, index) =>
image.rect.width === 64 &&
image.rect.height === 64 &&
Math.abs(
image.rect.x -
(geometry.cards[index]!.x +
(geometry.cards[index]!.width - image.rect.width) / 2)
) < 0.1
)
).toBe(true);
expect(
geometry.images.every(
(image) => image.naturalWidth > 0 && image.naturalHeight > 0
)
).toBe(true);
expect(geometry.images.every((image) => image.objectFit === 'fill')).toBe(true);
const fallbackImages = page.locator(
'.card-holder .portrait img[data-fallback-applied="true"]'
);
await expect(fallbackImages).not.toHaveCount(0);
expect(assetTracker.userIconRequests).toBe(await fallbackImages.count());
expect(Math.abs(geometry.selectionBody.y - 130.9375)).toBeLessThan(0.6);
expect(Math.abs(geometry.createSection.height - 87.375)).toBeLessThan(0.6);
expect(geometry.firstButton.height).toBe(19);
expect(geometry.footerBanner.height).toBeCloseTo(20.1875, 3);
await expect(page.locator('.invitation-table tbody tr')).toHaveCount(0);
await expect(page.locator('.footer-banner')).toContainText(
'삼국지 모의전투 HiDCHe core2026'
);
await expect(page.locator('.footer-banner a')).toHaveText('Credit');
const firstTrait = page.locator('.card-holder .trait-tooltip').first();
await firstTrait.hover();
await expect(firstTrait.getByRole('tooltip')).toBeVisible();
await page.screenshot({
path: testInfo.outputPath('select-general-desktop-hover.png'),
fullPage: true,
});
const firstButton = page.locator('.card-holder .select-button').first();
await firstButton.focus();
await expect(firstButton).toHaveCSS('outline-style', 'auto');
await expect(firstButton).toHaveCSS('outline-width', '1px');
await firstButton.hover();
await expect(firstButton).toHaveCSS('background-color', 'rgb(25, 25, 25)');
await page.setViewportSize({ width: 500, height: 900 });
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
.toBeGreaterThanOrEqual(1008);
await page.screenshot({
path: testInfo.outputPath('select-general-mobile.png'),
fullPage: true,
});
const validText = await page.locator('.selection-body small span').textContent();
expect(validText).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
const displayedExpiry = new Date(`${validText!.replace(' ', 'T')}+09:00`).getTime();
const delta = Math.max(displayedExpiry - Date.now(), 0);
await page.clock.fastForward(delta);
await expect(page.locator('.expired-text')).toHaveCount(0);
await page.clock.fastForward(2_000);
await expect(page.locator('.expired-text')).toHaveText('- 만료 -');
});
test('shuffles non-neutral invitation nations with Ref-style row content and colors', async ({
page,
}) => {
await db.nation.deleteMany({ where: { id: { in: fixtureNationIds } } });
await db.nation.createMany({
data: [
{
id: fixtureNationIds[0]!,
name: '테스트국A',
color: '#330000',
level: 1,
meta: { infoText: 'A 권유' },
},
{
id: fixtureNationIds[1]!,
name: '테스트국B',
color: '#FFFF00',
level: 1,
meta: { infoText: 'B 권유' },
},
{
id: fixtureNationIds[2]!,
name: '테스트국C',
color: '#000080',
level: 1,
meta: { infoText: 'C 권유' },
},
],
});
try {
await installSession(page);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('select-general');
await waitForPool(page);
const rows = page.locator('.invitation-table tbody tr');
await expect(rows.locator('.invitation-nation')).toHaveText([
'테스트국C',
'테스트국A',
'테스트국B',
]);
await expect(rows.locator('.invitation-message')).toHaveText([
'C 권유',
'A 권유',
'B 권유',
]);
await expect(rows.nth(0)).toHaveCSS('background-color', 'rgb(0, 0, 128)');
await expect(rows.nth(1)).toHaveCSS('background-color', 'rgb(51, 0, 0)');
await expect(rows.nth(2)).toHaveCSS('background-color', 'rgb(255, 255, 0)');
await expect(rows.nth(0)).toHaveCSS('color', 'rgb(255, 255, 255)');
await expect(rows.nth(2)).toHaveCSS('color', 'rgb(0, 0, 0)');
const invitationGeometry = await page.locator('.invitation-table').evaluate((table) => {
const rect = table.getBoundingClientRect();
return { x: rect.x, width: rect.width };
});
expect(invitationGeometry).toEqual({ x: 100, width: 1000 });
} finally {
await db.nation.deleteMany({ where: { id: { in: fixtureNationIds } } });
}
});
test('creates, rejects cooldown, exposes MyPage action, and reselects through the live API', async ({
page,
}) => {
const dialogs: string[] = [];
const createClientRequestIds: string[] = [];
let injectCreateTimeout = true;
page.on('dialog', async (dialog) => {
dialogs.push(dialog.message());
await dialog.accept();
});
await page.route('**/trpc/join.selectPoolGeneral?batch=1', async (route) => {
const findClientRequestId = (value: unknown): string | undefined => {
if (!value || typeof value !== 'object') return undefined;
if (
'clientRequestId' in value &&
typeof value.clientRequestId === 'string'
) {
return value.clientRequestId;
}
for (const nested of Object.values(value)) {
const found = findClientRequestId(nested);
if (found) return found;
}
return undefined;
};
const clientRequestId = findClientRequestId(route.request().postDataJSON());
expect(clientRequestId).toMatch(/^[0-9a-f-]{36}$/);
createClientRequestIds.push(clientRequestId!);
if (!injectCreateTimeout) {
await route.continue();
return;
}
injectCreateTimeout = false;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
error: {
message:
'장수 선택 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.',
code: -32008,
data: {
code: 'TIMEOUT',
httpStatus: 408,
path: 'join.selectPoolGeneral',
},
},
},
]),
});
});
const assetTracker: AssetTracker = { userIconRequests: 0 };
await installSession(page, assetTracker);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('select-general');
await waitForPool(page);
const candidateCards = page.locator('.card-holder > .general-card');
const fallbackCard = candidateCards
.filter({ has: page.locator('img[data-fallback-applied="true"]') })
.first();
await expect(fallbackCard).toBeVisible();
const initialName = await fallbackCard.locator('h4').first().textContent();
const userIconRequestsBeforePreview = assetTracker.userIconRequests;
await fallbackCard.locator('.select-button').click();
await expect(page.locator('.selected-card')).toHaveCount(1);
await expect(
page.locator('.selected-card img[data-fallback-applied="true"]')
).toBeVisible();
expect(assetTracker.userIconRequests).toBeGreaterThan(userIconRequestsBeforePreview);
await page.locator('.custom-form select').selectOption('che_안전');
await page.getByRole('button', { name: '다시입력' }).click();
await expect(page.locator('.custom-form select')).toHaveValue('Random');
await expect(page.locator('.selected-card')).toHaveCount(1);
await page.locator('.custom-form select').selectOption('che_안전');
await page.locator('#build-general').click();
await expect.poll(() => dialogs).toContain(
'실패했습니다: 장수 선택 요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.'
);
await waitForPool(page);
const retryCard = page
.locator('.card-holder > .general-card')
.filter({ has: page.locator('h4', { hasText: initialName?.trim() ?? '' }) })
.first();
await retryCard.locator('.select-button').click();
await page.locator('.custom-form select').selectOption('che_안전');
await page.locator('#build-general').click();
await expect(page).toHaveURL(/\/hwe\/$/);
expect(dialogs.filter((message) => message === '이 장수로 생성할까요?')).toHaveLength(2);
await expect.poll(() => dialogs).toContain('선택한 장수로 생성했습니다.');
expect(createClientRequestIds).toHaveLength(2);
expect(createClientRequestIds[1]).toBe(createClientRequestIds[0]);
const created = await db.general.findFirstOrThrow({ where: { userId } });
expect(created.name).toBe(initialName?.trim());
expect(created.personalCode).toBe('che_안전');
expect(created.specialCode).toMatch(/^che_event_/);
const createEvent = await db.inputEvent.findFirstOrThrow({
where: { actorUserId: userId, eventType: 'selectPoolCreate' },
orderBy: { sequence: 'desc' },
});
expect(createEvent).toMatchObject({ status: 'SUCCEEDED', attempts: 1 });
expect(createEvent.requestId).toMatch(
new RegExp(`^select-pool:${userId}:[0-9a-f-]{36}:create$`)
);
expect(
await page.evaluate(() =>
window.sessionStorage.getItem('sammo-select-pool-pending-action')
)
).toBeNull();
await page.goto('my-page');
const actionLink = page.locator('.select-general-link');
await expect(actionLink).toBeVisible();
await expect(actionLink.locator('..')).toContainText(
/다른 장수 선택\s*\(\d{4}-\d{2}-\d{2}/
);
await expect(actionLink).toHaveCSS('width', '160px');
await expect(actionLink).toHaveCSS('height', '30px');
dialogs.length = 0;
await page.goto('select-general');
await expect.poll(() => dialogs).toContain('실패했습니다: 아직 다시 고를 수 없습니다');
await expect(page.locator('.error-text')).toHaveText('아직 다시 고를 수 없습니다');
const availableAt = '2026-07-29T00:00:00.000Z';
const cooldownRequestId = `select-pool-live-cooldown-${randomUUID()}`;
await db.inputEvent.create({
data: {
requestId: cooldownRequestId,
target: 'ENGINE',
eventType: 'patchGeneral',
payload: {
type: 'patchGeneral',
requestId: cooldownRequestId,
generalId: created.id,
patch: {
meta: {
next_change: availableAt,
nextChangeAt: availableAt,
},
},
} as GamePrisma.InputJsonValue,
},
});
await expect
.poll(
async () =>
(
await db.inputEvent.findUniqueOrThrow({
where: { requestId: cooldownRequestId },
})
).status
)
.toBe('SUCCEEDED');
dialogs.length = 0;
await page.reload();
await waitForPool(page);
const cards = page.locator('.card-holder > .general-card');
const names = await cards.locator('h4').allTextContents();
const targetIndex = names.findIndex((name) => name.trim() !== created.name);
expect(targetIndex).toBeGreaterThanOrEqual(0);
const targetName = names[targetIndex]!.trim();
await cards.nth(targetIndex).locator('.select-button').click();
await expect(page).toHaveURL(/\/hwe\/$/);
await expect.poll(() => dialogs).toContain(`이 장수를 선택할까요? : ${targetName}`);
await expect.poll(() => dialogs).toContain('선택한 장수로 변경했습니다.');
await expect
.poll(async () => (await db.general.findUniqueOrThrow({ where: { id: created.id } })).name)
.toBe(targetName);
const reselectEvent = await db.inputEvent.findFirstOrThrow({
where: { actorUserId: userId, eventType: 'selectPoolReselect' },
orderBy: { sequence: 'desc' },
});
expect(reselectEvent).toMatchObject({ status: 'SUCCEEDED', attempts: 1 });
expect(reselectEvent.requestId).toMatch(
new RegExp(`^select-pool:${userId}:[0-9a-f-]{36}:reselect$`)
);
expect(
await page.evaluate(() =>
window.sessionStorage.getItem('sammo-select-pool-pending-action')
)
).toBeNull();
});
});
+114
View File
@@ -0,0 +1,114 @@
import { expect, test, type Page, type Route } from '@playwright/test';
const response = (data: unknown) => ({ result: { data } });
const operationNames = (route: Route): string[] => {
const url = new URL(route.request().url());
return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(',');
};
const publicResponse = (operation: string): unknown => {
if (operation === 'public.getMapLayout') {
return response({ mapName: 'che', cityList: [] });
}
if (operation === 'public.getCachedMap') {
return response({ year: 180, month: 1, cityList: [], nationList: [], history: [] });
}
if (operation === 'public.getWorldTrend') {
return response({ year: 180, month: 1, turnTerm: 5 });
}
if (operation === 'public.getNationList' || operation === 'public.getGeneralList') {
return response([]);
}
throw new Error(`Unhandled public tRPC operation: ${operation}`);
};
const seedGameStorage = async (page: Page, gameToken: string): Promise<void> => {
await page.addInitScript((token) => {
window.localStorage.setItem('sammo-game-token', token);
window.localStorage.setItem('sammo-game-profile', 'che:default');
}, gameToken);
};
test('removes an invalid ga_ token and redirects an authenticated route to public', async ({
page,
}) => {
await seedGameStorage(page, 'ga_invalid');
let gatewayRequests = 0;
await page.route('http://127.0.0.1:15120/api/trpc/**', async (route) => {
gatewayRequests += 1;
await route.abort();
});
await page.route('**/che/api/trpc/**', async (route) => {
const operations = operationNames(route);
if (operations.includes('auth.status')) {
expect(route.request().headers().authorization).toBe('Bearer ga_invalid');
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'invalid token' }),
});
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(operations.map(publicResponse)),
});
});
await page.goto('select-general');
await expect(page).toHaveURL(/\/che\/public$/);
await expect(page.getByRole('heading', { name: '공개 동향' })).toBeVisible();
expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBeNull();
expect(gatewayRequests).toBe(0);
});
test('keeps a valid ga_ token when only lobby.info is unavailable', async ({ page }) => {
await seedGameStorage(page, 'ga_valid');
page.on('dialog', (dialog) => dialog.accept());
let gatewayRequests = 0;
let statusRequests = 0;
let lobbyRequests = 0;
await page.route('http://127.0.0.1:15120/api/trpc/**', async (route) => {
gatewayRequests += 1;
await route.abort();
});
await page.route('**/che/api/trpc/**', async (route) => {
const operations = operationNames(route);
if (operations.includes('auth.status')) {
statusRequests += 1;
expect(route.request().headers().authorization).toBe('Bearer ga_valid');
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([response({ userId: 'valid-user' })]),
});
return;
}
if (operations.includes('lobby.info')) {
lobbyRequests += 1;
await route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({ error: 'lobby unavailable' }),
});
return;
}
await route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({ error: 'fixture page data unavailable' }),
});
});
await page.goto('select-general');
await expect(page).toHaveURL(/\/che\/select-general$/);
await expect(page.locator('.page-title')).toContainText('장 수 선 택');
expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBe(
'ga_valid'
);
expect(statusRequests).toBe(1);
expect(lobbyRequests).toBe(1);
expect(gatewayRequests).toBe(0);
});
@@ -206,6 +206,21 @@ const officerLevelOptions = [
<span>사기</span>
<input v-model.number="general.atmos" type="number" min="40" :max="options.config.maxAtmosByWar" />
</label>
<label class="field">
<span>내정특기</span>
<select v-model="general.special">
<option :value="null">-</option>
<option
v-for="trait in options.eventDomesticTraits"
:key="trait.key"
:value="trait.key"
>
{{ trait.name }}
</option>
</select>
</label>
</div>
<div class="form-row">
<label class="field">
<span>전특</span>
<select v-model="general.special2">
+9
View File
@@ -3,6 +3,7 @@ import MainView from '../views/MainView.vue';
import PublicView from '../views/PublicView.vue';
import LoginView from '../views/LoginView.vue';
import JoinView from '../views/JoinView.vue';
import SelectGeneralView from '../views/SelectGeneralView.vue';
import InheritView from '../views/InheritView.vue';
import AuctionView from '../views/AuctionView.vue';
import NationCitiesView from '../views/NationCitiesView.vue';
@@ -92,6 +93,14 @@ const routes = [
requiresNoGeneral: true,
},
},
{
path: '/select-general',
name: 'select-general',
component: SelectGeneralView,
meta: {
requiresAuth: true,
},
},
{
path: '/inherit',
name: 'inherit',
+35 -2
View File
@@ -101,7 +101,7 @@ export const useSessionStore = defineStore('session', {
},
async refreshGeneralStatus() {
if (!this.gameToken) {
this.status = 'authed';
this.status = this.sessionToken ? 'authed' : 'public';
return;
}
if (!isAccessToken(this.gameToken)) {
@@ -111,11 +111,44 @@ export const useSessionStore = defineStore('session', {
return;
}
}
try {
await gameTrpc.auth.status.query();
} catch {
this.setGameToken(null);
if (this.sessionToken && this.profile) {
try {
const issued = await gatewayTrpc.auth.issueGameSession.mutate({
sessionToken: this.sessionToken,
profile: this.profile,
});
this.setGameToken(issued.gameToken);
if (await this.exchangeGatewayToken()) {
await gameTrpc.auth.status.query();
} else {
throw new Error('Game token exchange failed.');
}
} catch {
this.setGameToken(null);
this.error = 'game_session_invalid';
this.status = 'public';
return;
}
} else {
this.error = 'game_session_invalid';
this.status = 'public';
return;
}
}
try {
const lobby = await gameTrpc.lobby.info.query();
this.status = lobby.myGeneral ? 'general' : 'authed';
} catch {
this.error = 'game_status_unavailable';
this.error = 'game_lobby_unavailable';
if (this.status === 'unknown' || this.status === 'public') {
this.status = 'authed';
}
}
},
async exchangeGatewayToken(): Promise<boolean> {
@@ -23,6 +23,7 @@ export type GeneralDraft = {
injury: number;
rice: number;
personal: string | null;
special: string | null;
special2: string | null;
crew: number;
crewtype: number;
@@ -57,6 +58,7 @@ export type BattleSimOptions = {
crewTypes: Array<{ id: number; name: string; armType: number }>;
};
nationTypes: Array<{ key: string; name: string; info: string }>;
eventDomesticTraits: Array<{ key: string; name: string; info: string }>;
warTraits: Array<{ key: string; name: string; info: string }>;
personalities: Array<{ key: string; name: string; info: string }>;
items: {
@@ -0,0 +1,22 @@
const KOREA_TIME_OFFSET_MS = 9 * 60 * 60 * 1000;
const pad = (value: number): string => String(value).padStart(2, '0');
export const formatSeoulDateTime = (value: string | Date): string => {
if (
typeof value === 'string' &&
!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(value.trim())
) {
return value.trim().replace('T', ' ').slice(0, 19);
}
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return typeof value === 'string' ? value.slice(0, 19) : '';
}
const koreaTime = new Date(date.getTime() + KOREA_TIME_OFFSET_MS);
return `${koreaTime.getUTCFullYear()}-${pad(koreaTime.getUTCMonth() + 1)}-${pad(
koreaTime.getUTCDate()
)} ${pad(koreaTime.getUTCHours())}:${pad(koreaTime.getUTCMinutes())}:${pad(
koreaTime.getUTCSeconds()
)}`;
};
@@ -134,6 +134,7 @@ const createGeneralDraft = (overrides?: Partial<GeneralDraft>): GeneralDraft =>
injury: 0,
rice: 5000,
personal: null,
special: null,
special2: null,
crew: 7000,
crewtype: baseCrew,
@@ -193,6 +194,7 @@ const applyGeneralExport = (target: GeneralDraft, data: GeneralExport) => {
target.injury = data.injury;
target.rice = data.rice;
target.personal = data.personal;
target.special = data.special;
target.special2 = data.special2;
target.crew = data.crew;
target.crewtype = data.crewtype;
@@ -228,6 +230,7 @@ const toExportedGeneral = (general: GeneralDraft): GeneralExport => ({
injury: general.injury,
rice: general.rice,
personal: general.personal,
special: general.special,
special2: general.special2,
crew: general.crew,
crewtype: general.crewtype,
@@ -401,6 +404,7 @@ const normalizeGeneralExport = (raw: Record<string, unknown>): GeneralExport =>
injury: readNumberValue(raw.injury, 0),
rice: readNumberValue(raw.rice, 0),
personal: readOptionalString(raw.personal),
special: readOptionalString(raw.special),
special2: readOptionalString(raw.special2),
crew: readNumberValue(raw.crew, 0),
crewtype: readNumberValue(raw.crewtype, 0),
@@ -438,6 +442,7 @@ const buildGeneralPayload = (
nation: nationId,
turntime: timestamp,
personal: general.personal,
special: general.special,
special2: general.special2,
crew: general.crew,
crewtype: general.crewtype,
@@ -872,6 +877,7 @@ const applyServerGeneral = async (target: GeneralDraft, generalId: number) => {
injury: response.general.injury,
rice: response.general.rice,
personal: response.general.personal,
special: response.general.special,
special2: response.general.special2,
crew: response.general.crew,
crewtype: response.general.crewtype,
+4
View File
@@ -194,6 +194,10 @@ const loadConfig = async () => {
error.value = null;
try {
const config = await trpc.join.getConfig.query();
if (config.selectionPool.enabled) {
await router.replace({ name: 'select-general' });
return;
}
joinConfig.value = config;
form.value.name = config.user.displayName || '';
applyBalancedStats();
+33 -1
View File
@@ -2,6 +2,7 @@
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { trpc } from '../utils/trpc';
import { formatLog } from '../utils/formatLog';
import { formatSeoulDateTime } from '../utils/legacyDateTime';
import { isDefenceTrainPenaltyWaivedByScenarioEffect } from '@sammo-ts/logic';
const SCREEN_MODE_KEY = 'sam.screenMode';
@@ -10,6 +11,7 @@ type ScreenMode = 'auto' | '500px' | '1000px';
type LogType = 'generalHistory' | 'battleDetail' | 'battleResult' | 'generalAction';
type ItemSlotKey = 'horse' | 'weapon' | 'book' | 'item';
type MyGeneralResponse = Awaited<ReturnType<typeof trpc.general.me.query>>;
type SelectionPoolStatus = Awaited<ReturnType<typeof trpc.join.getConfig.query>>['selectionPool'];
type WorldSnapshot = {
currentYear: number;
@@ -28,6 +30,7 @@ type SettingForm = {
const data = ref<MyGeneralResponse | null>(null);
const world = ref<WorldSnapshot>(null);
const selectionPoolStatus = ref<SelectionPoolStatus | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const screenMode = ref<ScreenMode>('auto');
@@ -130,6 +133,11 @@ const actionAvailability = computed(() => {
selectOtherGeneral: Boolean(npcMode === 2 && general?.npcState === 0),
};
});
const formatSelectionAvailableAt = computed(() => {
const value = selectionPoolStatus.value?.nextChangeAt;
if (!value) return '';
return formatSeoulDateTime(value);
});
const applyCustomCss = (text: string) => {
let style = document.getElementById('sammo-custom-css') as HTMLStyleElement | null;
@@ -161,12 +169,14 @@ const loadPage = async () => {
loading.value = true;
error.value = null;
try {
const [general, state] = await Promise.all([
const [general, state, joinConfig] = await Promise.all([
trpc.general.me.query(),
trpc.world.getState.query() as Promise<WorldSnapshot>,
trpc.join.getConfig.query(),
]);
data.value = general;
world.value = state;
selectionPoolStatus.value = joinConfig.selectionPool;
if (general) {
Object.assign(form, general.settings);
}
@@ -399,6 +409,20 @@ onMounted(() => {
접경 귀환
</button>
</div>
<div
v-if="actionAvailability.selectOtherGeneral && selectionPoolStatus?.enabled"
class="action-line"
>
다른 장수 선택
<template v-if="formatSelectionAvailableAt">
({{ formatSelectionAvailableAt }} 부터)
</template>
<br />
<RouterLink class="action-button select-general-link" to="/select-general">
다른 장수 선택
</RouterLink>
<br /><br />
</div>
<div class="screen-mode-row">
<span>500px/1000px 모드<br />(모바일 전용, 즉시 설정)</span>
@@ -612,6 +636,14 @@ dt {
margin: 4px 0;
background: #225500;
}
.select-general-link {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
color: #fff;
text-decoration: none;
}
.action-line {
margin: 12px 0;
}
@@ -0,0 +1,728 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useSessionStore } from '../stores/session';
import { formatSeoulDateTime } from '../utils/legacyDateTime';
import { trpc } from '../utils/trpc';
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
type Reservation = Awaited<ReturnType<typeof trpc.join.getSelectionPool.mutate>>;
type Candidate = Reservation['candidates'][number];
type Nation = JoinConfig['nations'][number];
type PendingSelectionAction = {
operation: 'create' | 'reselect';
uniqueName: string;
personality?: string;
clientRequestId: string;
};
const router = useRouter();
const session = useSessionStore();
const config = ref<JoinConfig | null>(null);
const reservation = ref<Reservation | null>(null);
const selectedUniqueName = ref<string | null>(null);
const nations = ref<Nation[]>([]);
const personality = ref('Random');
const loading = ref(true);
const submitting = ref(false);
const error = ref('');
const now = ref(Date.now());
let timer: number | null = null;
const pendingActionStorageKey = 'sammo-select-pool-pending-action';
const candidates = computed(() => reservation.value?.candidates ?? []);
const selectedCandidate = computed(
() => candidates.value.find((candidate) => candidate.uniqueName === selectedUniqueName.value) ?? null
);
const hasGeneral = computed(() => reservation.value?.hasGeneral ?? config.value?.selectionPool.hasGeneral ?? false);
const allowPersonality = computed(() => config.value?.selectionPool.allowOptions.includes('ego') ?? false);
const personalities = computed(() => config.value?.personalities ?? []);
const serverInfo = computed(() => config.value?.serverInfo ?? null);
const validUntil = computed(() => {
const value = reservation.value?.validUntil;
return value ? new Date(value).getTime() : 0;
});
const expired = computed(() => validUntil.value > 0 && now.value > validUntil.value);
const validUntilColor = computed(() => {
const remaining = validUntil.value - now.value;
if (remaining <= 0 || remaining > 30_000) {
return '#fff';
}
const channel = Math.max(0, Math.round((255 * remaining) / 30_000));
return `rgb(255, ${channel}, ${channel})`;
});
const errorText = (value: unknown): string =>
value instanceof Error ? value.message : typeof value === 'string' ? value : 'unknown_error';
const readPendingAction = (): PendingSelectionAction | null => {
try {
const raw = window.sessionStorage.getItem(pendingActionStorageKey);
if (!raw) return null;
const value = JSON.parse(raw) as Partial<PendingSelectionAction>;
if (
(value.operation !== 'create' && value.operation !== 'reselect') ||
typeof value.uniqueName !== 'string' ||
typeof value.clientRequestId !== 'string'
) {
return null;
}
return value as PendingSelectionAction;
} catch {
return null;
}
};
const getPendingAction = (
operation: PendingSelectionAction['operation'],
uniqueName: string,
requestedPersonality?: string
): PendingSelectionAction => {
const current = readPendingAction();
if (
current?.operation === operation &&
current.uniqueName === uniqueName &&
current.personality === requestedPersonality
) {
return current;
}
const next: PendingSelectionAction = {
operation,
uniqueName,
...(requestedPersonality ? { personality: requestedPersonality } : {}),
clientRequestId: crypto.randomUUID(),
};
window.sessionStorage.setItem(pendingActionStorageKey, JSON.stringify(next));
return next;
};
const clearPendingAction = (action: PendingSelectionAction): void => {
if (readPendingAction()?.clientRequestId === action.clientRequestId) {
window.sessionStorage.removeItem(pendingActionStorageKey);
}
};
const isIndeterminateTimeout = (value: unknown): boolean => {
if (!value || typeof value !== 'object' || !('data' in value)) return false;
const data = value.data;
return Boolean(
data &&
typeof data === 'object' &&
'code' in data &&
data.code === 'TIMEOUT'
);
};
const formatDateTime = (value: string | null | undefined): string => {
if (!value) return '';
return formatSeoulDateTime(value);
};
const shuffleNations = (source: Nation[]): Nation[] => {
const shuffled = [...source];
for (let index = shuffled.length - 1; index > 0; index -= 1) {
const random = new Uint32Array(1);
crypto.getRandomValues(random);
const swapIndex = random[0]! % (index + 1);
[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex]!, shuffled[index]!];
}
return shuffled;
};
const userIconBaseUrl =
import.meta.env.VITE_GATEWAY_USER_ICON_BASE_URL ?? '/gateway/api/user-icons';
const imageUrl = (candidate: Candidate): string =>
candidate.imageServer
? `${userIconBaseUrl.replace(/\/$/, '')}/${candidate.picture}`
: `/image/icons/${candidate.picture}`;
const useFallbackImage = (event: Event): void => {
const image = event.currentTarget as HTMLImageElement;
if (image.dataset.fallbackApplied === 'true') return;
image.dataset.fallbackApplied = 'true';
image.src = '/image/icons/default.jpg';
};
const personalityName = (key: string | null): string | null => {
if (!key) return null;
return personalities.value.find((entry) => entry.key === key)?.name ?? key;
};
const personalityInfo = (key: string | null): string =>
key ? personalities.value.find((entry) => entry.key === key)?.info ?? '' : '';
const lightTextNationColors = new Set([
'',
'#330000',
'#FF0000',
'#800000',
'#A0522D',
'#FF6347',
'#808000',
'#008000',
'#2E8B57',
'#008080',
'#6495ED',
'#0000FF',
'#000080',
'#483D8B',
'#7B68EE',
'#800080',
'#A9A9A9',
'#000000',
]);
const nationTextColor = (color: string): string =>
lightTextNationColors.has(color.toUpperCase()) ? '#FFFFFF' : '#000000';
const selectCandidate = async (candidate: Candidate): Promise<void> => {
if (!hasGeneral.value) {
selectedUniqueName.value = candidate.uniqueName;
return;
}
if (!confirm(`이 장수를 선택할까요? : ${candidate.generalName}`)) {
return;
}
submitting.value = true;
const pending = getPendingAction('reselect', candidate.uniqueName);
try {
await trpc.join.reselectPoolGeneral.mutate({
uniqueName: candidate.uniqueName,
clientRequestId: pending.clientRequestId,
});
clearPendingAction(pending);
alert('선택한 장수로 변경했습니다.');
await session.refreshGeneralStatus();
await router.push('/');
} catch (cause) {
console.error(cause);
if (!isIndeterminateTimeout(cause)) {
clearPendingAction(pending);
}
alert(`실패했습니다: ${errorText(cause)}`);
await loadPage();
} finally {
submitting.value = false;
}
};
const createGeneral = async (): Promise<void> => {
const candidate = selectedCandidate.value;
if (!candidate) {
alert('장수를 선택해주세요!');
return;
}
if (!confirm('이 장수로 생성할까요?')) {
return;
}
submitting.value = true;
const pending = getPendingAction('create', candidate.uniqueName, personality.value);
try {
await trpc.join.selectPoolGeneral.mutate({
uniqueName: candidate.uniqueName,
personality: personality.value,
clientRequestId: pending.clientRequestId,
});
clearPendingAction(pending);
alert('선택한 장수로 생성했습니다.');
await session.refreshGeneralStatus();
await router.push('/');
} catch (cause) {
console.error(cause);
if (!isIndeterminateTimeout(cause)) {
clearPendingAction(pending);
}
alert(`실패했습니다: ${errorText(cause)}`);
await loadPage();
} finally {
submitting.value = false;
}
};
async function loadPage(): Promise<void> {
loading.value = true;
error.value = '';
selectedUniqueName.value = null;
try {
const nextConfig = await trpc.join.getConfig.query();
config.value = nextConfig;
nations.value = shuffleNations(nextConfig.nations);
if (!nextConfig.selectionPool.enabled) {
await router.replace(nextConfig.selectionPool.hasGeneral ? '/' : '/join');
return;
}
reservation.value = await trpc.join.getSelectionPool.mutate();
now.value = Date.now();
} catch (cause) {
console.error(cause);
error.value = errorText(cause);
alert(`실패했습니다: ${error.value}`);
} finally {
loading.value = false;
}
}
const goBack = (): void => {
if (window.history.length > 1) {
router.back();
return;
}
void router.push(hasGeneral.value ? '/' : '/join');
};
onMounted(() => {
timer = window.setInterval(() => {
now.value = Date.now();
}, 1_000);
void loadPage();
});
onBeforeUnmount(() => {
if (timer !== null) {
window.clearInterval(timer);
}
});
</script>
<template>
<main class="select-pool-page legacy-bg0">
<header class="page-title with-border">
<br />
<button class="legacy-button" type="button" @click="goBack">돌아가기</button>
</header>
<table v-if="serverInfo" class="server-info-table legacy-bg0">
<tbody>
<tr>
<td>
현재 : {{ serverInfo.currentYear }} {{ serverInfo.currentMonth }}
(<span class="cyan">{{ serverInfo.tickMinutes }} </span> 서버)<br />
등록 장수 : 유저 {{ serverInfo.userGeneralCount }} / {{ serverInfo.maxGeneral }} +
<span class="cyan">NPC {{ serverInfo.npcGeneralCount }} </span>
</td>
</tr>
</tbody>
</table>
<table class="invitation-table legacy-bg0">
<thead>
<tr>
<td colspan="2" class="legacy-bg1">임관 권유 메시지</td>
</tr>
</thead>
<tbody>
<tr
v-for="nation in nations"
:key="nation.id"
:style="{
color: nationTextColor(nation.color),
backgroundColor: nation.color,
}"
>
<td class="invitation-nation">{{ nation.name }}</td>
<td><div class="invitation-message">{{ nation.scoutMessage ?? '-' }}</div></td>
</tr>
</tbody>
</table>
<section class="selection-section">
<h1 class="section-title legacy-bg1 with-border">장수 선택</h1>
<div class="selection-body with-border">
<div v-if="loading">불러오는 중...</div>
<div v-else-if="error" class="error-text">{{ error }}</div>
<template v-else-if="reservation">
<small v-if="!expired">
(<span :style="{ color: validUntilColor }">{{ formatDateTime(reservation.validUntil) }}</span
>까지 유효)
</small>
<small v-else class="expired-text">- 만료 -</small>
<br />
<div class="card-holder">
<article
v-for="candidate in candidates"
:key="candidate.uniqueName"
class="general-card"
>
<h4 class="legacy-bg1 with-border">{{ candidate.generalName }}</h4>
<h4 class="portrait">
<img
:src="imageUrl(candidate)"
:alt="candidate.generalName"
width="64"
height="64"
@error="useFallbackImage"
/>
</h4>
<p>
{{ candidate.leadership }} / {{ candidate.strength }} / {{ candidate.intel }}<br />
<span v-if="candidate.ego" class="trait-tooltip" tabindex="0">
{{ personalityName(candidate.ego) }}
<span role="tooltip">{{ personalityInfo(candidate.ego) }}</span>
</span>
<br v-if="candidate.ego" />
<span class="trait-tooltip" tabindex="0">
{{ candidate.specialDomesticName }}
<span role="tooltip">{{ candidate.specialDomesticInfo }}</span>
</span>
/
<span>{{ candidate.specialWar ?? '-' }}</span
><br /><br />
보병: {{ Math.trunc(candidate.dex[0] / 1000) }}K<br />
궁병: {{ Math.trunc(candidate.dex[1] / 1000) }}K<br />
기병: {{ Math.trunc(candidate.dex[2] / 1000) }}K<br />
귀병: {{ Math.trunc(candidate.dex[3] / 1000) }}K<br />
차병: {{ Math.trunc(candidate.dex[4] / 1000) }}K<br />
</p>
<button
class="select-button with-border"
type="button"
:disabled="submitting"
@click="selectCandidate(candidate)"
>
선택하기
</button>
</article>
</div>
</template>
</div>
</section>
<section v-if="reservation && !hasGeneral" class="create-section">
<h1 class="section-title legacy-bg1 with-border">장수 생성</h1>
<div class="create-body with-border">
<div id="left-pad">
<article v-if="selectedCandidate" class="general-card selected-card">
<h4 class="legacy-bg1 with-border">{{ selectedCandidate.generalName }}</h4>
<h4 class="portrait">
<img
:src="imageUrl(selectedCandidate)"
:alt="selectedCandidate.generalName"
width="64"
height="64"
@error="useFallbackImage"
/>
</h4>
<p>
{{ selectedCandidate.leadership }} / {{ selectedCandidate.strength }} /
{{ selectedCandidate.intel }}<br />
<span v-if="selectedCandidate.ego" class="trait-tooltip" tabindex="0">
{{ personalityName(selectedCandidate.ego) }}
<span role="tooltip">{{ personalityInfo(selectedCandidate.ego) }}</span>
</span>
<br v-if="selectedCandidate.ego" />
<span class="trait-tooltip" tabindex="0">
{{ selectedCandidate.specialDomesticName }}
<span role="tooltip">{{ selectedCandidate.specialDomesticInfo }}</span>
</span>
/
<span>{{ selectedCandidate.specialWar ?? '-' }}</span
><br /><br />
보병: {{ Math.trunc(selectedCandidate.dex[0] / 1000) }}K<br />
궁병: {{ Math.trunc(selectedCandidate.dex[1] / 1000) }}K<br />
기병: {{ Math.trunc(selectedCandidate.dex[2] / 1000) }}K<br />
귀병: {{ Math.trunc(selectedCandidate.dex[3] / 1000) }}K<br />
차병: {{ Math.trunc(selectedCandidate.dex[4] / 1000) }}K<br />
</p>
<button class="select-button with-border" type="button">선택하기</button>
</article>
<template v-else>장수를<br />선택해주세요!</template>
</div>
<form class="custom-form" @submit.prevent="createGeneral">
<table>
<tbody>
<tr v-if="allowPersonality">
<th class="legacy-bg1">성격</th>
<td>
<select v-model="personality">
<option value="Random">????</option>
<option
v-for="entry in personalities.filter((item) => item.key !== 'Random')"
:key="entry.key"
:value="entry.key"
>
{{ entry.name }}
</option>
</select>
<span>
{{ personalities.find((entry) => entry.key === personality)?.info ?? '' }}
</span>
</td>
</tr>
<tr>
<td colspan="2" class="join-guidance">
임의의 도시에서 재야로 시작하며 건국과 임관은 게임 내에서 실행합니다.
</td>
</tr>
<tr>
<td class="create-action">
<button
id="build-general"
class="legacy-button"
type="submit"
:disabled="submitting"
>
장수생성
</button>
</td>
<td>
<button
class="legacy-button"
type="reset"
:disabled="submitting"
@click="personality = 'Random'"
>
다시입력
</button>
</td>
</tr>
</tbody>
</table>
</form>
</div>
</section>
<footer class="page-footer">
<div class="footer-back with-border">
<button class="legacy-button" type="button" @click="goBack">돌아가기</button>
</div>
<div class="footer-banner with-border">
<small>
삼국지 모의전투 HiDCHe core2026 / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
HideD /
<a
href="https://sam.hided.net/wiki/hidche/credit"
target="_blank"
rel="noopener noreferrer"
>Credit</a
>
</small>
</div>
</footer>
</main>
</template>
<style scoped>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
.select-pool-page {
width: 1000px;
min-width: 1000px;
margin: 8px auto 0;
color: #fff;
line-height: 1.3;
text-align: center;
overflow: visible;
}
.with-border {
border: solid 1px;
border-top-color: gray;
border-left-color: gray;
border-right-color: #000;
border-bottom-color: #000;
}
.page-title {
padding: 0;
text-align: left;
}
.page-title .legacy-button,
.footer-back .legacy-button {
padding: 1px 6px;
font-weight: 400;
line-height: 1.3;
}
.server-info-table,
.invitation-table {
border: 1px solid;
border-collapse: collapse;
border-top-color: gray;
border-left-color: gray;
border-right-color: #000;
border-bottom-color: #000;
border-spacing: 0;
font-size: 14px;
text-align: center;
word-break: break-all;
}
.server-info-table {
width: 100%;
}
.server-info-table td,
.invitation-table td {
border: 1px solid gray;
padding: 0;
}
.server-info-table td {
padding: 1px 0;
}
.invitation-table {
margin: 0 auto;
}
.invitation-nation {
width: 130px;
}
.invitation-message {
width: 870px;
max-width: 870px;
max-height: 200px;
overflow: hidden;
}
.cyan {
color: cyan;
}
.selection-section,
.create-section {
margin: 0;
}
.section-title {
margin: 0;
padding: 0;
color: inherit;
font-size: 14px;
font-weight: 700;
line-height: 18.2px;
}
.selection-body {
padding: 0;
}
.card-holder {
text-align: center;
white-space: normal;
}
.general-card {
width: 125px;
display: inline-block;
box-sizing: content-box;
border: solid 1px;
border-top-color: gray;
border-left-color: gray;
border-right-color: #000;
border-bottom-color: #000;
vertical-align: top;
}
.general-card h4,
.general-card p {
margin: 0;
}
.general-card h4 {
padding: 0;
}
.portrait {
text-align: center;
}
.portrait img {
display: inline;
width: 64px;
height: 64px;
vertical-align: baseline;
object-fit: fill;
}
.select-button {
width: 100%;
height: 19px;
padding: 0 4px;
border-radius: 0;
background: #191919;
color: #fff;
line-height: normal;
}
.expired-text,
.error-text {
color: red;
}
.create-section {
margin-top: 10px;
}
.create-body {
display: flex;
text-align: left;
}
#left-pad {
flex: 1;
padding-top: 8px;
text-align: center;
}
.selected-card .select-button {
display: none;
}
.custom-form {
flex: 4;
}
.custom-form table {
width: 100%;
border-collapse: collapse;
}
.custom-form th,
.custom-form td {
padding: 0;
text-align: left;
}
.custom-form th {
width: 200px;
text-align: right;
}
.custom-form select {
color: #fff;
background: #000;
}
.custom-form .legacy-button {
padding: 3px 6px;
font-weight: 400;
}
.join-guidance {
text-align: center;
}
.create-action {
width: 200px;
text-align: right;
}
.footer-back,
.footer-banner {
text-align: left;
}
.footer-banner a {
color: #fff;
text-decoration: underline;
}
button:disabled {
cursor: default;
opacity: 0.55;
}
.select-button:disabled {
background: #333;
}
.select-button:focus-visible,
.custom-form select:focus-visible,
.trait-tooltip:focus-visible {
outline: auto 1px;
outline-offset: 0;
}
.trait-tooltip {
position: relative;
cursor: help;
}
.trait-tooltip [role='tooltip'] {
display: none;
position: absolute;
z-index: 20;
left: 50%;
bottom: calc(100% + 4px);
width: 220px;
padding: 5px 7px;
transform: translateX(-50%);
border: 1px solid #888;
background: #202020;
color: #fff;
text-align: left;
white-space: normal;
word-break: keep-all;
}
.trait-tooltip:hover [role='tooltip'],
.trait-tooltip:focus [role='tooltip'] {
display: block;
}
@media (max-width: 1000px) {
.select-pool-page {
margin-left: 8px;
margin-right: 0;
}
}
</style>
@@ -45,6 +45,7 @@ export class InMemoryGatewaySessionService implements GatewaySessionService {
sanctions: user.sanctions,
createdAt: user.createdAt,
issuedAt: new Date().toISOString(),
legacyMemberNo: user.legacyMemberNo,
};
this.sessions.set(sessionToken, {
info,
@@ -99,6 +100,7 @@ export class InMemoryGatewaySessionService implements GatewaySessionService {
sanctions: session.sanctions,
createdAt: session.createdAt,
issuedAt: new Date().toISOString(),
legacyMemberNo: session.legacyMemberNo,
};
const key = buildGameKey(profile, gameToken);
this.gameSessions.set(key, {
@@ -17,6 +17,14 @@ const readObject = <T extends object>(value: unknown, fallback: T): T => {
return value as T;
};
const readLegacyMemberNo = (value: unknown): number | undefined => {
const legacyData = readObject<Record<string, unknown>>(value, {});
const memberNo = legacyData.memberNo;
return typeof memberNo === 'number' && Number.isSafeInteger(memberNo) && memberNo > 0
? memberNo
: undefined;
};
const mapUser = (row: {
id: string;
loginId: string;
@@ -39,6 +47,7 @@ const mapUser = (row: {
kakaoGraceStartedAt: Date;
deleteAfter: Date | null;
createdAt: Date;
legacyData: GatewayPrisma.JsonValue;
}): UserRecord => ({
id: row.id,
username: row.loginId,
@@ -61,6 +70,7 @@ const mapUser = (row: {
passwordHash: row.passwordHash,
passwordSalt: row.passwordSalt,
createdAt: row.createdAt.toISOString(),
legacyMemberNo: readLegacyMemberNo(row.legacyData),
});
export const createPostgresUserRepository = (
@@ -56,6 +56,7 @@ export class RedisGatewaySessionService implements GatewaySessionService {
sanctions: user.sanctions,
createdAt: user.createdAt,
issuedAt: new Date().toISOString(),
legacyMemberNo: user.legacyMemberNo,
};
await this.client.set(this.keys.sessionKey(sessionToken), JSON.stringify(info), {
EX: this.sessionTtlSeconds,
@@ -108,6 +109,7 @@ export class RedisGatewaySessionService implements GatewaySessionService {
sanctions: session.sanctions,
createdAt: session.createdAt,
issuedAt: new Date().toISOString(),
legacyMemberNo: session.legacyMemberNo,
};
const gameKey = this.keys.gameSessionKey(profile, gameToken);
const gameSetKey = this.keys.sessionGameSetKey(sessionToken);
@@ -9,6 +9,7 @@ export interface GatewaySessionInfo {
sanctions: UserSanctions;
createdAt: string;
issuedAt: string;
legacyMemberNo?: number;
}
export interface GameSessionInfo {
@@ -22,6 +23,7 @@ export interface GameSessionInfo {
sanctions: UserSanctions;
createdAt: string;
issuedAt: string;
legacyMemberNo?: number;
}
export interface GatewaySessionConfig {
@@ -20,6 +20,7 @@ export interface UserRecord {
passwordHash: string;
passwordSalt: string;
createdAt: string;
legacyMemberNo?: number;
}
export interface PublicUser {
+1
View File
@@ -672,6 +672,7 @@ export const appRouter = router({
displayName: gameSession.displayName,
roles: gameSession.roles,
createdAt: gameSession.createdAt,
legacyMemberNo: gameSession.legacyMemberNo,
},
sanctions: gameSession.sanctions,
identity: {
+28
View File
@@ -516,6 +516,34 @@ describe('gateway auth flow', () => {
expect(validated?.user.username).toBe('tester');
});
it('keeps the migrated member number inside the encrypted game identity', async () => {
const { caller, users, sessions } = buildCaller();
const user = await users.createUser({
username: 'legacy-seed-user',
password: 'secretpass',
});
user.legacyMemberNo = 42;
const session = await sessions.createSession(user);
const issued = await caller.auth.issueGameSession({
sessionToken: session.sessionToken,
profile: 'che:default',
});
const payload = decryptGameSessionToken(issued.gameToken, 'test-secret');
expect(payload?.user.legacyMemberNo).toBe(42);
const validated = await caller.auth.validateGameSession({
profile: 'che:default',
gameToken: issued.gameToken,
});
expect(validated).toMatchObject({
user: {
id: user.id,
},
});
expect(validated?.user).not.toHaveProperty('legacyMemberNo');
});
it('revokes the gateway session and every linked game session on logout', async () => {
const { caller, users, sessions } = buildCaller();
const user = await users.createUser({
@@ -0,0 +1,159 @@
import { expect, test, type Page, type Route } from '@playwright/test';
const response = (data: unknown) => ({ result: { data } });
const operationNames = (route: Route): string[] => {
const url = new URL(route.request().url());
return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(',');
};
const fulfillTrpc = async (route: Route, results: unknown[]): Promise<void> => {
await route.fulfill({
status: 200,
contentType: 'application/json',
headers: {
'access-control-allow-origin': '*',
},
body: JSON.stringify(results),
});
};
const installFixture = async (page: Page) => {
const gameOperations: Array<{ operation: string; authorization: string | undefined }> = [];
await page.addInitScript(() => {
window.localStorage.setItem('sammo-session-token', 'gateway-lobby-session');
});
await page.route('http://127.0.0.1:15130/api/trpc/**', async (route) => {
const results = operationNames(route).map((operation) => {
if (operation === 'me') {
return response({
id: 'lobby-user',
username: 'lobby-user',
displayName: '로비사용자',
roles: ['user'],
kakaoVerified: true,
createdAt: '2026-07-30T00:00:00.000Z',
});
}
if (operation === 'lobby.notice') {
return response('');
}
if (operation === 'lobby.profiles') {
return response([
{
profileName: 'hwe:903',
profile: 'hwe',
scenario: '903',
status: 'RUNNING',
apiPort: 15015,
runtime: {
apiRunning: true,
daemonRunning: true,
auctionRunning: true,
battleSimRunning: true,
tournamentRunning: true,
},
korName: 'hwe',
color: '#ffffff',
localAccountPolicy: {
accessAllowed: true,
canCreateGeneral: true,
requiresKakaoVerification: false,
graceEndsAt: null,
},
},
]);
}
if (operation === 'auth.issueGameSession') {
return response({
profile: 'hwe:903',
gameToken: 'encrypted-gateway-game-token',
expiresAt: '2026-07-30T01:00:00.000Z',
});
}
throw new Error(`Unhandled gateway tRPC operation: ${operation}`);
});
await fulfillTrpc(route, results);
});
await page.route('http://localhost:15015/api/trpc/**', async (route) => {
const authorization = route.request().headers().authorization;
const results = operationNames(route).map((operation) => {
gameOperations.push({ operation, authorization });
if (operation === 'auth.exchangeGatewayToken') {
return response({
accessToken: 'ga_lobby-access-token',
profile: 'hwe:903',
expiresAt: '2026-07-30T01:00:00.000Z',
});
}
if (operation === 'lobby.info') {
return response({
year: 180,
month: 1,
userCnt: 1,
maxUserCnt: 500,
npcCnt: 0,
nationCnt: 0,
turnTerm: 5,
fictionMode: '가상',
starttime: '2026-07-30 00:00:00',
opentime: '2026-07-30 00:00:00',
turntime: '2026-07-30 00:05:00',
otherTextInfo: '',
isUnited: 0,
selectionPoolEnabled: true,
myGeneral: {
name: '선택장수',
picture: 'account-hash.png',
imageServer: 1,
},
});
}
if (operation === 'public.getMapLayout') {
return response({ mapName: 'che', cityList: [] });
}
if (operation === 'public.getCachedMap') {
return response({ year: 180, month: 1, cityList: [], nationList: [] });
}
throw new Error(`Unhandled game tRPC operation: ${operation}`);
});
await fulfillTrpc(route, results);
});
await page.route('**/gateway/api/user-icons/account-hash.png', async (route) => {
await route.fulfill({
status: 200,
contentType: 'image/png',
body: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64'
),
});
});
return gameOperations;
};
test('exchanges the gateway token before loading authenticated lobby general data', async ({
page,
}) => {
const gameOperations = await installFixture(page);
await page.goto('lobby');
const row = page.locator('tbody tr').filter({ hasText: 'hwe섭' });
await expect(row).toContainText('선택장수');
await expect(row.getByRole('button', { name: '입장' })).toBeVisible();
const portrait = row.locator('img');
await expect(portrait).toHaveAttribute(
'src',
'/gateway/api/user-icons/account-hash.png'
);
await expect.poll(() => portrait.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1);
expect(gameOperations.find(({ operation }) => operation === 'auth.exchangeGatewayToken')).toEqual({
operation: 'auth.exchangeGatewayToken',
authorization: undefined,
});
expect(gameOperations.find(({ operation }) => operation === 'lobby.info')).toEqual({
operation: 'lobby.info',
authorization: 'Bearer ga_lobby-access-token',
});
});
@@ -10,6 +10,7 @@ export default defineConfig({
'server-operations.spec.ts',
'admin-runtime-actions.spec.ts',
'lobby-admin-navigation.spec.ts',
'lobby-game-auth.spec.ts',
'logout.spec.ts',
],
fullyParallel: false,
+2 -1
View File
@@ -6,7 +6,7 @@ export type GameRouter = typeof appRouter;
const resolveProfileUrl = (template: string, profile: string): string =>
template.replaceAll('{profile}', encodeURIComponent(profile));
export const createGameTrpc = (profile: string, port: number) => {
export const createGameTrpc = (profile: string, port: number, gameToken?: string) => {
const urlTemplate = import.meta.env.VITE_GAME_API_URL_TEMPLATE;
const url = urlTemplate
? resolveProfileUrl(urlTemplate, profile)
@@ -15,6 +15,7 @@ export const createGameTrpc = (profile: string, port: number) => {
links: [
httpBatchLink({
url,
headers: gameToken ? { authorization: `Bearer ${gameToken}` } : undefined,
}),
],
});
+54 -5
View File
@@ -14,6 +14,7 @@ type GameRouterOutput = inferRouterOutputs<GameRouter>;
type MeOutput = GatewayRouterOutput['me'];
type LobbyProfile = GatewayRouterOutput['lobby']['profiles'][number];
type LobbyInfo = GameRouterOutput['lobby']['info'];
type LobbyGeneral = NonNullable<LobbyInfo['myGeneral']>;
type PublicMap = GameRouterOutput['public']['getCachedMap'];
type PublicMapLayout = GameRouterOutput['public']['getMapLayout'];
type MapPreviewBundle = {
@@ -38,9 +39,25 @@ const canAccessAdmin = computed(
) ?? false
);
const needsKakaoVerification = computed(() => me.value !== null && !me.value.kakaoVerified);
const userIconBaseUrl =
import.meta.env.VITE_GATEWAY_USER_ICON_BASE_URL ?? '/gateway/api/user-icons';
const formatGraceEndsAt = (value: string | null | undefined): string =>
value ? new Date(value).toLocaleString('ko-KR') : '';
const resolveGeneralPicture = (general: LobbyGeneral): string => {
const picture = general.picture?.trim() || 'default.jpg';
return general.imageServer
? `${userIconBaseUrl.replace(/\/$/, '')}/${encodeURIComponent(picture)}`
: `/image/icons/${encodeURIComponent(picture)}`;
};
const handleGeneralPictureError = (event: Event): void => {
const image = event.currentTarget as HTMLImageElement;
if (image.dataset.fallbackApplied === 'true') {
return;
}
image.dataset.fallbackApplied = 'true';
image.src = '/image/icons/default.jpg';
};
onMounted(async () => {
try {
@@ -52,12 +69,31 @@ onMounted(async () => {
notice.value = await trpc.lobby.notice.query();
profiles.value = await trpc.lobby.profiles.query();
const sessionToken = window.localStorage.getItem('sammo-session-token');
const detailTasks = profiles.value.map(async (profile) => {
if (profile.status !== 'RUNNING' && profile.status !== 'PREOPEN') {
return;
}
const gameTrpc = createGameTrpc(profile.profile, profile.apiPort);
const publicGameTrpc = createGameTrpc(profile.profile, profile.apiPort);
let gameToken: string | undefined;
if (sessionToken) {
try {
const issued = await trpc.auth.issueGameSession.mutate({
sessionToken,
profile: profile.profileName,
});
const exchanged = await publicGameTrpc.auth.exchangeGatewayToken.mutate({
gatewayToken: issued.gameToken,
});
gameToken = exchanged.accessToken;
} catch (error) {
console.error(`Failed to authenticate lobby game session for ${profile.profileName}`, error);
}
}
const gameTrpc = gameToken
? createGameTrpc(profile.profile, profile.apiPort, gameToken)
: publicGameTrpc;
const [infoResult, layoutResult, mapResult] = await Promise.allSettled([
gameTrpc.lobby.info.query(),
gameTrpc.public.getMapLayout.query(),
@@ -69,7 +105,6 @@ onMounted(async () => {
} else {
console.error(`Failed to fetch info for ${profile.profileName}`, infoResult.reason);
}
if (layoutResult.status === 'fulfilled' && mapResult.status === 'fulfilled') {
profileMapPreviews.value[profile.profileName] = {
mapLayout: layoutResult.value,
@@ -302,8 +337,13 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
class="w-12 h-12 mx-auto bg-zinc-800 rounded overflow-hidden border border-zinc-700"
>
<img
:src="profileDetails[profile.profileName]?.myGeneral?.picture ?? undefined"
:src="
resolveGeneralPicture(
profileDetails[profile.profileName]!.myGeneral!
)
"
class="w-full h-full object-cover"
@error="handleGeneralPictureError"
/>
</div>
</td>
@@ -332,12 +372,21 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
entryLoading[profile.profileName] ||
profile.localAccountPolicy?.canCreateGeneral === false
"
@click="handleEnter(profile, '/join')"
@click="
handleEnter(
profile,
profileDetails[profile.profileName]?.selectionPoolEnabled
? '/select-general'
: '/join'
)
"
>
{{
profile.localAccountPolicy?.canCreateGeneral === false
? '인증 필요'
: '장수생성'
: profileDetails[profile.profileName]?.selectionPoolEnabled
? '장수선택'
: '장수생성'
}}
</button>
</template>
+4 -1
View File
@@ -24,6 +24,7 @@ export interface GatewayUserInfo {
displayName: string;
roles: string[];
createdAt?: string;
legacyMemberNo?: number;
}
export interface GameSessionTokenPayload {
@@ -82,7 +83,9 @@ const parsePayload = (value: unknown): GameSessionTokenPayload | null => {
typeof user.id !== 'string' ||
typeof user.username !== 'string' ||
typeof user.displayName !== 'string' ||
!Array.isArray(user.roles)
!Array.isArray(user.roles) ||
(user.legacyMemberNo !== undefined &&
(!Number.isSafeInteger(user.legacyMemberNo) || user.legacyMemberNo <= 0))
) {
return null;
}
+38
View File
@@ -204,6 +204,22 @@ export type TurnDaemonCommand =
specialWar?: string;
};
}
| {
type: 'selectPoolCreate';
requestId?: string;
userId: string;
ownerDisplayName: string;
uniqueName: string;
personality: string;
seedOwnerIdentity: string | number;
}
| {
type: 'selectPoolReselect';
requestId?: string;
userId: string;
ownerDisplayName: string;
uniqueName: string;
}
| {
type: 'auctionBid';
requestId?: string;
@@ -460,6 +476,28 @@ export type TurnDaemonCommandResult =
generalId: number;
reason: string;
}
| {
type: 'selectPoolCreate';
ok: true;
generalId: number;
}
| {
type: 'selectPoolCreate';
ok: false;
code: 'BAD_REQUEST' | 'PRECONDITION_FAILED' | 'CONFLICT' | 'INTERNAL_SERVER_ERROR';
reason: string;
}
| {
type: 'selectPoolReselect';
ok: true;
generalId: number;
}
| {
type: 'selectPoolReselect';
ok: false;
code: 'BAD_REQUEST' | 'PRECONDITION_FAILED' | 'CONFLICT' | 'INTERNAL_SERVER_ERROR';
reason: string;
}
| {
type: 'auctionBid';
ok: true;
+13
View File
@@ -198,6 +198,19 @@ model General {
@@map("general")
}
model SelectPoolEntry {
id Int @id @default(autoincrement())
uniqueName String @unique @map("unique_name") @db.VarChar(20)
ownerUserId String? @map("owner_user_id")
generalId Int? @unique @map("general_id")
reservedUntil DateTime? @map("reserved_until")
info Json
@@index([ownerUserId])
@@index([reservedUntil, generalId])
@@map("select_pool")
}
model GeneralAccessLog {
id Int @id @default(autoincrement())
generalId Int @unique @map("general_id")
@@ -0,0 +1,17 @@
CREATE TABLE "select_pool" (
"id" SERIAL PRIMARY KEY,
"unique_name" VARCHAR(20) NOT NULL,
"owner_user_id" TEXT,
"general_id" INTEGER,
"reserved_until" TIMESTAMP(3),
"info" JSONB NOT NULL
);
CREATE UNIQUE INDEX "select_pool_unique_name_key"
ON "select_pool"("unique_name");
CREATE UNIQUE INDEX "select_pool_general_id_key"
ON "select_pool"("general_id");
CREATE INDEX "select_pool_owner_user_id_idx"
ON "select_pool"("owner_user_id");
CREATE INDEX "select_pool_reserved_until_general_id_idx"
ON "select_pool"("reserved_until", "general_id");
+1
View File
@@ -6,6 +6,7 @@ export interface DatabaseClient {
$executeRaw: GamePrismaClient['$executeRaw'];
worldState: GamePrisma.WorldStateDelegate;
general: GamePrisma.GeneralDelegate;
selectPoolEntry: GamePrisma.SelectPoolEntryDelegate;
generalAccessLog: GamePrisma.GeneralAccessLogDelegate;
trafficPeriod: GamePrisma.TrafficPeriodDelegate;
trafficPeriodGeneral: GamePrisma.TrafficPeriodGeneralDelegate;
+6
View File
@@ -49,6 +49,7 @@ export interface TurnEngineGeneralRow {
deadYear: number;
affinity: number | null;
picture: string | null;
imageServer: number;
meta: JsonValue;
penalty: JsonValue;
turnTime: Date;
@@ -193,6 +194,8 @@ export interface TurnEngineGeneralUpdateInput {
bornYear?: number;
deadYear?: number;
picture: string | null;
imageServer: number;
startAge: number;
horseCode: string;
weaponCode: string;
bookCode: string;
@@ -208,6 +211,7 @@ export interface TurnEngineGeneralUpdateInput {
export interface TurnEngineGeneralCreateManyInput {
id: number;
userId?: string | null;
name: string;
nationId: number;
cityId: number;
@@ -241,6 +245,8 @@ export interface TurnEngineGeneralCreateManyInput {
bornYear?: number;
deadYear?: number;
picture?: string | null;
imageServer?: number;
startAge?: number;
lastTurn?: InputJsonValue;
penalty?: InputJsonValue;
}
+10 -2
View File
@@ -16,7 +16,9 @@ import { createOfficerLevelActionModules } from './officerLevel.js';
import {
createTraitCatalog,
DOMESTIC_TRAIT_KEYS,
EVENT_DOMESTIC_TRAIT_KEYS,
loadDomesticTraitModules,
loadEventDomesticTraitModules,
loadNationTraitModules,
loadPersonalityTraitModules,
loadWarTraitModules,
@@ -87,14 +89,20 @@ export const loadActionModuleBundle = async <TriggerState extends GeneralTrigger
unitSet?: UnitSetDefinition,
scenarioEffect?: ScenarioEffectKey | null
): Promise<ActionModuleBundle<TriggerState>> => {
const [domestic, war, personality, nation, itemModules] = await Promise.all([
const [domestic, eventDomestic, war, personality, nation, itemModules] = await Promise.all([
loadDomesticTraitModules([...DOMESTIC_TRAIT_KEYS]),
loadEventDomesticTraitModules([...EVENT_DOMESTIC_TRAIT_KEYS]),
loadWarTraitModules([...WAR_TRAIT_KEYS]),
loadPersonalityTraitModules([...PERSONALITY_TRAIT_KEYS]),
loadNationTraitModules([...NATION_TRAIT_KEYS]),
loadItemModules([...ITEM_KEYS]) as Promise<ItemModule<TriggerState>[]>,
]);
const traitCatalog = createTraitCatalog<TriggerState>({ domestic, war, personality, nation });
const traitCatalog = createTraitCatalog<TriggerState>({
domestic: [...domestic, ...eventDomestic],
war,
personality,
nation,
});
const officer = createOfficerLevelActionModules<TriggerState>();
const items = createItemActionModules(createItemModuleRegistry(itemModules));
const inherit = createInheritBuffModules();
@@ -0,0 +1,126 @@
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import {
isWarTraitKey,
type WarTraitKey,
WarTraitLoader,
} from '@sammo-ts/logic/actionModules/traits/war/index.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_부상무효 } from '@sammo-ts/logic/war/triggers/che_견고.js';
export const EVENT_DOMESTIC_TRAIT_KEYS = [
'che_event_귀병',
'che_event_신산',
'che_event_환술',
'che_event_집중',
'che_event_신중',
'che_event_반계',
'che_event_보병',
'che_event_궁병',
'che_event_기병',
'che_event_공성',
'che_event_돌격',
'che_event_무쌍',
'che_event_견고',
'che_event_위압',
'che_event_저격',
'che_event_필살',
'che_event_징병',
'che_event_의술',
'che_event_격노',
'che_event_척사',
] as const;
export type EventDomesticTraitKey = (typeof EVENT_DOMESTIC_TRAIT_KEYS)[number];
export type EventDomesticTraitModule = TraitModule;
export const EVENT_GYEONGO_RAISE_TYPE = BaseWarUnitTrigger.TYPE_ITEM;
export const isEventDomesticTraitKey = (value: string): value is EventDomesticTraitKey =>
EVENT_DOMESTIC_TRAIT_KEYS.includes(value as EventDomesticTraitKey);
const resolveWarKey = (key: EventDomesticTraitKey): WarTraitKey => {
const warKey = key.replace(/^che_event_/, 'che_');
if (!isWarTraitKey(warKey)) {
throw new Error(`Event domestic trait has no canonical war trait: ${key}`);
}
return warKey;
};
const withRefEventOverrides = (
key: EventDomesticTraitKey,
canonical: TraitModule
): EventDomesticTraitModule => {
const { selection: _selection, ...behavior } = canonical;
const alias: EventDomesticTraitModule = {
...behavior,
key,
kind: 'domestic',
};
if (key === 'che_event_무쌍' && canonical.getWarPowerMultiplier) {
alias.getWarPowerMultiplier = (context, unit, oppose) => {
const general =
'getGeneral' in unit &&
typeof (unit as { getGeneral?: unknown }).getGeneral === 'function'
? (
unit as typeof unit & {
getGeneral: () => { role: { specialWar: string | null } };
}
).getGeneral()
: null;
return general?.role.specialWar === canonical.key
? [1, 1]
: canonical.getWarPowerMultiplier!(context, unit, oppose);
};
}
if (key === 'che_event_견고') {
alias.getBattleInitTriggerList = (context) => {
if (!context.unit) return null;
return new WarTriggerCaller(
new che_부상무효(context.unit, EVENT_GYEONGO_RAISE_TYPE)
);
};
alias.getBattlePhaseTriggerList = (context) => {
if (!context.unit) return null;
return new WarTriggerCaller(
new che_부상무효(context.unit, EVENT_GYEONGO_RAISE_TYPE)
);
};
}
return alias;
};
export class EventDomesticTraitLoader {
private readonly cache = new Map<EventDomesticTraitKey, Promise<EventDomesticTraitModule>>();
constructor(private readonly warLoader = new WarTraitLoader()) {}
async load(key: EventDomesticTraitKey): Promise<EventDomesticTraitModule> {
const cached = this.cache.get(key);
if (cached) {
return cached;
}
const loading = this.warLoader
.load(resolveWarKey(key))
.then((canonical) => withRefEventOverrides(key, canonical));
this.cache.set(key, loading);
return loading;
}
}
export const loadEventDomesticTraitModules = async (
keys: EventDomesticTraitKey[],
loader = new EventDomesticTraitLoader()
): Promise<EventDomesticTraitModule[]> => {
const modules: EventDomesticTraitModule[] = [];
const seen = new Set<string>();
for (const key of keys) {
if (seen.has(key)) {
continue;
}
seen.add(key);
modules.push(await loader.load(key));
}
return modules;
};
@@ -3,6 +3,7 @@ export * from './requirements.js';
export * from './selector.js';
export * from './catalog.js';
export * from './domestic/index.js';
export * from './eventDomestic/index.js';
export * from './war/index.js';
export * from './personality/index.js';
export * from './nation/index.js';
@@ -3,9 +3,12 @@ import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/actionModules
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/actionModules/traits/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import { che_부상무효 } from '@sammo-ts/logic/war/triggers/che_견고.js';
export const GYEONGO_RAISE_TYPE =
BaseWarUnitTrigger.TYPE_NONE + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 404;
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
context: WarActionContext,
@@ -45,11 +48,11 @@ export const traitModule: TraitModule = {
}) as TraitModule['onCalcOpposeStat'],
getBattleInitTriggerList: (_context) => {
if (!_context.unit) return null;
return new WarTriggerCaller(new che_부상무효(_context.unit));
return new WarTriggerCaller(new che_부상무효(_context.unit, GYEONGO_RAISE_TYPE));
},
getBattlePhaseTriggerList: (_context) => {
if (!_context.unit) return null;
return new WarTriggerCaller(new che_부상무효(_context.unit));
return new WarTriggerCaller(new che_부상무효(_context.unit, GYEONGO_RAISE_TYPE));
},
getWarPowerMultiplier: (_context, _unit, _oppose) => {
return [1, 0.9];
@@ -3,8 +3,8 @@ import { BaseWarUnitTrigger } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
export class che_부상무효 extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, TriggerPriority.Begin + 200);
constructor(unit: WarUnit, raiseType = BaseWarUnitTrigger.TYPE_NONE) {
super(unit, TriggerPriority.Begin + 200, raiseType);
}
protected actionWar(
@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest';
import {
DOMESTIC_TRAIT_KEYS,
EVENT_GYEONGO_RAISE_TYPE,
EVENT_DOMESTIC_TRAIT_KEYS,
isWarTraitKey,
loadEventDomesticTraitModules,
type WarTraitKey,
WarTraitLoader,
} from '../src/actionModules/traits/index.js';
import { GYEONGO_RAISE_TYPE } from '../src/actionModules/traits/war/che_견고.js';
import { createWarTriggerEnv } from '../src/war/triggers.js';
import type { WarActionContext } from '../src/war/actions.js';
import type { WarUnit } from '../src/war/units.js';
const canonicalKey = (eventKey: string): WarTraitKey => {
const key = eventKey.replace(/^che_event_/, 'che_');
if (!isWarTraitKey(key)) {
throw new Error(`Missing canonical war trait for ${eventKey}`);
}
return key;
};
describe('Ref event domestic traits', () => {
it('loads all 20 exact DB keys without contaminating ordinary domestic selection keys', async () => {
const modules = await loadEventDomesticTraitModules([...EVENT_DOMESTIC_TRAIT_KEYS]);
const warLoader = new WarTraitLoader();
expect(modules).toHaveLength(20);
expect(DOMESTIC_TRAIT_KEYS).toHaveLength(8);
expect(DOMESTIC_TRAIT_KEYS.some((key) => key.startsWith('che_event_'))).toBe(false);
for (const module of modules) {
const canonical = await warLoader.load(canonicalKey(module.key));
expect(module.key).toMatch(/^che_event_/);
expect(module.kind).toBe('domestic');
expect(module.name).toBe(canonical.name);
expect(module.info).toBe(canonical.info);
expect(module.getName?.()).toBe(canonical.getName?.());
expect(module.getInfo?.()).toBe(canonical.getInfo?.());
expect(module.selection).toBeUndefined();
}
});
it('suppresses only the duplicate event multiplier for dual-slot 무쌍', async () => {
const [eventMusang] = await loadEventDomesticTraitModules(['che_event_무쌍']);
const unit = {
getGeneral: () => ({
role: { specialWar: 'che_무쌍' },
meta: { rank_killnum: 40 },
}),
} as unknown as WarUnit;
const context = { unit } as unknown as WarActionContext;
expect(eventMusang!.getWarPowerMultiplier?.(context, unit, unit)).toEqual([1, 1]);
});
it('keeps event and ordinary 견고 injury-prevention triggers distinct by raise type', async () => {
const [eventGyeongo] = await loadEventDomesticTraitModules(['che_event_견고']);
const canonical = await new WarTraitLoader().load('che_견고');
const activated: string[] = [];
const unit = {
getUnitId: () => 7,
isAttacker: () => true,
activateSkill: (name: string) => activated.push(name),
} as unknown as WarUnit;
const oppose = {
getUnitId: () => 8,
isAttacker: () => false,
} as unknown as WarUnit;
const context = { unit } as unknown as WarActionContext;
const caller = canonical.getBattleInitTriggerList?.(context);
caller?.merge(eventGyeongo!.getBattleInitTriggerList?.(context));
caller?.fire(
{ rng: null as never, attacker: unit, defender: oppose },
createWarTriggerEnv()
);
expect(GYEONGO_RAISE_TYPE).toBe(413696);
expect(EVENT_GYEONGO_RAISE_TYPE).toBe(1);
expect(activated).toEqual(['부상무효', '부상무효']);
});
});
+3
View File
@@ -74,6 +74,9 @@ importers:
'@sammo-ts/common':
specifier: workspace:*
version: link:../../packages/common
'@sammo-ts/game-engine':
specifier: workspace:*
version: link:../game-engine
'@sammo-ts/infra':
specifier: workspace:*
version: link:../../packages/infra
+1849
View File
@@ -0,0 +1,1849 @@
{
"columns": ["generalName", "leadership", "strength", "intel", "specialDomestic", "dex", "imgsvr", "picture"],
"data": [
["⑨탈곡기", 69, 12, 80, "che_event_징병", [12066, 27302, 29463, 307356, 16448], 1, "9ed8be6.gif?=20190417"],
["⑤사토미나미", 64, 76, 9, "che_event_격노", [120294, 394511, 53112, 109495, 14523], 1, "dc6174a.jpg?=20181111"],
["⑮제노에이지", 70, 10, 88, "che_event_척사", [30833, 35100, 26918, 322818, 26489], 1, "ddd1fd7.jpg?=20190320"],
["㉖병리학적자세", 72, 15, 93, "che_event_집중", [2670, 3953, 12338, 193185, 16524], 1, "3679089.jpg?=20180629"],
["㉖진충 장창영", 73, 15, 88, "che_event_신중", [3425, 0, 25734, 132488, 10355], 0, "default.jpg"],
["⑨수장", 77, 81, 10, "che_event_무쌍", [30407, 48709, 527680, 124849, 30458], 1, "897dde2.png?=20190411"],
["⑧미아뇌미아", 73, 88, 10, "che_event_견고", [19423, 37715, 328893, 86796, 3955], 0, "default.jpg"],
["⑬오징어", 72, 83, 10, "che_event_척사", [44553, 271485, 26964, 40191, 24728], 1, "8cab968.png?=20190826"],
["㉖돼지", 78, 88, 15, "che_event_위압", [343248, 7128, 29621, 56379, 28202], 1, "b0ce79d.png?=20200721"],
["⑬만샘", 69, 82, 10, "che_event_견고", [4997, 2422, 120127, 21479, 31194], 1, "4a206a1.jpg?=20181122"],
["③건전한촉수병사", 75, 82, 10, "che_event_척사", [70841, 763849, 52226, 120536, 34635], 1, "438ec73.png?=20180827"],
["㉒바보", 70, 10, 83, "che_event_집중", [13844, 26294, 21207, 381343, 41219], 1, "3efe080.gif?=20200425"],
["㉕건방진노예", 66, 10, 78, "che_event_신중", [71873, 46075, 31281, 645604, 28140], 0, "default.jpg"],
["⑥지용갓", 79, 71, 10, "che_event_무쌍", [67819, 8353, 8991, 16235, 2242], 1, "1f3a6e6.png?=20180926"],
["⑥기술머신", 68, 10, 84, "che_event_징병", [2566, 33904, 19642, 113599, 12616], 0, "default.jpg"],
["⑩다유", 78, 57, 9, "che_event_저격", [0, 0, 0, 0, 382659], 1, "f489a2a.jpg?=20181226"],
["⑲카오스피닉스", 69, 10, 85, "che_event_집중", [19178, 5186, 32314, 199666, 14066], 1, "7f80b2f.jpg?=20190715"],
["⑬광삼이", 75, 10, 80, "che_event_척사", [0, 12441, 16502, 101363, 6710], 1, "69fc07c.jpg?=20180926"],
["⑦김채원", 71, 9, 80, "che_event_환술", [78132, 91448, 149393, 850221, 17856], 1, "6e10dd5.png?=20190126"],
["④취한 말랑말", 71, 86, 10, "che_event_무쌍", [3685, 5291, 376679, 93323, 14462], 1, "6094d05.jpg?=20180823"],
["④나아가다", 70, 86, 11, "che_event_무쌍", [7959, 34075, 262397, 48795, 30785], 0, "default.jpg"],
["㉖프레이이시스", 88, 76, 15, "che_event_궁병", [44142, 199714, 8520, 36716, 21812], 1, "b723845.jpg?=20200716"],
["⑦당나귀", 70, 68, 9, "che_event_견고", [227160, 8034, 34626, 151813, 7032], 1, "27e9dc8.jpg?=20190126"],
["⑦태극권", 78, 80, 10, "che_event_위압", [19166, 58452, 452711, 85597, 13187], 1, "e7183a7.jpg?=20181101"],
["⑤레이널드", 66, 75, 9, "che_event_징병", [470721, 39021, 110384, 139118, 20949], 1, "39784e5.jpg?=20181119"],
["㉕rwitch", 70, 10, 78, "che_event_신중", [49944, 27761, 14294, 225622, 10598], 1, "dcc638d.jpg?=20190725"],
["㉕쿠로", 80, 62, 10, "che_event_척사", [109136, 754022, 27120, 118731, 8779], 1, "55dc7c0.gif?=20200604"],
["⑤아유", 63, 9, 76, "che_event_징병", [71632, 13272, 57668, 353287, 20061], 1, "13333bf.jpg?=20181025"],
["⑤사로나이트광산노예", 64, 9, 75, "che_event_신중", [51428, 22312, 83340, 409816, 16341], 1, "3b864b.jpg?=20181029"],
["④헨젤과 그레텔", 70, 86, 11, "che_event_위압", [24800, 205402, 14520, 27234, 24838], 0, "default.jpg"],
["⑳리무루", 70, 10, 83, "che_event_반계", [11891, 64290, 28360, 221298, 20815], 1, "c40f9f7.jpg?=20200222"],
["⑨고통의가시", 84, 10, 73, "che_event_필살", [44271, 19457, 28877, 350600, 23753], 1, "f554ad6.gif?=20190402"],
["⑧오로라", 72, 88, 11, "che_event_척사", [14496, 153379, 13494, 50367, 9522], 1, "2c42d8a.jpg?=20190314"],
["㉙카이스트", 73, 15, 85, "che_event_척사", [7111, 4589, 2954, 99781, 12445], 1, "9361ef8.jpg?=20180907"],
["③페이트", 81, 42, 41, "che_event_무쌍", [1907, 9788, 7036, 14153, 830532], 1, "6b8a0d8.jpg?=20180807"],
["②조조", 74, 10, 87, "che_event_저격", [51367, 84856, 85836, 416202, 32660], 1, "20c3e40.jpg?=20180723"],
["⑦하야함", 64, 9, 82, "che_event_신산", [66175, 18423, 61422, 555881, 20891], 0, "default.jpg"],
["㉔만샘", 71, 10, 83, "che_event_신중", [15125, 16130, 10306, 211417, 29217], 1, "4a206a1.jpg?=20181122"],
["⑬아오바", 68, 10, 86, "che_event_저격", [21221, 8291, 20955, 293615, 25369], 1, "3651a80.jpg?=20190815"],
["④네이미", 83, 72, 11, "che_event_징병", [80581, 72327, 97231, 100303, 6604], 0, "default.jpg"],
["⑩양양", 83, 57, 9, "che_event_저격", [0, 0, 0, 0, 825914], 0, "default.jpg"],
["⑩카니아", 81, 10, 9, "che_event_저격", [0, 0, 0, 0, 382507], 1, "ff65ff7.jpg?=20190520"],
["⑭아메스", 71, 11, 84, "che_event_필살", [49869, 25555, 17450, 495585, 43482], 1, "a889885.jpg?=20190905"],
["②시타오 미우", 86, 75, 10, "che_event_격노", [30707, 123520, 586021, 216726, 21916], 1, "ca834c3.jpg?=20180723"],
["⑰보스곰", 70, 11, 82, "che_event_집중", [18515, 19364, 6689, 182243, 8353], 1, "773556e.gif?=20190822"],
["㉙마왕", 71, 85, 17, "che_event_저격", [0, 10033, 112335, 25714, 14310], 1, "f7358b.jpg?=20200822"],
["⑨콧코로", 87, 71, 10, "che_event_척사", [75273, 403453, 16584, 114760, 21249], 1, "1c20687.png?=20190410"],
["⑳돌아온너구리", 72, 11, 87, "che_event_반계", [16455, 19708, 23120, 961863, 25311], 1, "b90cb6f.jpg?=20200307"],
["⑨이수", 70, 85, 10, "che_event_징병", [18047, 34335, 384104, 139097, 16896], 0, "default.jpg"],
["③장수는랜덤", 82, 72, 10, "che_event_저격", [34971, 139642, 51909, 44252, 3833], 0, "default.jpg"],
["⑦우양우", 69, 81, 9, "che_event_위압", [123772, 1053319, 85606, 223038, 50266], 1, "2381a44.jpg?=20190227"],
["⑩수장", 82, 59, 9, "che_event_저격", [0, 0, 4352, 369, 566867], 1, "5dca65b.jpg?=20190510"],
["㉒메리레녹스", 15, 82, 67, "che_event_필살", [2114, 15028, 125743, 36260, 4538], 1, "cf65bb3.jpg?=20200418"],
["⑨의리", 72, 84, 10, "che_event_돌격", [67642, 38794, 417181, 115711, 24007], 1, "ec56615.png?=20190412"],
["①망포", 77, 77, 10, "che_event_징병", [54497, 12612, 280281, 74044, 40149], 1, "bc427b1.jpg?=20180713"],
["⑬작은달", 74, 11, 81, "che_event_신중", [14904, 7585, 13167, 185480, 2421], 0, "default.jpg"],
["㉖개미호랑2", 73, 90, 15, "che_event_견고", [288, 0, 93742, 6059, 5087], 1, "9e0429e.jpg?=20200313"],
["⑪오야코돈", 84, 10, 66, "che_event_징병", [24370, 25967, 16362, 245597, 7436], 0, "default.jpg"],
["⑬장손신희", 69, 10, 86, "che_event_귀병", [10741, 10967, 22591, 126547, 6602], 1, "e7a5bde.jpg?=20190329"],
["①돌려돌려돌림판", 73, 10, 84, "che_event_필살", [45091, 105611, 63987, 669570, 23342], 0, "default.jpg"],
["②그저늅늅", 73, 10, 89, "che_event_저격", [37299, 99655, 134493, 511159, 27544], 1, "6d750a7.jpg?=20180727"],
["㉗DDDD", 86, 15, 79, "che_event_신산", [7092, 17438, 43824, 429534, 6462], 1, "a1ad420.jpg?=20200425"],
["④오니즈카", 70, 85, 11, "che_event_무쌍", [32187, 49027, 623233, 132977, 47681], 1, "ca0b15e.gif?=20181003"],
["④늘모", 72, 82, 11, "che_event_필살", [41572, 19959, 280942, 60726, 26062], 1, "e7c163.png?=20180801"],
["㉖창모", 76, 15, 89, "che_event_집중", [40734, 7540, 9892, 203633, 18165], 1, "43193f0.jpg?=20200716"],
["⑨밀리아 레이지", 71, 11, 85, "che_event_저격", [35676, 50476, 34660, 284472, 13483], 1, "d30881.jpg?=20190430"],
["㉙빼빼로", 74, 86, 15, "che_event_돌격", [62472, 49336, 102711, 34245, 39102], 1, "5d0c1e1.png?=20201111"],
["⑥행복했으면좋겠어", 71, 10, 83, "che_event_필살", [47597, 42923, 33901, 346748, 14100], 0, "default.jpg"],
["㉓평민킬러", 73, 10, 89, "che_event_집중", [63098, 43476, 29417, 844603, 47081], 1, "fb23a32.jpg?=20190904"],
["⑥캐로", 83, 67, 10, "che_event_격노", [56226, 8253, 20368, 23984, 2074], 0, "default.jpg"],
["⑤랜덤맛달팽이", 69, 88, 10, "che_event_격노", [42778, 68822, 2045, 21277, 4591], 1, "fb0d354.jpg?=20180629"],
["⑦심심", 68, 79, 9, "che_event_무쌍", [41316, 43117, 538329, 221739, 22335], 0, "default.jpg"],
["㉑평민킬러", 69, 10, 86, "che_event_신산", [17249, 10020, 17188, 244496, 21336], 1, "fb23a32.jpg?=20190904"],
["⑤기술만딴거시킴하야", 62, 9, 77, "che_event_징병", [35186, 31487, 40931, 229698, 385], 0, "default.jpg"],
["㉗아리아", 73, 15, 86, "che_event_집중", [13320, 10692, 7100, 166331, 2240], 1, "f7358b.jpg?=20200822"],
["⑪수장", 70, 10, 86, "che_event_집중", [11534, 31540, 10081, 251307, 14906], 1, "b9b784a.jpg?=20190521"],
["⑬아르르 나쟈", 67, 10, 86, "che_event_척사", [18958, 26620, 11302, 310352, 35137], 1, "fc45711.jpg?=20190813"],
["⑥미아", 81, 73, 10, "che_event_무쌍", [7429, 24293, 233314, 36299, 26380], 1, "19ebd2b.jpg?=20181127"],
["⑫네이미", 86, 10, 73, "che_event_저격", [64624, 50349, 38151, 437873, 73914], 0, "default.jpg"],
["㉘그저늅늅", 74, 16, 94, "che_event_집중", [29189, 40126, 16335, 207133, 26108], 1, "5cedbb3.jpg?=20190817"],
["⑦한서진", 75, 79, 9, "che_event_돌격", [82482, 152047, 1950450, 330586, 67134], 1, "53f1ebd.jpg?=20190221"],
["⑰외심장", 68, 11, 81, "che_event_저격", [0, 16392, 17309, 143792, 18492], 1, "36110cd.jpg?=20180826"],
["⑪Radon", 87, 70, 10, "che_event_위압", [34067, 69241, 430748, 164549, 10316], 1, "55ad2a4.png?=20190703"],
["⑱지바", 87, 72, 11, "che_event_의술", [12719, 58657, 855054, 128490, 72336], 1, "6b57eee.gif?=20191225"],
["⑳정채연", 69, 83, 10, "che_event_견고", [11469, 42765, 298862, 33741, 33835], 1, "9705097.jpg?=20191001"],
["⑮사스케", 71, 87, 10, "che_event_저격", [563427, 18874, 76929, 97725, 32075], 1, "986348a.jpg?=20190815"],
["㉓카오스피닉스", 76, 10, 86, "che_event_반계", [70496, 92823, 46881, 670985, 21305], 1, "7f80b2f.jpg?=20190715"],
["⑫꼬리주세요", 68, 83, 10, "che_event_격노", [42603, 100423, 4058, 51466, 1350], 1, "e068502.jpg?=20190725"],
["①물자조달쟁이", 71, 10, 83, "che_event_필살", [16354, 49134, 67257, 316348, 34250], 1, "3679089.jpg?=20180629"],
["⑪이시리스", 72, 11, 86, "che_event_신산", [60260, 73608, 37067, 627605, 24308], 0, "default.jpg"],
["③코시미즈 사치코", 72, 10, 85, "che_event_징병", [13834, 59966, 63614, 426330, 33482], 1, "9e501a.jpg?=20180908"],
["⑩병리학적자세", 64, 9, 78, "che_event_저격", [0, 0, 0, 41371, 630303], 1, "3679089.jpg?=20180629"],
["⑭라피스라쥴리", 71, 10, 85, "che_event_환술", [16954, 4260, 14882, 147116, 18215], 0, "default.jpg"],
["㉗카이스트", 77, 15, 88, "che_event_귀병", [44185, 31197, 24729, 378695, 18448], 1, "9361ef8.jpg?=20180907"],
["⑦자무카", 63, 9, 75, "che_event_필살", [19541, 9842, 45793, 414027, 14627], 0, "default.jpg"],
["⑭구미호", 86, 67, 10, "che_event_무쌍", [0, 0, 17413, 6313, 207000], 1, "8bb1f0e.gif?=20190920"],
["㉒마검", 69, 10, 81, "che_event_신중", [0, 17799, 31869, 254581, 3714], 0, "default.jpg"],
["⑭독타맨", 71, 12, 81, "che_event_저격", [46531, 28118, 39184, 259310, 7580], 1, "8a7c8cb.png?=20190908"],
["⑭화신주유", 71, 86, 10, "che_event_징병", [31448, 73215, 480282, 116607, 41044], 1, "15d60d3.jpg?=20190902"],
["㉒문중", 83, 69, 10, "che_event_징병", [11130, 44446, 55949, 45412, 289305], 1, "47d367b.jpg?=20200423"],
["⑨미야와키 사쿠라", 72, 10, 87, "che_event_격노", [48886, 51927, 21151, 374378, 19392], 1, "34cff5.png?=20190411"],
["⑨잔느얼터", 75, 10, 78, "che_event_징병", [23336, 33119, 77850, 365223, 2547], 1, "6a00940.jpg?=20190416"],
["①리안", 70, 10, 84, "che_event_집중", [9269, 21474, 5739, 112015, 24221], 1, "ee5bdbe.jpg?=20180701"],
["④몬스터볼", 80, 10, 73, "che_event_귀병", [73965, 30754, 34570, 147996, 11187], 1, "5249d93.jpg?=20180920"],
["⑱땅땅이", 84, 75, 10, "che_event_척사", [83217, 914271, 42441, 178821, 140063], 1, "99cd27f.gif?=20190606"],
["⑦갓드오브갓크", 68, 9, 78, "che_event_징병", [103404, 81444, 51333, 895043, 28267], 0, "default.jpg"],
["①네겝", 80, 74, 10, "che_event_위압", [20766, 151448, 110818, 53177, 14122], 1, "734cbaf.jpg?=20180629"],
["①이빌라이져", 69, 84, 10, "che_event_징병", [32806, 208458, 7833, 39657, 24775], 0, "default.jpg"],
["③천괴금", 87, 70, 10, "che_event_돌격", [521270, 33837, 123408, 112940, 52119], 1, "f10e3d9.png?=20180702"],
["㉕김 신", 65, 10, 79, "che_event_집중", [104056, 39683, 38908, 578257, 12852], 1, "9fd757a.gif?=20200602"],
["⑤뭐지", 62, 9, 78, "che_event_환술", [62280, 8354, 46707, 334569, 22150], 0, "default.jpg"],
["③BB", 73, 9, 82, "che_event_징병", [72223, 38600, 142453, 308997, 9039], 1, "2bf3a09.jpg?=20180823"],
["⑰짜냥이은하", 68, 10, 83, "che_event_저격", [0, 17237, 6965, 121973, 20673], 1, "f1ae17a.jpg?=20191121"],
["㉙껄룩", 74, 85, 16, "che_event_저격", [106366, 5100, 0, 21741, 20446], 0, "default.jpg"],
["③으앙쥬금", 69, 10, 82, "che_event_저격", [12098, 6366, 54111, 114435, 0], 0, "default.jpg"],
["⑪북오더", 70, 84, 10, "che_event_필살", [26481, 204763, 2980, 51762, 19395], 1, "4939af6.gif?=20190609"],
["③모라스", 72, 82, 11, "che_event_위압", [110373, 496492, 104955, 112526, 37253], 0, "default.jpg"],
["⑦진리", 76, 9, 71, "che_event_신산", [41450, 29904, 81247, 596107, 11971], 1, "ac7abfd.jpg?=20190304"],
["⑩김기사", 77, 9, 57, "che_event_저격", [0, 1440, 1331, 0, 227796], 1, "8996332.jpg?=20190315"],
["⑩땅땅이", 84, 32, 33, "che_event_저격", [0, 0, 0, 0, 755483], 1, "99cd27f.gif?=20190606"],
["⑱임사영", 69, 10, 85, "che_event_환술", [10512, 10675, 19954, 167913, 6599], 1, "d5dc381.gif?=20190719"],
["④아노리엔", 69, 83, 10, "che_event_돌격", [145314, 6307, 19575, 57695, 8624], 0, "default.jpg"],
["④삼남매엄마", 72, 10, 81, "che_event_신산", [43189, 22613, 16437, 140446, 8310], 0, "default.jpg"],
["⑫USB", 73, 10, 90, "che_event_환술", [7103, 19575, 7369, 235149, 13518], 1, "ee1c9bb.png?=20190724"],
["㉗민토의속마음", 75, 16, 91, "che_event_신중", [16200, 23263, 5382, 401151, 4961], 1, "e610ff7.gif?=20200813"],
["㉓슬라임", 85, 11, 76, "che_event_신산", [42817, 45770, 28685, 298605, 6343], 0, "default.jpg"],
["④미아", 84, 72, 10, "che_event_격노", [31525, 56587, 464741, 122044, 21784], 1, "4bc280.jpg?=20180928"],
["⑤꿀잠", 66, 9, 75, "che_event_척사", [21658, 43997, 52341, 294593, 21948], 0, "default.jpg"],
["⑦삼남매엄마", 68, 80, 9, "che_event_저격", [107623, 600187, 65903, 187226, 18331], 0, "default.jpg"],
["②천괴금", 18, 84, 66, "che_event_견고", [50749, 57903, 106604, 23124, 9462], 1, "f10e3d9.png?=20180702"],
["⑧後唐 이존욱", 71, 87, 11, "che_event_징병", [185604, 12082, 57933, 65980, 4613], 1, "d6243ee.png?=20190318"],
["①마르", 72, 11, 81, "che_event_신중", [6329, 26749, 54384, 277444, 31517], 1, "3da8a49.jpg?=20180417"],
["⑬베이비소울", 82, 73, 10, "che_event_견고", [230121, 7083, 12067, 49111, 24169], 1, "7081d76.jpg?=20190815"],
["㉕너튜브", 66, 74, 10, "che_event_위압", [52091, 403138, 51218, 98799, 11904], 1, "758baa7.png?=20200604"],
["④아스톨포", 85, 69, 10, "che_event_격노", [36712, 58521, 302369, 60007, 30688], 1, "e4187ba.png?=20180921"],
["②삼남매엄마", 78, 86, 10, "che_event_위압", [42973, 1314344, 92685, 185252, 69135], 0, "default.jpg"],
["⑨knot", 70, 10, 83, "che_event_척사", [5900, 16468, 50574, 272492, 31237], 0, "default.jpg"],
["⑫타나고", 71, 10, 88, "che_event_척사", [28707, 28597, 13419, 410646, 27977], 0, "default.jpg"],
["⑩코코로", 81, 9, 59, "che_event_저격", [0, 0, 0, 0, 649435], 1, "cd05b08.jpg?=20190405"],
["⑧의리", 72, 83, 10, "che_event_보병", [69696, 193146, 73782, 63784, 5145], 1, "7781b99.jpg?=20190208"],
["⑤제노에이지", 64, 9, 77, "che_event_척사", [36402, 20985, 55806, 466946, 23361], 1, "af4fec8.png?=20180818"],
["⑪륜", 91, 11, 11, "che_event_필살", [5759, 4925, 3378, 22723, 268931], 1, "8c97e55.jpg?=20190627"],
["⑧다유", 82, 76, 10, "che_event_격노", [25375, 17820, 99509, 14090, 0], 1, "f489a2a.jpg?=20181226"],
["⑤나레이터킬러", 76, 85, 10, "che_event_무쌍", [472558, 101666, 138447, 207689, 14561], 0, "default.jpg"],
["⑤코우치 카에데", 69, 10, 80, "che_event_필살", [22084, 20595, 15855, 155083, 13891], 0, "default.jpg"],
["⑤마헤", 66, 9, 74, "che_event_저격", [52441, 38100, 72327, 335229, 2858], 1, "b763c2b.jpg?=20181030"],
["⑪개꿀", 68, 82, 10, "che_event_격노", [19641, 112569, 4288, 33032, 566], 1, "5b0cf23.jpg?=20190625"],
["㉙수장", 84, 76, 15, "che_event_필살", [40848, 170042, 5282, 36200, 36530], 1, "2a0afd0.jpg?=20201116"],
["㉓카이스트", 74, 10, 90, "che_event_신산", [91890, 65253, 43139, 1103338, 46718], 1, "9361ef8.jpg?=20180907"],
["⑦くま", 69, 9, 78, "che_event_신산", [48593, 87456, 110447, 792291, 27094], 1, "7c91370.jpg?=20190125"],
["⑧포트리스3패왕전", 72, 10, 88, "che_event_신산", [47334, 57935, 76625, 542159, 60865], 1, "e32fd7f.jpg?=20190314"],
["④사무엘", 71, 84, 10, "che_event_무쌍", [48137, 373281, 38488, 106864, 48046], 1, "63dc452.gif?=20181007"],
["⑰카오스피닉스", 68, 10, 85, "che_event_격노", [7023, 5005, 37197, 305666, 40237], 1, "7f80b2f.jpg?=20190715"],
["⑨∵∴∵∴∵∴∵∴∵", 71, 86, 10, "che_event_돌격", [363918, 18088, 46544, 69087, 67576], 1, "6eeb68b.jpg?=20190426"],
["⑭카이스트", 69, 10, 82, "che_event_징병", [27264, 28439, 17789, 299287, 25189], 1, "9361ef8.jpg?=20180907"],
["⑦이드", 69, 79, 9, "che_event_궁병", [96782, 903159, 65095, 225631, 22495], 1, "5e36874.jpg?=20190126"],
["㉘Akaya", 75, 15, 92, "che_event_신중", [13763, 17503, 14681, 228950, 11775], 0, "default.jpg"],
["⑮모기", 79, 11, 78, "che_event_집중", [13922, 26086, 44434, 247837, 14501], 1, "e5ab704.jpg?=20190926"],
["⑫광삼이", 77, 10, 81, "che_event_저격", [41662, 26088, 57286, 399158, 22107], 1, "69fc07c.jpg?=20180926"],
["④사이토", 69, 82, 10, "che_event_보병", [232961, 15823, 38083, 55895, 13744], 1, "55bc1af.png?=20180923"],
["②만샘", 81, 66, 11, "che_event_격노", [17847, 165, 95616, 10808, 11331], 1, "37d189c.jpg?=20180420"],
["⑮ㄹㅇ카오스피닉스", 70, 10, 86, "che_event_신산", [36799, 11400, 47821, 376753, 31452], 1, "7f80b2f.jpg?=20190715"],
["⑤공돌이푸", 65, 9, 74, "che_event_저격", [26189, 39718, 105060, 408632, 13614], 0, "default.jpg"],
["⑪쒸익쒸익", 84, 73, 11, "che_event_견고", [10684, 35883, 195196, 43139, 31175], 1, "dbeeb0d.jpg?=20190620"],
["⑥북오더", 85, 39, 39, "che_event_징병", [5116, 33377, 3025, 8620, 217779], 1, "c0276e1.gif?=20180917"],
["④미스트 뱀파이어", 74, 10, 83, "che_event_신산", [39692, 37650, 88767, 550379, 19224], 0, "default.jpg"],
["④리안", 73, 10, 84, "che_event_저격", [33341, 71461, 47493, 347034, 3226], 1, "2347b5d.png?=20180929"],
["㉓킹구", 85, 70, 10, "che_event_무쌍", [9273, 166633, 8086, 26783, 18799], 1, "fb3c625.gif?=20190428"],
["⑨박초롱", 73, 86, 10, "che_event_필살", [110735, 426298, 28849, 103873, 24173], 1, "95c959e.gif?=20190423"],
["④쉬원찮은남자", 81, 74, 11, "che_event_징병", [116387, 388862, 62044, 104735, 42207], 0, "default.jpg"],
["④앙 로리로리띵", 72, 10, 83, "che_event_징병", [52524, 72230, 58483, 334355, 2515], 1, "7b880bc.jpg?=20181003"],
["④모라스", 75, 10, 82, "che_event_저격", [95531, 33656, 52433, 379296, 23626], 1, "383cea4.jpg?=20180919"],
["⑱Eman", 83, 73, 11, "che_event_견고", [524297, 16805, 65673, 86185, 78182], 1, "4358ef.png?=20191212"],
["②ㅊㅂ", 69, 86, 11, "che_event_무쌍", [46323, 245706, 45031, 61371, 7224], 1, "fd20d5d.png?=20180724"],
["③카미야 나오", 86, 70, 10, "che_event_위압", [492919, 28506, 73738, 100671, 28939], 1, "df23b15.jpg?=20180823"],
["⑳쒸익쒸익", 70, 10, 85, "che_event_척사", [22360, 73290, 21091, 433104, 26204], 1, "dbeeb0d.jpg?=20190620"],
["㉗임사영", 72, 15, 91, "che_event_신중", [8874, 10068, 10408, 64188, 9078], 1, "7f9473e.gif?=20200211"],
["⑥성산동피주먹", 79, 70, 11, "che_event_저격", [13286, 79622, 99744, 36991, 2711], 1, "50ac5f9.jpg?=20180922"],
["④타마모노마에2", 69, 11, 86, "che_event_척사", [25790, 20738, 8685, 223927, 11530], 1, "1d37633.png?=20181002"],
["⑳소리", 70, 10, 82, "che_event_신산", [18201, 24368, 8029, 182878, 14390], 0, "default.jpg"],
["⑲외심장", 70, 10, 83, "che_event_신산", [27620, 7809, 12829, 236951, 14985], 1, "36110cd.jpg?=20180826"],
["⑳병리학적자세", 71, 13, 85, "che_event_격노", [49347, 69493, 29432, 577911, 35546], 1, "3679089.jpg?=20180629"],
["⑬병리학적자세", 69, 11, 86, "che_event_집중", [20788, 27119, 17218, 430245, 31586], 1, "3679089.jpg?=20180629"],
["⑫나폴레옹", 82, 79, 10, "che_event_돌격", [25797, 512433, 4289, 37028, 39345], 1, "86e6b9f.jpg?=20190719"],
["⑦바젤기우스", 68, 77, 9, "che_event_척사", [122333, 632620, 56502, 177462, 26641], 1, "7668f10.jpg?=20190224"],
["㉖늦엇따당", 88, 75, 15, "che_event_무쌍", [124422, 7894, 38306, 38130, 7684], 1, "99cd27f.gif?=20190606"],
["⑥소환된민심러", 88, 65, 10, "che_event_필살", [201574, 17252, 30411, 69617, 50974], 1, "da319ad.jpg?=20181202"],
["⑩양대가리", 82, 60, 9, "che_event_저격", [0, 0, 0, 0, 832628], 1, "3f6d349.png?=20190611"],
["⑦이수임", 68, 9, 79, "che_event_필살", [101117, 65138, 86227, 844329, 44984], 1, "555823.jpg?=20190223"],
["⑥나나야 시키", 86, 69, 11, "che_event_징병", [46158, 358868, 59307, 72490, 18654], 1, "c19b6b4.jpg?=20181129"],
["①하루", 77, 77, 10, "che_event_견고", [49781, 447666, 81863, 89704, 170291], 1, "3aa501a.jpg?=20180710"],
["⑩킹유신", 81, 59, 9, "che_event_저격", [0, 0, 0, 0, 312096], 0, "default.jpg"],
["⑭오리온자리", 83, 10, 74, "che_event_징병", [47618, 57455, 63919, 394671, 22354], 1, "71c0d1f.jpg?=20190718"],
["④도롱", 69, 86, 10, "che_event_저격", [17265, 40914, 186054, 35840, 20720], 0, "default.jpg"],
["⑰아유", 68, 10, 82, "che_event_집중", [7734, 8572, 10520, 80613, 7195], 1, "ba186bf.gif?=20181220"],
["⑦M950", 66, 81, 9, "che_event_기병", [27724, 58882, 635964, 174216, 24974], 1, "a0ffece.jpg?=20190222"],
["⑬도추", 68, 10, 80, "che_event_반계", [11515, 6747, 28055, 118253, 6832], 0, "default.jpg"],
["⑩물타오르네", 86, 10, 70, "che_event_저격", [0, 0, 0, 0, 677948], 0, "default.jpg"],
["⑦이시리스", 79, 71, 9, "che_event_필살", [61227, 239908, 982438, 205558, 39449], 1, "c82f6e9.jpg?=20190219"],
["⑤브베", 63, 76, 9, "che_event_격노", [27816, 57873, 358679, 154777, 13518], 1, "6ad3375.gif?=20181111"],
["⑮말살하라!", 70, 11, 88, "che_event_필살", [11960, 20293, 38107, 204967, 9763], 1, "7c8c92c.png?=20190926"],
["⑫꼬리원자력발전소", 83, 11, 78, "che_event_징병", [77866, 73785, 28477, 543106, 14279], 1, "daf6db8.gif?=20190728"],
["③사무엘", 76, 10, 75, "che_event_필살", [7031, 45315, 27985, 174012, 8240], 1, "4dc6165.jpg?=20180907"],
["㉘호두농구왕왕쌍", 89, 75, 15, "che_event_위압", [13559, 31626, 243416, 44992, 84661], 1, "889056.jpg?=20200910"],
["⑫くま", 71, 10, 86, "che_event_격노", [31791, 41890, 2891, 305708, 29838], 1, "770f53.jpg?=20190718"],
["⑪광삼이", 75, 11, 81, "che_event_필살", [36088, 12976, 13658, 128236, 12846], 1, "69fc07c.jpg?=20180926"],
["㉗Oz Vessalius", 75, 16, 92, "che_event_신산", [25935, 10913, 24946, 422082, 46168], 1, "21780f2.jpg?=20200812"],
["⑧이시리스", 70, 10, 92, "che_event_환술", [4224, 13911, 20364, 87313, 12026], 1, "71ddb79.jpg?=20190326"],
["㉔돌아온너구리", 83, 68, 10, "che_event_필살", [10038, 91922, 75801, 14335, 19033], 0, "default.jpg"],
["⑪미스티", 69, 10, 83, "che_event_필살", [19934, 26442, 12818, 190245, 2521], 1, "1aadcba.png?=20180908"],
["⑲팬지", 69, 10, 83, "che_event_귀병", [29259, 13912, 9221, 252237, 11550], 1, "2091b86.gif?=20200112"],
["㉖크류", 83, 15, 81, "che_event_신중", [22567, 0, 2181, 140933, 9623], 1, "51540ea.jpg?=20200425"],
["⑬외심장", 67, 10, 88, "che_event_궁병", [35429, 27914, 21938, 176344, 3636], 1, "36110cd.jpg?=20180826"],
["⑦란카", 66, 80, 10, "che_event_척사", [18859, 80142, 639802, 202527, 21643], 1, "b62fa50.png?=20190125"],
["②물조명성너프필수", 87, 78, 10, "che_event_무쌍", [39205, 177529, 1298420, 161286, 84310], 0, "default.jpg"],
["⑲DDDD", 69, 10, 84, "che_event_신중", [9856, 19505, 20360, 140278, 76359], 1, "a5c75e7.jpg?=20191223"],
["㉖민트토끼", 72, 16, 94, "che_event_저격", [0, 7112, 0, 130793, 8075], 1, "3d9efb2.gif?=20200717"],
["㉙이즈미", 74, 15, 86, "che_event_신중", [7309, 4430, 4873, 129565, 22800], 1, "5d37b46.jpg?=20201105"],
["⑩SARS", 83, 59, 9, "che_event_저격", [0, 328, 3234, 0, 755372], 1, "b84944.jpg?=20180829"],
["㉘하루1분", 73, 15, 93, "che_event_신중", [9558, 17184, 15922, 222769, 22134], 0, "default.jpg"],
["⑦멘헤라짱", 67, 79, 9, "che_event_필살", [83422, 589437, 53954, 245165, 23338], 1, "18c3b09.gif?=20190212"],
["①하우젤", 83, 73, 10, "che_event_견고", [9640, 282753, 32486, 46108, 48603], 1, "edb7b8f.jpg?=20180628"],
["⑨카오스피닉스", 71, 11, 86, "che_event_저격", [16280, 47355, 44860, 409190, 27873], 1, "2af0641.jpg?=20180706"],
["⑮SARS", 72, 88, 10, "che_event_필살", [21664, 41907, 455807, 83835, 16117], 1, "b84944.jpg?=20180829"],
["㉒정채연", 72, 78, 10, "che_event_저격", [13635, 8336, 228418, 54829, 2030], 1, "9705097.jpg?=20191001"],
["⑲박일아", 86, 68, 10, "che_event_의술", [472187, 20600, 50311, 90313, 33568], 1, "8608979.gif?=20191024"],
["⑧샹그릴라", 90, 65, 10, "che_event_저격", [4194, 11660, 3717, 20398, 201660], 1, "4c1ded2.jpg?=20190315"],
["⑲하와와", 70, 10, 80, "che_event_의술", [10472, 14586, 32496, 224165, 3473], 0, "default.jpg"],
["㉙나타", 74, 15, 88, "che_event_집중", [36198, 2357, 3981, 246405, 32928], 1, "c17b4b2.jpg?=20201111"],
["⑦나데코", 74, 9, 67, "che_event_격노", [36133, 45948, 64671, 548665, 15763], 0, "default.jpg"],
["⑪꽃냥", 69, 10, 87, "che_event_척사", [31586, 38835, 8777, 348707, 22640], 1, "3447fa.jpg?=20190620"],
["⑫미나토 유키나", 67, 12, 89, "che_event_신중", [10753, 0, 0, 78068, 25787], 1, "dfbfdef.jpg?=20190718"],
["⑤로스트아크", 65, 9, 76, "che_event_신중", [25200, 35103, 54330, 348951, 9350], 1, "ae8cbdd.gif?=20181114"],
["⑩킬러조", 86, 10, 70, "che_event_저격", [0, 0, 2560, 0, 667723], 1, "cd43ea0.jpg?=20190509"],
["⑥아범", 72, 83, 10, "che_event_저격", [44487, 67379, 284123, 84323, 11329], 0, "default.jpg"],
["⑤알카서스", 62, 9, 76, "che_event_신산", [43771, 34833, 59350, 260207, 12641], 1, "c540130.png?=20181025"],
["①무장", 72, 10, 83, "che_event_징병", [9269, 30696, 26088, 296119, 30616], 0, "default.jpg"],
["⑮미스티", 70, 11, 86, "che_event_신중", [22069, 26853, 20110, 225012, 12403], 1, "1aadcba.png?=20180908"],
["⑧솬수123", 74, 10, 84, "che_event_격노", [21442, 34717, 50146, 343042, 13138], 0, "default.jpg"],
["⑥일반장푸", 55, 55, 55, "che_event_위압", [211052, 24547, 54442, 62236, 11215], 0, "default.jpg"],
["⑭킹구없이는못살아", 83, 71, 10, "che_event_척사", [71050, 389547, 13480, 84486, 24910], 1, "fb3c625.gif?=20190428"],
["㉓Crow", 84, 10, 79, "che_event_집중", [126408, 60376, 55452, 852899, 49764], 1, "44d980a.png?=20200130"],
["⑳카오스피닉스", 68, 10, 85, "che_event_신중", [9873, 34390, 16220, 207908, 6158], 1, "7f80b2f.jpg?=20190715"],
["⑧지나가는뉴비", 77, 81, 10, "che_event_징병", [67964, 569025, 57216, 107022, 32017], 0, "default.jpg"],
["①Tiasse", 79, 74, 10, "che_event_척사", [0, 32371, 21087, 0, 126195], 1, "7ebcf8a.jpg?=20180713"],
["③태극권", 84, 68, 10, "che_event_척사", [45600, 30105, 434387, 78535, 10418], 0, "default.jpg"],
["㉒돌아온너구리", 71, 10, 84, "che_event_환술", [28251, 37813, 40168, 489498, 18502], 0, "default.jpg"],
["④ㅅㅂㅅㅌ", 68, 10, 86, "che_event_신산", [26880, 28657, 13333, 237541, 10287], 0, "default.jpg"],
["③5분장멍펫", 78, 79, 10, "che_event_견고", [519553, 39580, 68995, 110004, 38426], 1, "6d750a7.jpg?=20180727"],
["㉓치카", 88, 75, 11, "che_event_견고", [59691, 412283, 20674, 113450, 10538], 1, "890b253.jpg?=20191024"],
["⑩트수", 76, 83, 10, "che_event_저격", [0, 0, 0, 0, 792011], 1, "483e099.png?=20190608"],
["⑦다유", 74, 71, 9, "che_event_위압", [430265, 53205, 141991, 172907, 6422], 1, "f489a2a.jpg?=20181226"],
["㉖불랑기 화승총병", 72, 15, 88, "che_event_척사", [8991, 14612, 19908, 110377, 9626], 0, "default.jpg"],
["⑥G11", 88, 39, 38, "che_event_공성", [0, 0, 5233, 5684, 685600], 1, "acd69c.jpg?=20181217"],
["⑬민트토끼", 68, 11, 86, "che_event_저격", [6148, 35368, 11119, 285649, 49994], 1, "85cdfc4.gif?=20190816"],
["④아소", 79, 10, 73, "che_event_반계", [27199, 35368, 11270, 186085, 11353], 1, "3d22492.jpg?=20180417"],
["㉘플라", 90, 77, 16, "che_event_돌격", [37546, 45018, 197291, 45059, 24196], 1, "816bde9.jpg?=20200716"],
["⑧아유", 70, 10, 89, "che_event_필살", [218, 27987, 31365, 157038, 14991], 1, "ba186bf.gif?=20181220"],
["㉗만샘", 76, 15, 91, "che_event_의술", [6157, 31823, 34012, 367505, 30094], 1, "4a206a1.jpg?=20181122"],
["⑥진리", 86, 69, 11, "che_event_격노", [36439, 94821, 614861, 171342, 55636], 1, "d286702.jpg?=20181210"],
["④낙지꾸미", 71, 11, 81, "che_event_반계", [22422, 20670, 27673, 182196, 2200], 0, "default.jpg"],
["⑤어린이", 69, 67, 9, "che_event_보병", [198235, 5722, 47605, 87111, 4298], 0, "default.jpg"],
["⑬함병선입니다", 85, 10, 71, "che_event_집중", [5299, 8700, 5919, 180926, 42624], 1, "cf49f01.png?=20190815"],
["⑳개미호랑이", 88, 66, 11, "che_event_기병", [20770, 93715, 371211, 111363, 17549], 1, "9e0429e.jpg?=20200313"],
["⑫분노의꼬리", 70, 10, 91, "che_event_신산", [31635, 4564, 11118, 263932, 37081], 1, "d5dc381.gif?=20190719"],
["㉗시뉴카린", 75, 16, 90, "che_event_환술", [12433, 27173, 41403, 367167, 25788], 1, "4d0310a.gif?=20200814"],
["⑥삼남매아빠", 86, 68, 10, "che_event_돌격", [32331, 203325, 24993, 20871, 26702], 0, "default.jpg"],
["④반지전쟁", 73, 11, 83, "che_event_필살", [50374, 34214, 56396, 293089, 6166], 1, "aaa8f40.jpg?=20180921"],
["⑫사마의", 69, 10, 85, "che_event_신산", [21375, 45978, 7392, 253606, 9454], 1, "a8a606f.png?=20190718"],
["⑥귀요미아", 70, 10, 85, "che_event_징병", [2135, 6844, 22413, 139699, 27634], 0, "default.jpg"],
["⑤여행중인규일", 71, 9, 71, "che_event_저격", [17754, 38769, 38741, 326399, 14612], 0, "default.jpg"],
["㉙그저늅늅", 73, 87, 15, "che_event_무쌍", [24005, 18846, 104901, 19928, 20662], 1, "5cedbb3.jpg?=20190817"],
["①삼남매엄마", 71, 82, 10, "che_event_위압", [17014, 104979, 80972, 30229, 18935], 0, "default.jpg"],
["①소마", 73, 81, 10, "che_event_견고", [456171, 41579, 99061, 122518, 38878], 0, "default.jpg"],
["①밥알요정", 77, 77, 10, "che_event_위압", [28076, 54221, 319263, 61411, 48289], 1, "cf505d0.png?=20180630"],
["⑮유산슬", 70, 10, 87, "che_event_척사", [46162, 19807, 63717, 381651, 16287], 1, "ea9648d.png?=20190922"],
["㉑독구", 67, 10, 86, "che_event_신산", [24010, 16507, 15159, 197802, 24127], 1, "f6d2724.png?=20200405"],
["⑮보스곰", 68, 10, 88, "che_event_척사", [18291, 20014, 20890, 466974, 19230], 1, "773556e.gif?=20190822"],
["⑬슬라임", 82, 71, 11, "che_event_필살", [216026, 23994, 60126, 45284, 24173], 1, "b1c4193.jpg?=20190815"],
["②모라스", 71, 11, 89, "che_event_반계", [34004, 84004, 71256, 328734, 26015], 0, "default.jpg"],
["㉔정채연", 72, 80, 10, "che_event_척사", [34, 77053, 8489, 12061, 9739], 1, "9705097.jpg?=20191001"],
["①사서고생함", 71, 10, 83, "che_event_신산", [11473, 12217, 7075, 90081, 16414], 0, "default.jpg"],
["④하우젤", 88, 68, 10, "che_event_위압", [20159, 38333, 431831, 93542, 17783], 1, "dcff9fd.jpg?=20180823"],
["㉗킹구", 78, 87, 15, "che_event_척사", [7824, 16063, 426658, 25495, 23824], 1, "fb3c625.gif?=20190428"],
["㉒평민킬러", 71, 84, 10, "che_event_견고", [463509, 20230, 45383, 94311, 26646], 1, "fb23a32.jpg?=20190904"],
["⑬샴", 71, 10, 84, "che_event_저격", [21982, 11782, 11700, 173334, 21517], 1, "b3a530b.jpg?=20190813"],
["㉔죽음의검", 68, 10, 85, "che_event_집중", [7103, 12435, 25654, 116850, 19841], 0, "default.jpg"],
["⑮작업DB레코드작성", 69, 83, 10, "che_event_저격", [19064, 14467, 184235, 44727, 39148], 0, "default.jpg"],
["⑮으내배고파", 70, 10, 79, "che_event_신중", [10469, 102, 10738, 95680, 14171], 1, "3137ad5.jpg?=20190928"],
["⑫호무새", 77, 10, 82, "che_event_신산", [52590, 48055, 14950, 441744, 18624], 1, "6cb46de.png?=20190719"],
["④오늘의시", 75, 10, 81, "che_event_신산", [39140, 36691, 77924, 277429, 4382], 1, "97e5890.jpg?=20180921"],
["⑥솔라", 68, 10, 88, "che_event_저격", [8111, 23457, 23190, 181106, 20785], 1, "bf39f5b.jpg?=20181129"],
["⑨드류", 70, 10, 87, "che_event_척사", [24454, 61025, 30717, 412103, 23796], 1, "507327d.png?=20181001"],
["⑨아유", 72, 10, 87, "che_event_귀병", [49094, 31810, 25388, 304121, 21048], 1, "ba186bf.gif?=20181220"],
["⑫이초홍", 72, 10, 87, "che_event_척사", [24368, 17926, 16179, 374429, 15483], 1, "f2eb713.jpg?=20190725"],
["⑧프론트라인", 85, 74, 10, "che_event_견고", [10577, 58347, 437741, 113767, 31575], 0, "default.jpg"],
["⑦이쓰미", 74, 9, 80, "che_event_집중", [119264, 171778, 140776, 937683, 22379], 1, "fd6a0fd.jpg?=20181212"],
["⑩뾰루퉁", 66, 66, 9, "che_event_저격", [0, 0, 0, 0, 283777], 1, "8565afb.jpg?=20190516"],
["⑫이해고", 84, 75, 10, "che_event_징병", [35015, 41263, 488453, 144773, 40520], 1, "f332b13.jpg?=20190718"],
["⑧주", 75, 10, 85, "che_event_저격", [37271, 45911, 59793, 373759, 24692], 1, "4b1ac40.jpg?=20190314"],
["⑪핥짝", 68, 11, 89, "che_event_척사", [9799, 18998, 19026, 169985, 11730], 0, "default.jpg"],
["⑧치카", 89, 71, 10, "che_event_저격", [5121, 106719, 322316, 102085, 23643], 1, "764d882.png?=20190328"],
["①칸자키 란코", 69, 11, 85, "che_event_격노", [31239, 23167, 14054, 322927, 44759], 1, "ad53ac0.jpg?=20180708"],
["②뉴 호라이즌", 70, 11, 81, "che_event_반계", [9810, 48984, 42004, 244641, 21509], 1, "d9cd4b9.jpg?=20180726"],
["④체셔 고양이", 71, 10, 83, "che_event_신중", [26473, 56190, 46547, 278807, 20649], 1, "147130f.jpg?=20180919"],
["③외심장", 72, 10, 81, "che_event_징병", [39575, 63598, 51536, 281240, 18768], 1, "36110cd.jpg?=20180826"],
["④카이스트", 71, 9, 83, "che_event_환술", [23800, 9744, 88692, 423257, 18339], 1, "9361ef8.jpg?=20180907"],
["⑤나나야 시키", 73, 67, 9, "che_event_척사", [14326, 47834, 288028, 66027, 17713], 1, "9a3937c.gif?=20181025"],
["㉔킹구갓구", 84, 68, 10, "che_event_위압", [9360, 41905, 238478, 34531, 29359], 1, "fb3c625.gif?=20190428"],
["㉗Hide_D", 76, 16, 87, "che_event_저격", [21081, 17905, 1664, 264773, 31872], 1, "9f0a2a8.png?=20200106"],
["⑤세미", 79, 62, 9, "che_event_무쌍", [153468, 302113, 65001, 123718, 27836], 1, "32b7fdd.jpg?=20181115"],
["⑬무한파워안경", 79, 71, 10, "che_event_척사", [1674, 12511, 155750, 41617, 12934], 1, "b7e1a1f.jpg?=20180525"],
["②삼남매아빠", 90, 74, 10, "che_event_돌격", [1191995, 118931, 143223, 200158, 69313], 0, "default.jpg"],
["⑦시즈", 66, 74, 9, "che_event_위압", [16998, 68128, 492139, 179190, 16752], 1, "1b650f4.png?=20190125"],
["⑫탐욕의 꼬리", 70, 10, 88, "che_event_반계", [38919, 32335, 3161, 279227, 33822], 1, "563530e.gif?=20190718"],
["㉕료우기시키", 80, 63, 10, "che_event_보병", [833050, 35969, 111901, 202125, 11973], 1, "559083f.jpg?=20190905"],
["㉗asdf", 88, 78, 15, "che_event_무쌍", [23286, 81912, 18007, 45249, 2397], 0, "default.jpg"],
["⑥병리학적자세", 67, 11, 88, "che_event_귀병", [6584, 10186, 3922, 117751, 20404], 1, "3679089.jpg?=20180629"],
["㉗[진]배틀메이지", 75, 15, 90, "che_event_신중", [33585, 14363, 2670, 127250, 6480], 1, "a795726.jpg?=20200813"],
["⑦으아앙으아", 68, 9, 78, "che_event_필살", [69443, 53046, 67386, 491997, 6881], 1, "d5186a7.jpg?=20190221"],
["②아소", 80, 10, 71, "che_event_환술", [5648, 34720, 17499, 154226, 2429], 1, "3d22492.jpg?=20180417"],
["⑩성벽킬러", 83, 57, 9, "che_event_저격", [0, 0, 0, 0, 1030193], 1, "9bf4880.png?=20190601"],
["㉕김갑환", 71, 80, 10, "che_event_징병", [43859, 46941, 440670, 132634, 8075], 1, "dcff9fd.jpg?=20180823"],
["⑤사스케", 76, 62, 9, "che_event_무쌍", [175522, 8297, 37318, 90551, 6779], 1, "4c58102.jpg?=20180927"],
["⑧병리학적자세", 68, 10, 92, "che_event_격노", [6678, 20844, 27831, 239390, 16689], 1, "3679089.jpg?=20180629"],
["⑳엘사", 72, 86, 10, "che_event_징병", [25913, 336804, 10527, 50218, 29056], 1, "45d0270.jpg?=20200305"],
["㉑마을주민", 72, 82, 10, "che_event_견고", [18109, 5167, 217098, 41508, 16615], 1, "70c3a0c.jpg?=20200402"],
["⑲땅땅이", 78, 73, 10, "che_event_저격", [95387, 291628, 11819, 46427, 35673], 1, "99cd27f.gif?=20190606"],
["⑤whan", 65, 76, 9, "che_event_격노", [266868, 25411, 53288, 112882, 29615], 1, "68aacd2.jpg?=20180929"],
["⑭지장", 69, 10, 85, "che_event_귀병", [26425, 20784, 16703, 119589, 4307], 0, "default.jpg"],
["⑭메로나광팬486번", 70, 85, 10, "che_event_위압", [46820, 276167, 11080, 56776, 18037], 0, "default.jpg"],
["⑨맹수", 72, 10, 87, "che_event_신산", [53942, 56966, 71198, 393899, 31399], 1, "4383ce5.jpg?=20190411"],
["⑯극대노초홍", 68, 10, 89, "che_event_귀병", [4065, 7900, 343, 127230, 16640], 1, "82a5a70.jpg?=20191024"],
["⑭연채홍", 69, 11, 85, "che_event_징병", [32359, 36578, 16365, 464843, 37703], 1, "a12d463.jpg?=20190905"],
["⑳박일도", 66, 16, 85, "che_event_격노", [17887, 35636, 18792, 316640, 25348], 0, "default.jpg"],
["㉖레벤", 73, 15, 91, "che_event_반계", [4430, 3734, 10545, 160291, 14480], 0, "default.jpg"],
["③성산동피주먹", 79, 10, 68, "che_event_저격", [65739, 25051, 23440, 135202, 6580], 0, "default.jpg"],
["⑲KOSPI", 71, 84, 10, "che_event_척사", [8367, 162879, 8247, 53554, 2289], 0, "default.jpg"],
["⑫꼬리조아♥", 70, 11, 88, "che_event_징병", [39364, 19097, 20618, 360367, 27894], 1, "f3a2c6c.gif?=20190718"],
["⑨경국지색소교", 70, 10, 85, "che_event_필살", [8501, 27452, 39856, 199412, 2328], 0, "default.jpg"],
["㉗연채홍", 75, 15, 90, "che_event_집중", [32340, 5375, 24321, 262482, 36315], 1, "773556e.gif?=20190822"],
["㉙민트토끼", 73, 16, 87, "che_event_신중", [11797, 19044, 4646, 251880, 13077], 1, "e04b397.gif?=20201119"],
["⑨황약사", 90, 68, 10, "che_event_의술", [23121, 116700, 297522, 146101, 310045], 1, "437813e.jpg?=20190408"],
["③커피", 74, 83, 10, "che_event_무쌍", [23743, 47980, 490257, 71293, 37683], 1, "f4e0e1e.jpg?=20180831"],
["㉒철별", 70, 10, 83, "che_event_집중", [20629, 23019, 33232, 338959, 52732], 0, "default.jpg"],
["㉑냐옹", 69, 84, 10, "che_event_견고", [930, 118947, 3147, 40362, 8218], 1, "23ba519.jpg?=20190318"],
["㉓나데코", 74, 89, 10, "che_event_견고", [516777, 18931, 45398, 135108, 14663], 1, "9705097.jpg?=20191001"],
["⑩사이다라떼", 68, 10, 78, "che_event_저격", [0, 0, 0, 0, 159778], 0, "default.jpg"],
["⑫집나간파이", 84, 72, 10, "che_event_척사", [241768, 10221, 30656, 82470, 14006], 1, "f76fa74.jpg?=20190629"],
["⑫바밀리오", 69, 10, 93, "che_event_척사", [11265, 5326, 0, 114450, 8013], 1, "6979928.jpg?=20190713"],
["⑩갓트라브", 81, 57, 9, "che_event_저격", [0, 0, 0, 0, 216548], 0, "default.jpg"],
["⑨북오더", 71, 85, 10, "che_event_보병", [345488, 27073, 59009, 84979, 20740], 1, "c0276e1.gif?=20180917"],
["㉔호시이 미키", 81, 73, 10, "che_event_돌격", [3492, 30132, 295674, 41506, 38362], 1, "4c1e8c4.jpg?=20200515"],
["⑧내말내나라", 69, 11, 84, "che_event_환술", [24618, 28517, 36777, 214686, 11299], 1, "643224f.gif?=20190314"],
["①신태용", 83, 69, 10, "che_event_견고", [5727, 46946, 59215, 21735, 11359], 0, "default.jpg"],
["③제노에이지", 72, 11, 82, "che_event_척사", [69979, 20905, 59075, 272240, 23002], 1, "af4fec8.png?=20180818"],
["⑧마비노기", 73, 88, 10, "che_event_격노", [61414, 65906, 430638, 98369, 28638], 1, "4b78ef3.png?=20190314"],
["㉔개미호랑이블린", 68, 10, 83, "che_event_집중", [17332, 9170, 7337, 174876, 24439], 1, "9e0429e.jpg?=20200313"],
["③♂", 88, 69, 10, "che_event_필살", [101047, 392165, 53609, 110187, 21354], 1, "6f11fd5.jpg?=20180908"],
["②휘뚜루마뚜루코코밍", 64, 48, 47, "che_event_필살", [6232, 37680, 92653, 19943, 11263], 1, "c4d2567.jpg?=20180801"],
["⑤삼남매아빠", 77, 66, 9, "che_event_징병", [43427, 62336, 649154, 162123, 17930], 0, "default.jpg"],
["㉗파워에이드", 75, 15, 90, "che_event_집중", [14697, 26762, 45820, 288257, 38236], 1, "7cde6c8.jpg?=20200813"],
["②미리안느", 73, 11, 82, "che_event_척사", [26597, 62757, 48307, 347272, 22933], 1, "1f1c8ae.jpg?=20180804"],
["㉖팀쿡", 90, 77, 15, "che_event_견고", [22014, 211466, 11573, 55566, 24781], 0, "default.jpg"],
["⑧료우기시키", 69, 11, 89, "che_event_귀병", [3139, 23017, 12181, 57842, 3943], 1, "f1d936e.jpg?=20190217"],
["④성산동피주먹", 81, 72, 10, "che_event_징병", [40028, 332014, 19912, 74292, 34827], 1, "50ac5f9.jpg?=20180922"],
["㉙갈근", 74, 16, 87, "che_event_신중", [11696, 7548, 11825, 163198, 37843], 0, "default.jpg"],
["⑧리플", 71, 90, 10, "che_event_위압", [48892, 526250, 33767, 114495, 13117], 1, "fbe44ca.jpg?=20181207"],
["⑬아이린", 70, 85, 11, "che_event_돌격", [14001, 364900, 14622, 67165, 15973], 1, "8dac73c.jpg?=20180922"],
["⑨세리카", 69, 10, 82, "che_event_반계", [17232, 51043, 35663, 274179, 17434], 1, "58b4689.jpg?=20180607"],
["⑦에이다", 80, 79, 10, "che_event_필살", [552743, 70661, 125163, 121463, 3754], 0, "default.jpg"],
["⑪료우기시키", 82, 76, 10, "che_event_무쌍", [58091, 391277, 10916, 123158, 25056], 1, "f1d936e.jpg?=20190217"],
["⑫보스곰", 69, 11, 89, "che_event_신산", [31497, 36831, 5917, 271843, 17167], 1, "398e96e.gif?=20190719"],
["⑬체리맛농약", 68, 10, 85, "che_event_집중", [9478, 8554, 16493, 186045, 14890], 1, "2dc4058.jpg?=20190816"],
["③Lenn", 86, 69, 10, "che_event_위압", [4054, 16088, 954, 11775, 646951], 1, "fb2c85e.jpg?=20180825"],
["⑪이초홍", 71, 10, 86, "che_event_필살", [11443, 64213, 47527, 392635, 12294], 1, "8d5e5ae.jpg?=20190619"],
["㉗개미호랑이", 74, 16, 90, "che_event_신중", [5946, 21933, 38387, 290896, 20432], 1, "9e0429e.jpg?=20200313"],
["⑥ⓟ피카츄", 74, 80, 11, "che_event_격노", [94017, 512262, 70646, 140405, 24945], 1, "4178de2.gif?=20181216"],
["③보리", 69, 10, 86, "che_event_격노", [9193, 12245, 32244, 168746, 23275], 0, "default.jpg"],
["⑩나카노 니노", 81, 35, 35, "che_event_저격", [0, 0, 0, 0, 284738], 1, "bee3096.jpg?=20190607"],
["㉕푸린", 65, 10, 80, "che_event_집중", [56792, 32471, 84344, 817450, 11776], 1, "d3c25f2.jpg?=20200604"],
["⑭강서유서", 79, 12, 74, "che_event_신산", [26367, 14057, 15839, 234235, 7314], 1, "66b9506.png?=20180830"],
["⑦엘사", 67, 80, 9, "che_event_저격", [109578, 621099, 181391, 147403, 22900], 1, "a9361f3.jpg?=20190216"],
["㉗따당따당", 86, 79, 15, "che_event_돌격", [33281, 22170, 169360, 46034, 29983], 1, "99cd27f.gif?=20190606"],
["③버그", 74, 82, 10, "che_event_필살", [21080, 95436, 461525, 102087, 19127], 1, "d85ebbb.jpg?=20180828"],
["⑰박일아", 71, 10, 84, "che_event_신산", [27111, 30262, 68620, 549278, 28962], 1, "8608979.gif?=20191024"],
["②인간 사냥꾼", 86, 76, 12, "che_event_견고", [837722, 97147, 163548, 183617, 53832], 1, "bc6b556.jpg?=20180807"],
["⑦노루야캐요", 71, 9, 79, "che_event_신산", [73481, 89326, 139455, 640098, 14188], 1, "8212a8.jpg?=20190207"],
["⑬고먐미", 72, 11, 82, "che_event_집중", [17898, 24555, 9780, 100383, 6341], 1, "3832b08.jpg?=20190816"],
["㉔독구", 69, 10, 85, "che_event_저격", [30082, 30706, 17351, 383652, 27441], 1, "a7388f.png?=20200424"],
["④랜슬롯", 69, 12, 85, "che_event_신중", [24888, 24360, 12387, 266151, 11751], 1, "c6b18a7.jpg?=20180920"],
["㉗평민킬러", 93, 74, 15, "che_event_견고", [328375, 19870, 72127, 73179, 28622], 1, "fb23a32.jpg?=20190904"],
["⑥whan", 70, 10, 85, "che_event_환술", [69303, 60730, 59751, 424941, 18374], 1, "68aacd2.jpg?=20180929"],
["⑳길냥이", 88, 70, 10, "che_event_돌격", [58332, 1147833, 29817, 49163, 73191], 1, "eed811e.jpg?=20200305"],
["㉖김갑환", 87, 78, 15, "che_event_돌격", [346524, 20951, 20680, 47837, 27193], 1, "dcff9fd.jpg?=20180823"],
["①아유", 83, 73, 10, "che_event_척사", [45035, 53813, 347280, 112550, 44124], 1, "bf8e3b6.jpg?=20180630"],
["㉓くま", 79, 82, 10, "che_event_견고", [134416, 872336, 31536, 176686, 39790], 1, "770f53.jpg?=20190718"],
["⑰강희정", 69, 10, 80, "che_event_필살", [5176, 3177, 13554, 96984, 6566], 0, "default.jpg"],
["㉕평민킬러", 82, 64, 10, "che_event_징병", [71985, 87732, 907056, 207533, 22096], 1, "fb23a32.jpg?=20190904"],
["④나레이터킬러", 69, 83, 10, "che_event_돌격", [39369, 201201, 15393, 38859, 8826], 0, "default.jpg"],
["①오다에리나", 75, 77, 12, "che_event_격노", [14744, 282180, 13513, 60424, 38930], 1, "37df877.jpg?=20180628"],
["④김나영", 74, 82, 11, "che_event_징병", [627412, 66791, 123809, 168420, 40471], 1, "2e8d570.jpg?=20180823"],
["③강서유서", 70, 10, 81, "che_event_신중", [3727, 31580, 3151, 192153, 12428], 1, "66b9506.png?=20180830"],
["⑪장손신희", 71, 85, 11, "che_event_돌격", [271136, 11573, 24245, 98125, 14353], 1, "e7a5bde.jpg?=20190329"],
["⑯카이스트", 67, 12, 88, "che_event_집중", [0, 8331, 3443, 112322, 23572], 1, "9361ef8.jpg?=20180907"],
["⑧구스", 83, 78, 10, "che_event_격노", [24175, 106086, 390664, 106907, 23754], 1, "ab6bca8.jpg?=20190314"],
["②다니엘레메로시", 89, 73, 10, "che_event_위압", [20232, 493247, 48439, 114876, 12879], 1, "b22d7e2.jpg?=20180719"],
["④SARS죽어라", 81, 72, 10, "che_event_격노", [456673, 16672, 56076, 182918, 10976], 0, "default.jpg"],
["⑪무당벌레", 86, 10, 72, "che_event_신중", [70603, 57301, 22890, 393328, 18909], 0, "default.jpg"],
["⑬유닠주세요", 69, 84, 10, "che_event_보병", [284593, 13309, 24316, 53190, 15918], 0, "default.jpg"],
["⑦제갈여포", 78, 9, 70, "che_event_신산", [50824, 106658, 94434, 640028, 8782], 1, "e8e8adc.jpg?=20180419"],
["④아비그네일", 81, 76, 10, "che_event_저격", [703575, 28205, 59804, 154355, 53862], 1, "6ac8d24.jpg?=20180922"],
["⑬카이스트", 79, 11, 70, "che_event_반계", [20038, 14423, 10433, 247657, 18057], 1, "9361ef8.jpg?=20180907"],
["②미이케 나에코", 70, 12, 83, "che_event_반계", [13081, 42253, 0, 71878, 481], 1, "275eac4.jpg?=20180719"],
["⑥엘레나", 80, 10, 74, "che_event_저격", [16534, 40965, 76651, 513623, 125427], 1, "a3ff40d.gif?=20181130"],
["②평민킬러", 76, 86, 10, "che_event_필살", [13032, 114838, 491599, 128087, 28161], 1, "6add0f.jpg?=20180515"],
["㉑시즈", 67, 83, 10, "che_event_궁병", [0, 172722, 19227, 22465, 10309], 0, "default.jpg"],
["⑦하우젤", 82, 63, 9, "che_event_격노", [51778, 110841, 687062, 277802, 9156], 1, "dcff9fd.jpg?=20180823"],
["㉒바보카린", 12, 78, 74, "che_event_공성", [0, 0, 0, 4979, 105255], 1, "56538b1.gif?=20200423"],
["⑳연채홍", 71, 10, 87, "che_event_반계", [20796, 46280, 29340, 308056, 15651], 1, "773556e.gif?=20190822"],
["⑩시그마", 81, 9, 57, "che_event_저격", [0, 0, 0, 0, 384254], 1, "6a2c77.png?=20190509"],
["⑤김나영", 64, 78, 9, "che_event_무쌍", [39651, 56408, 664139, 105312, 49958], 1, "2e8d570.jpg?=20180823"],
["㉑나루토", 69, 10, 84, "che_event_신산", [14556, 21833, 49278, 320953, 52099], 1, "20daf9a.jpg?=20200412"],
["㉘로리", 79, 88, 15, "che_event_견고", [55755, 376241, 31380, 72421, 54233], 1, "50794a6.jpg?=20190923"],
["⑩진산월", 79, 41, 26, "che_event_저격", [0, 0, 0, 0, 530788], 0, "default.jpg"],
["⑤콘", 67, 9, 75, "che_event_집중", [38829, 34815, 65177, 349753, 19354], 1, "78a5139.jpg?=20181025"],
["⑨아루지사마", 67, 89, 12, "che_event_격노", [69655, 2651, 18650, 27009, 6205], 1, "3d319b5.png?=20190422"],
["①SARS", 52, 30, 83, "che_event_신산", [13983, 51943, 16887, 272296, 24209], 0, "default.jpg"],
["⑨갓갓갓갓갓갓갓갓갓", 81, 12, 74, "che_event_척사", [22697, 32256, 40896, 225407, 7946], 0, "default.jpg"],
["㉕개미호람머스", 69, 74, 10, "che_event_궁병", [84791, 458427, 27411, 140321, 9241], 1, "9e0429e.jpg?=20200313"],
["⑦빨간망토", 67, 9, 82, "che_event_필살", [39744, 80838, 114714, 491886, 17876], 1, "862bd8e.gif?=20181028"],
["⑩장수", 82, 58, 9, "che_event_저격", [0, 2186, 993, 0, 613617], 1, "f8c3037.png?=20190510"],
["④사슴초밥", 75, 75, 10, "che_event_징병", [102922, 17437, 6036, 58513, 0], 0, "default.jpg"],
["④처리오빠", 91, 11, 65, "che_event_징병", [17661, 23306, 10243, 206302, 82133], 1, "589d020.jpg?=20180911"],
["⑱외심장", 74, 10, 85, "che_event_격노", [38743, 25546, 77584, 789121, 32975], 1, "36110cd.jpg?=20180826"],
["⑫노란멍충이", 70, 88, 11, "che_event_격노", [35591, 157802, 965, 27862, 25253], 1, "3666aa6.png?=20190621"],
["⑳뉴턴", 74, 83, 10, "che_event_견고", [12269, 12093, 610061, 15602, 16881], 1, "3d166a8.jpg?=20200313"],
["③삼남매아빠", 86, 70, 10, "che_event_위압", [55808, 45866, 370360, 107655, 17687], 0, "default.jpg"],
["⑧그랜드체이스", 72, 89, 11, "che_event_무쌍", [23952, 66298, 531825, 100342, 34197], 1, "68492ea.png?=20190322"],
["⑬사스케", 71, 82, 10, "che_event_무쌍", [188505, 9542, 49633, 30420, 13319], 1, "986348a.jpg?=20190815"],
["⑭광삼이", 76, 11, 76, "che_event_보병", [21468, 16917, 21536, 172885, 14604], 1, "69fc07c.jpg?=20180926"],
["⑦난세", 69, 80, 9, "che_event_무쌍", [83022, 952009, 71208, 256930, 28036], 1, "43adc65.png?=20190125"],
["⑮로리", 86, 74, 10, "che_event_저격", [27690, 35965, 378072, 91087, 7640], 1, "50794a6.jpg?=20190923"],
["①덕장", 70, 83, 10, "che_event_필살", [7822, 44706, 180343, 44938, 30984], 0, "default.jpg"],
["⑤이블린", 63, 9, 77, "che_event_신중", [41156, 35238, 49522, 310521, 12249], 0, "default.jpg"],
["㉑정채연", 69, 10, 83, "che_event_집중", [11570, 6004, 10179, 112286, 7241], 1, "9705097.jpg?=20191001"],
["⑰료우기시키", 83, 70, 10, "che_event_징병", [23231, 264555, 25084, 67976, 30971], 1, "559083f.jpg?=20190905"],
["㉗독구", 77, 15, 90, "che_event_신산", [13756, 39987, 36335, 519343, 19490], 1, "b769004.png?=20200726"],
["⑪먹튀", 69, 11, 82, "che_event_의술", [36140, 29015, 10634, 283666, 13675], 0, "default.jpg"],
["⑧사스케", 74, 86, 10, "che_event_무쌍", [908375, 67876, 37480, 137638, 48849], 1, "e9a3054.jpg?=20190315"],
["⑩무당벌레", 89, 10, 67, "che_event_저격", [0, 0, 0, 0, 638842], 0, "default.jpg"],
["①견고", 73, 81, 11, "che_event_필살", [315554, 37306, 106669, 60814, 36308], 0, "default.jpg"],
["㉔Hide_D", 67, 12, 83, "che_event_신중", [2455, 8108, 0, 127077, 31017], 1, "9f0a2a8.png?=20200106"],
["⑪파이", 68, 10, 88, "che_event_저격", [3904, 18236, 13663, 172482, 30437], 1, "f76fa74.jpg?=20190629"],
["㉔smile", 73, 81, 10, "che_event_필살", [21651, 46701, 13805, 9823, 44002], 0, "default.jpg"],
["⑳마티아스", 74, 85, 10, "che_event_돌격", [55552, 621174, 9929, 183007, 19129], 1, "acb1a64.jpg?=20200305"],
["㉒무공요정", 71, 10, 82, "che_event_저격", [17338, 4521, 34150, 267043, 36848], 1, "a6cba65.jpg?=20200426"],
["②아유", 67, 10, 88, "che_event_격노", [77457, 134449, 58064, 367649, 26674], 1, "337e79.gif?=20180810"],
["⑤리나 인버스", 61, 9, 76, "che_event_신중", [14351, 18919, 19595, 193212, 6067], 1, "edea22f.jpg?=20181109"],
["㉓ㅁㄴㅇ", 71, 11, 90, "che_event_환술", [28425, 49919, 43282, 379771, 27489], 1, "99e4505.jpg?=20200110"],
["⑫개미호랑이", 71, 11, 88, "che_event_필살", [33821, 55291, 13340, 434325, 17702], 1, "fccc37e.jpg?=20190718"],
["④삭턴전문가", 82, 74, 10, "che_event_징병", [62474, 244552, 34576, 64241, 12855], 0, "default.jpg"],
["⑦삼남매아빠", 66, 9, 81, "che_event_징병", [48188, 88624, 121403, 575709, 26406], 0, "default.jpg"],
["⑫콜오브듀티모던워페", 67, 10, 84, "che_event_집중", [26962, 17161, 10166, 162451, 4866], 0, "default.jpg"],
["㉔천무군사육백언", 69, 10, 84, "che_event_신중", [2691, 24634, 23579, 152708, 15548], 1, "9cc7bf4.jpg?=20200514"],
["⑦지각생", 70, 9, 78, "che_event_징병", [56118, 100258, 166732, 700840, 12866], 0, "default.jpg"],
["⑦강준상", 81, 63, 9, "che_event_저격", [39959, 89426, 895211, 222298, 38418], 1, "6e146a6.jpg?=20190208"],
["⑨클로제린츠", 72, 10, 87, "che_event_집중", [27280, 35134, 38566, 423035, 29805], 1, "3333f22.png?=20190421"],
["④리자", 90, 11, 11, "che_event_견고", [1624, 0, 0, 3270, 106626], 1, "3347525.jpg?=20180923"],
["⑤내정관리", 64, 9, 75, "che_event_환술", [20933, 13179, 31599, 255307, 6447], 0, "default.jpg"],
["⑩할머니가타고있어요", 79, 9, 60, "che_event_저격", [0, 0, 0, 0, 451820], 1, "d44d197.jpg?=20190511"],
["⑤쒸프트키까안빠쪄요", 75, 65, 9, "che_event_돌격", [27314, 54053, 502008, 102585, 34047], 1, "df1edd9.jpg?=20181112"],
["⑪아이린", 69, 88, 10, "che_event_의술", [38847, 302361, 10787, 108187, 15091], 1, "8dac73c.jpg?=20180922"],
["①숨질래", 72, 10, 82, "che_event_신중", [26125, 51576, 13806, 143839, 12059], 1, "14451e7.jpg?=20180628"],
["⑰아무생각이없다", 82, 71, 11, "che_event_저격", [77665, 534847, 42515, 88850, 83144], 1, "b205386.png?=20191121"],
["⑩카이스트", 86, 31, 33, "che_event_저격", [0, 0, 0, 0, 1223236], 1, "9361ef8.jpg?=20180907"],
["②ㄹㅇㅇㄷ", 81, 70, 10, "che_event_필살", [14492, 94130, 12818, 27543, 2501], 0, "default.jpg"],
["⑫카오스피닉스", 74, 10, 87, "che_event_격노", [43498, 45060, 17716, 492016, 52704], 1, "7f80b2f.jpg?=20190715"],
["③료우기시키", 73, 83, 10, "che_event_돌격", [12205, 81805, 567176, 112020, 39996], 1, "21b50f8.jpg?=20180628"],
["⑥rm", 68, 85, 10, "che_event_필살", [154644, 8568, 16381, 30634, 20576], 0, "default.jpg"],
["㉔세미얼터", 81, 71, 11, "che_event_기병", [2742, 0, 146616, 38088, 28427], 1, "5b8a3a2.png?=20200515"],
["㉒펭수크류", 71, 10, 81, "che_event_신산", [33213, 26177, 39027, 176741, 8993], 1, "51540ea.jpg?=20200425"],
["⑤수장", 71, 69, 9, "che_event_징병", [393819, 14573, 99500, 192367, 10696], 1, "d74c9d2.png?=20181115"],
["㉑너굴", 69, 10, 85, "che_event_신중", [16178, 7887, 20527, 160398, 32499], 1, "66117f6.jpg?=20200326"],
["④에스프레소", 71, 10, 85, "che_event_척사", [16033, 19220, 34154, 301057, 18160], 1, "414450a.jpg?=20180920"],
["⑨만샘", 70, 10, 90, "che_event_척사", [46471, 30711, 39646, 423223, 17614], 1, "4a206a1.jpg?=20181122"],
["⑥낙지꾸미", 70, 10, 86, "che_event_집중", [25236, 39355, 54994, 269065, 12632], 0, "default.jpg"],
["②의의동망", 73, 11, 83, "che_event_징병", [23486, 79302, 65582, 263157, 10248], 0, "default.jpg"],
["⑦엘레나", 76, 66, 9, "che_event_저격", [20447, 91942, 651419, 217481, 17384], 0, "default.jpg"],
["⑥하더놈", 79, 77, 10, "che_event_격노", [47846, 173265, 27062, 45012, 8575], 0, "default.jpg"],
["⑩무면허음주졸음운전", 84, 57, 9, "che_event_저격", [0, 4964, 0, 3633, 918989], 1, "b31ef66.png?=20190509"],
["㉗레벤", 90, 76, 15, "che_event_척사", [120441, 235217, 47229, 94905, 17168], 0, "default.jpg"],
["⑩H2O", 70, 69, 10, "che_event_저격", [34, 0, 0, 0, 269246], 0, "default.jpg"],
["㉓급식왕스쿨뱅킹", 72, 10, 89, "che_event_환술", [37907, 9906, 9511, 244230, 0], 1, "6337f32.jpg?=20200130"],
["⑬낙지꿈", 67, 12, 83, "che_event_필살", [2892, 9475, 26566, 156253, 14355], 0, "default.jpg"],
["⑧강서유서", 77, 10, 71, "che_event_필살", [14075, 2897, 24731, 108732, 11313], 1, "66b9506.png?=20180830"],
["⑤ㅂ", 74, 10, 87, "che_event_귀병", [80206, 28401, 91962, 456778, 15910], 0, "default.jpg"],
["⑥사키", 71, 83, 11, "che_event_척사", [11373, 30497, 275111, 89443, 17809], 1, "e43844d.gif?=20181113"],
["⑧죽창(+5)", 71, 10, 83, "che_event_척사", [17568, 11968, 27215, 238804, 8117], 1, "1aca7f8.gif?=20190315"],
["②안함", 94, 66, 11, "che_event_돌격", [0, 8139, 60828, 37158, 71639], 0, "default.jpg"],
["①이드", 71, 86, 10, "che_event_위압", [31794, 771290, 45847, 120990, 34428], 1, "7cbc70f.jpg?=20180708"],
["⑨레프 트로츠키", 71, 10, 87, "che_event_저격", [28923, 67108, 56415, 311092, 16486], 0, "default.jpg"],
["⑫평민킬러", 75, 85, 10, "che_event_격노", [821464, 28148, 28488, 50811, 41345], 1, "fba3a6f.jpg?=20190620"],
["⑭베이비소울", 81, 74, 10, "che_event_돌격", [232065, 13137, 19322, 48172, 19217], 1, "7081d76.jpg?=20190815"],
["㉒아유", 68, 10, 84, "che_event_격노", [11833, 16785, 26012, 117790, 9230], 1, "ba186bf.gif?=20181220"],
["⑥군사", 70, 87, 10, "che_event_척사", [60174, 211670, 25411, 40467, 21897], 1, "c3ab9cf.gif?=20181214"],
["⑭RDiceR", 68, 10, 85, "che_event_귀병", [15255, 8952, 9137, 211907, 19706], 1, "52f8628.gif?=20190906"],
["㉒프린세스 라라", 69, 10, 85, "che_event_척사", [49749, 16153, 31945, 312008, 17412], 1, "74c06d5.jpg?=20200426"],
["㉑만샘", 71, 10, 83, "che_event_환술", [44529, 32519, 18408, 392858, 28775], 1, "4a206a1.jpg?=20181122"],
["⑭삼겹살", 69, 12, 84, "che_event_필살", [29349, 28857, 27566, 493233, 29115], 0, "default.jpg"],
["⑳뼈다구", 70, 10, 80, "che_event_징병", [10699, 15715, 16446, 192094, 26719], 1, "3a5cefc.jpg?=20200305"],
["⑨료우기시키", 89, 70, 10, "che_event_징병", [63515, 646751, 85636, 274417, 11284], 1, "f1d936e.jpg?=20190217"],
["③XIV", 70, 11, 84, "che_event_집중", [64494, 5167, 35960, 389298, 21709], 0, "default.jpg"],
["②라스트오더", 89, 72, 10, "che_event_징병", [68419, 352418, 128029, 126673, 20134], 1, "54eec56.jpg?=20180719"],
["⑧베스", 69, 10, 89, "che_event_징병", [31597, 9786, 44001, 196109, 173740], 1, "d06809.gif?=20190331"],
["㉕모기", 67, 10, 77, "che_event_집중", [80108, 51064, 91634, 699115, 12937], 0, "default.jpg"],
["㉓꼬리구이", 78, 10, 89, "che_event_집중", [92707, 65943, 72371, 1497580, 45820], 1, "8429eeb.jpg?=20200129"],
["④연애는글렀다", 68, 11, 87, "che_event_척사", [22244, 4444, 13810, 81458, 3146], 0, "default.jpg"],
["⑤나님짱짱맨", 62, 9, 77, "che_event_환술", [10217, 16335, 33484, 245139, 10266], 0, "default.jpg"],
["⑰테러범A", 71, 83, 10, "che_event_돌격", [5207, 31776, 380523, 65349, 40883], 1, "85ec285.jpg?=20191121"],
["⑱료우기시키", 87, 70, 10, "che_event_필살", [23567, 57578, 349644, 88587, 6442], 1, "559083f.jpg?=20190905"],
["④삼뚝배기", 74, 84, 10, "che_event_필살", [486388, 32180, 66450, 169517, 26048], 1, "b83b5ec.jpg?=20180920"],
["⑧조유리", 71, 10, 88, "che_event_환술", [17168, 49908, 57551, 419852, 24774], 1, "3c34e54.png?=20190319"],
["㉔카이스트", 67, 10, 84, "che_event_집중", [14063, 3225, 17634, 179188, 22413], 1, "9361ef8.jpg?=20180907"],
["⑨카리야건국좀", 72, 10, 85, "che_event_신중", [27807, 18047, 51723, 365393, 25330], 1, "b09d01a.jpg?=20190412"],
["㉗티와즈", 76, 88, 16, "che_event_견고", [556071, 10300, 11065, 10215, 14337], 1, "b033007.jpg?=20200813"],
["④댄스러시 스타덤", 70, 10, 87, "che_event_저격", [14825, 23016, 29737, 312419, 33446], 1, "595e059.png?=20180920"],
["⑳킹구", 89, 69, 10, "che_event_척사", [45029, 715848, 14603, 30270, 26022], 1, "fb3c625.gif?=20190428"],
["③도롱", 70, 86, 10, "che_event_징병", [10979, 73507, 235528, 63495, 3587], 0, "default.jpg"],
["⑫n아텐 누", 77, 11, 85, "che_event_의술", [52920, 72334, 38899, 871059, 38895], 1, "862bd8e.gif?=20181028"],
["①죽음의 별", 72, 10, 85, "che_event_신중", [37180, 29723, 96050, 353137, 25977], 1, "6532f29.jpg?=20180628"],
["㉒외심장", 70, 10, 85, "che_event_격노", [16952, 23301, 48755, 332118, 20076], 1, "36110cd.jpg?=20180826"],
["⑨하우젤", 86, 74, 10, "che_event_저격", [104661, 794153, 41042, 258603, 19995], 1, "dcff9fd.jpg?=20180823"],
["①니노미야아스카", 72, 82, 10, "che_event_견고", [34935, 31645, 475305, 84914, 66071], 1, "59bc7e3.jpg?=20180628"],
["⑦의리", 73, 10, 80, "che_event_환술", [0, 0, 20310, 131251, 6122], 1, "7781b99.jpg?=20190208"],
["㉓시부야 린", 89, 73, 10, "che_event_견고", [662468, 34698, 103205, 155208, 20254], 1, "4c2d759.jpg?=20200130"],
["㉑Hide_D", 70, 10, 85, "che_event_집중", [74756, 45847, 48208, 571922, 25386], 1, "9f0a2a8.png?=20200106"],
["②천사소녀네티", 78, 82, 11, "che_event_견고", [29293, 82635, 310168, 36313, 18853], 0, "default.jpg"],
["⑤야옹", 65, 9, 74, "che_event_집중", [40373, 21972, 60115, 476301, 21049], 1, "a2e5934.png?=20181005"],
["㉑동백", 78, 10, 75, "che_event_격노", [11642, 15113, 13534, 75242, 2143], 1, "77a930e.jpg?=20200403"],
["③죽창", 83, 72, 10, "che_event_징병", [20266, 91290, 428298, 89633, 23241], 1, "2bb1aa1.gif?=20180906"],
["⑦Samo", 69, 9, 78, "che_event_필살", [39017, 98992, 124392, 686942, 22982], 1, "c66b383.png?=20190128"],
["③카이스트", 80, 10, 67, "che_event_견고", [26313, 16984, 0, 113812, 1394], 1, "9361ef8.jpg?=20180907"],
["⑳하우젤", 70, 10, 82, "che_event_의술", [37358, 46474, 35833, 349428, 14200], 1, "dcff9fd.jpg?=20180823"],
["⑩쿠요", 70, 65, 9, "che_event_저격", [0, 0, 4792, 0, 176027], 1, "140a6b7.jpg?=20190425"],
["㉔료우기시키", 83, 69, 10, "che_event_위압", [4196, 20407, 121174, 15290, 16666], 1, "559083f.jpg?=20190905"],
["⑪사이다라떼", 68, 10, 88, "che_event_신산", [1262, 14488, 8803, 183807, 33362], 0, "default.jpg"],
["⑧삼남매엄마", 73, 10, 86, "che_event_필살", [59623, 21062, 54817, 450548, 43016], 0, "default.jpg"],
["⑧슈뢰딩거의컨셉", 71, 82, 11, "che_event_필살", [5959, 59489, 200209, 27348, 12419], 1, "25d2aee.png?=20190313"],
["㉙보스곰", 74, 16, 86, "che_event_징병", [5773, 10638, 9569, 140241, 12532], 1, "773556e.gif?=20190822"],
["⑬제갈여포", 68, 10, 86, "che_event_신중", [5095, 6254, 16106, 70183, 11630], 1, "e8e8adc.jpg?=20180419"],
["㉓강지", 93, 73, 10, "che_event_견고", [44477, 49136, 1025530, 49369, 60884], 1, "3a967d1.jpg?=20200129"],
["⑤쿠요", 67, 74, 9, "che_event_저격", [19103, 47276, 401889, 99063, 13258], 0, "default.jpg"],
["⑮엔야스", 71, 10, 86, "che_event_징병", [35183, 25203, 22806, 346819, 23894], 0, "default.jpg"],
["㉑뼈다구", 67, 10, 84, "che_event_척사", [2311, 11660, 18063, 100074, 10072], 1, "3a5cefc.jpg?=20200305"],
["⑩줄리엣", 83, 57, 9, "che_event_저격", [0, 0, 10522, 0, 521782], 1, "175639b.png?=20181025"],
["㉒천괴금", 82, 69, 10, "che_event_무쌍", [385643, 51874, 62500, 68081, 47300], 1, "2b239af.png?=20190824"],
["⑦ˇ˘ˇ", 82, 66, 9, "che_event_돌격", [273855, 377825, 240016, 251220, 41016], 1, "c759d44.gif?=20190304"],
["⑱카이스트", 72, 10, 88, "che_event_징병", [68073, 97291, 35977, 759031, 59509], 1, "9361ef8.jpg?=20180907"],
["⑤주사위논리학", 72, 64, 9, "che_event_저격", [12697, 53881, 270265, 117372, 7477], 0, "default.jpg"],
["㉒나주리", 68, 11, 76, "che_event_반계", [20116, 17173, 9485, 109636, 1108], 1, "f6034cb.jpg?=20200501"],
["④소열제유비", 72, 84, 10, "che_event_견고", [11387, 28409, 266302, 77049, 10257], 1, "a3b1bbb.png?=20180927"],
["㉕브라움", 70, 79, 10, "che_event_의술", [1148187, 46919, 102756, 197653, 11925], 1, "fe1f2b2.jpg?=20200604"],
["⑥ⓐ자비스", 85, 69, 10, "che_event_징병", [801593, 44873, 112364, 150904, 50588], 1, "472cf86.gif?=20181129"],
["⑦필터링", 68, 9, 80, "che_event_징병", [55599, 67493, 89094, 902284, 42323], 1, "726e55b.gif?=20190201"],
["⑧크리스토프", 69, 12, 79, "che_event_격노", [0, 19481, 12723, 169969, 10954], 1, "db80797.png?=20190319"],
["⑨이쓰미", 71, 10, 89, "che_event_격노", [23062, 44401, 88401, 493861, 28436], 1, "fd6a0fd.jpg?=20181212"],
["⑤김민주", 72, 65, 9, "che_event_척사", [319408, 14538, 73075, 90368, 16276], 1, "77f63a8.png?=20181115"],
["⑲독구", 69, 11, 82, "che_event_집중", [45070, 34467, 39750, 507842, 69412], 1, "ac701b0.jpg?=20180625"],
["⑫장손신희", 78, 83, 10, "che_event_위압", [93781, 450836, 8658, 77456, 25492], 1, "e7a5bde.jpg?=20190329"],
["㉘나누기", 75, 15, 89, "che_event_집중", [29650, 15283, 15334, 269656, 42610], 1, "913a59.png?=20200910"],
["⑲수장고기후레쉬", 80, 10, 68, "che_event_필살", [15200, 28897, 15378, 231527, 11432], 1, "1b8a3df.jpg?=20190620"],
["⑳くま", 70, 10, 85, "che_event_귀병", [44813, 44680, 27564, 545724, 32703], 1, "770f53.jpg?=20190718"],
["⑨이드", 71, 10, 88, "che_event_반계", [36788, 47153, 48086, 490883, 25385], 1, "94635d.jpg?=20190412"],
["⑲유다치", 69, 81, 10, "che_event_격노", [62138, 6372, 27245, 18063, 35289], 1, "b137f72.jpg?=20200109"],
["⑨n아텐 누", 72, 11, 85, "che_event_척사", [49309, 22980, 47358, 476320, 16007], 1, "862bd8e.gif?=20181028"],
["④제갈여포", 68, 9, 86, "che_event_반계", [30055, 19046, 18503, 259977, 30194], 1, "e8e8adc.jpg?=20180419"],
["㉑외심장", 68, 10, 86, "che_event_신산", [6561, 18117, 13756, 172785, 11193], 1, "36110cd.jpg?=20180826"],
["⑩둥근해가떡썹니다", 72, 9, 64, "che_event_저격", [0, 0, 1271, 0, 402331], 0, "default.jpg"],
["⑱Hide_D", 82, 10, 79, "che_event_반계", [23677, 64299, 101032, 1075276, 110262], 1, "ee5accd.jpg?=20190411"],
["⑦미스티", 78, 9, 66, "che_event_저격", [33389, 48563, 103132, 427364, 5118], 1, "1aadcba.png?=20180908"],
["⑬푸딩", 82, 70, 10, "che_event_무쌍", [10924, 141604, 12277, 22350, 12141], 1, "b50d01.png?=20190813"],
["⑥줄리엣 페르시아", 74, 10, 81, "che_event_저격", [52298, 54294, 52226, 477404, 31437], 1, "8c1432f.gif?=20181130"],
["⑫시뉴카린해명해", 91, 70, 10, "che_event_위압", [387404, 32960, 26004, 113309, 7305], 1, "25dd670.png?=20190718"],
["⑪사스케", 71, 84, 10, "che_event_돌격", [37570, 241466, 21685, 65233, 21668], 1, "bab8446.jpg?=20190607"],
["㉑카이스트", 71, 10, 83, "che_event_집중", [15916, 3103, 52042, 338498, 39722], 1, "9361ef8.jpg?=20180907"],
["⑯제노에이지", 69, 10, 87, "che_event_척사", [16571, 5402, 7313, 130992, 30501], 1, "ddd1fd7.jpg?=20190320"],
["㉔혜미", 86, 67, 10, "che_event_척사", [27610, 0, 16615, 320, 235800], 1, "699a977.jpg?=20200516"],
["⑯카오스피닉스", 69, 10, 87, "che_event_신중", [2923, 0, 1143, 102034, 48779], 1, "7f80b2f.jpg?=20190715"],
["①노이어", 81, 72, 11, "che_event_견고", [26847, 141906, 31976, 49329, 11615], 0, "default.jpg"],
["③미쿠", 70, 10, 82, "che_event_격노", [3236, 28883, 26843, 205329, 10790], 1, "a606d84.jpg?=20180906"],
["㉙배규리", 74, 15, 84, "che_event_징병", [0, 3898, 4601, 150071, 23672], 1, "74dec00.png?=20201114"],
["⑦Rei", 68, 76, 9, "che_event_징병", [406414, 46243, 39348, 224137, 55618], 1, "e7d55b1.jpg?=20190305"],
["⑩힐드", 85, 10, 9, "che_event_저격", [0, 0, 0, 0, 721701], 1, "af3224d.gif?=20190608"],
["⑮혜낭", 90, 70, 10, "che_event_격노", [39320, 65587, 554941, 56519, 25250], 1, "20f2421.gif?=20191007"],
["⑤이곽", 65, 75, 9, "che_event_척사", [15365, 28237, 253654, 122823, 7441], 1, "2fe54e6.jpg?=20181026"],
["㉑아키라의노예", 70, 10, 85, "che_event_척사", [7927, 3398, 11368, 185220, 10395], 1, "773556e.gif?=20190822"],
["④에드나", 72, 83, 10, "che_event_돌격", [32525, 447053, 35207, 149488, 23531], 1, "7c39fe8.gif?=20181007"],
["⑨보만다", 72, 85, 10, "che_event_위압", [15320, 19270, 451860, 95652, 29799], 1, "987059d.png?=20190416"],
["⑪비스마르크", 69, 85, 11, "che_event_견고", [59569, 350127, 4276, 106365, 19326], 0, "default.jpg"],
["⑫킹구갓구", 67, 87, 10, "che_event_격노", [683, 438, 87969, 16238, 14758], 1, "fb3c625.gif?=20190428"],
["⑦내정하면서구경", 78, 68, 11, "che_event_견고", [15879, 79061, 9283, 34178, 0], 0, "default.jpg"],
["⑩다시용", 85, 55, 21, "che_event_저격", [0, 0, 0, 0, 442376], 1, "224e088.jpg?=20190210"],
["⑧조밧", 67, 11, 89, "che_event_격노", [5925, 9749, 13547, 124949, 1988], 1, "f2d4b4.jpg?=20180629"],
["㉕치카", 74, 10, 67, "che_event_집중", [68769, 37400, 55441, 480780, 5910], 1, "890b253.jpg?=20191024"],
["㉘하우젤", 75, 15, 93, "che_event_필살", [12922, 34062, 17431, 324008, 21805], 1, "dcff9fd.jpg?=20180823"],
["⑫사이다라떼", 71, 11, 87, "che_event_반계", [32848, 38480, 21271, 270165, 26294], 0, "default.jpg"],
["⑧제노에이지", 73, 10, 89, "che_event_징병", [15693, 77469, 55024, 486598, 42597], 1, "ddd1fd7.jpg?=20190320"],
["②백순대먹고싶다정말", 73, 87, 10, "che_event_징병", [404116, 106854, 104632, 72717, 34280], 1, "745f44.png?=20180801"],
["④긴토키", 73, 83, 10, "che_event_위압", [7322, 36684, 270663, 84802, 12648], 1, "4f652ac.jpg?=20180920"],
["⑥아리아", 90, 12, 10, "che_event_척사", [0, 0, 0, 0, 103155], 1, "810c706.jpg?=20181204"],
["②갓오브블랙필드", 76, 87, 10, "che_event_척사", [8902, 127122, 1167401, 175493, 73242], 1, "51d1f77.jpg?=20180719"],
["⑨갓게이밍", 73, 10, 85, "che_event_귀병", [8031, 27721, 14522, 196166, 7523], 0, "default.jpg"],
["⑪고챱챱", 70, 10, 79, "che_event_귀병", [12080, 19902, 23658, 148494, 78], 1, "a036f8d.jpg?=20190625"],
["⑥마시멜로", 71, 85, 10, "che_event_척사", [156893, 79591, 61931, 39027, 9034], 1, "50102de.jpg?=20181129"],
["㉘불반도", 77, 15, 90, "che_event_환술", [48158, 28371, 34182, 367456, 8475], 0, "default.jpg"],
["⑦하치쿠지 마요이", 63, 9, 76, "che_event_반계", [11230, 8229, 32794, 329634, 10963], 1, "5ae23b6.png?=20190125"],
["㉖????", 85, 16, 79, "che_event_신산", [10110, 46523, 17083, 203641, 24701], 1, "c32df1b.png?=20200716"],
["⑳ㅇㄷㄷㄷㄷ", 86, 65, 10, "che_event_위압", [20920, 3639, 9650, 5776, 217267], 0, "default.jpg"],
["⑤토르", 70, 70, 9, "che_event_위압", [13891, 78002, 475486, 166609, 20564], 1, "5dc81c4.jpg?=20181025"],
["⑩오빠차뽑았다", 81, 56, 9, "che_event_저격", [0, 4052, 6894, 0, 808342], 0, "default.jpg"],
["⑫엔야스", 75, 79, 10, "che_event_필살", [279613, 20674, 50420, 83533, 5610], 0, "default.jpg"],
["⑨이거뭔겜임", 74, 82, 10, "che_event_궁병", [75422, 301634, 7768, 97862, 19755], 0, "default.jpg"],
["⑬개미호랑이", 73, 10, 83, "che_event_반계", [31732, 21205, 25759, 251836, 17704], 1, "fccc37e.jpg?=20190718"],
["⑦새장속의이상향", 67, 78, 10, "che_event_무쌍", [568342, 21286, 106013, 176015, 19470], 1, "83e3447.png?=20181025"],
["④제노에이지", 69, 10, 87, "che_event_반계", [16309, 18458, 42793, 308218, 32981], 1, "af4fec8.png?=20180818"],
["㉒12팩에2전설뜸", 71, 10, 81, "che_event_귀병", [24968, 20532, 26863, 320559, 15379], 0, "default.jpg"],
["②얍얍", 68, 11, 78, "che_event_격노", [911, 17228, 37991, 113521, 7690], 1, "e2ab4cc.png?=20180724"],
["⑮쀼웃", 70, 10, 83, "che_event_척사", [5396, 15250, 24483, 262781, 22067], 1, "8996332.jpg?=20190315"],
["⑤트리스 메리골드", 65, 9, 68, "che_event_척사", [30314, 11343, 4416, 168082, 11425], 1, "b319a52.gif?=20181102"],
["③악의천신", 70, 10, 83, "che_event_격노", [19282, 18707, 61114, 227276, 19514], 1, "15c55ee.jpg?=20180825"],
["⑦차민혁", 69, 9, 77, "che_event_반계", [57604, 58738, 99923, 560756, 45646], 1, "d618d9e.jpg?=20190125"],
["⑮메디브", 83, 75, 10, "che_event_저격", [37969, 64460, 532907, 107707, 32703], 1, "def2895.jpg?=20190719"],
["①카미라", 71, 82, 10, "che_event_필살", [13821, 171873, 64693, 51931, 16330], 1, "1acd89b.jpg?=20180711"],
["②미친오징어", 74, 10, 87, "che_event_격노", [10486, 56901, 91793, 322275, 29785], 1, "3257ac8.jpg?=20180702"],
["⑭보스곰", 69, 12, 84, "che_event_신중", [20892, 20619, 11898, 373761, 36358], 1, "773556e.gif?=20190822"],
["⑫꼬리", 72, 10, 91, "che_event_신산", [34317, 33432, 27660, 414807, 74294], 1, "33d4b88.jpg?=20190718"],
["⑭외심장", 71, 10, 85, "che_event_징병", [29197, 17856, 22532, 344942, 23220], 1, "36110cd.jpg?=20180826"],
["㉔평민킬러", 68, 10, 85, "che_event_신산", [1018, 5217, 15551, 136102, 9997], 1, "fb23a32.jpg?=20190904"],
["⑩런닝머신", 83, 55, 14, "che_event_저격", [0, 0, 0, 0, 1373421], 1, "92484bf.gif?=20190530"],
["⑫이한결", 74, 86, 10, "che_event_징병", [50723, 399206, 46192, 125802, 26769], 1, "63c4de9.jpg?=20190718"],
["⑰보부상", 83, 70, 10, "che_event_저격", [13971, 53677, 255336, 78844, 8343], 0, "default.jpg"],
["②미야노", 75, 87, 10, "che_event_견고", [51123, 578755, 94523, 120407, 50140], 1, "c0ee2d4.gif?=20180810"],
["㉕Hide_D", 64, 10, 82, "che_event_환술", [78921, 28116, 98282, 962494, 25927], 1, "9f0a2a8.png?=20200106"],
["⑤안젤리카", 64, 9, 78, "che_event_징병", [44868, 39118, 49174, 362569, 5650], 1, "21ed85a.jpg?=20181027"],
["㉓삼모몰라요", 75, 10, 88, "che_event_신중", [81146, 70702, 58908, 917107, 33321], 0, "default.jpg"],
["⑮멘헤라쨩", 78, 65, 15, "che_event_필살", [94766, 0, 10645, 17443, 11433], 1, "9e75693.gif?=20190908"],
["⑭모가미 시즈카", 87, 67, 10, "che_event_무쌍", [615372, 54021, 69462, 78953, 21642], 1, "5106f38.jpg?=20190905"],
["⑳임사영", 71, 10, 86, "che_event_집중", [38044, 46007, 22761, 356390, 61435], 1, "7f9473e.gif?=20200211"],
["④힝힝", 69, 85, 10, "che_event_저격", [389589, 10243, 26616, 42848, 44706], 1, "298293e.jpg?=20181006"],
["㉗플라", 90, 75, 15, "che_event_돌격", [37185, 273841, 127436, 98723, 19487], 1, "816bde9.jpg?=20200716"],
["②SARS", 64, 24, 85, "che_event_척사", [25531, 83930, 84949, 328883, 57640], 0, "default.jpg"],
["⑭평민킬러", 71, 85, 10, "che_event_징병", [466787, 17528, 56192, 82285, 38700], 1, "fb23a32.jpg?=20190904"],
["⑯크람푸스", 69, 10, 87, "che_event_신중", [1, 7058, 5433, 182682, 24958], 1, "8eb225b.gif?=20191104"],
["⑩고차비", 84, 9, 58, "che_event_저격", [0, 0, 0, 0, 817410], 1, "9a2b7ec.jpg?=20190511"],
["⑧수장", 76, 84, 10, "che_event_격노", [14890, 40335, 300427, 86584, 30832], 1, "20ada6f.png?=20190311"],
["③아오바 모카", 80, 72, 10, "che_event_위압", [35988, 165861, 25417, 55446, 6622], 1, "8e5030a.gif?=20180830"],
["③SARS", 58, 27, 83, "che_event_저격", [69179, 84867, 44979, 562722, 42208], 1, "b84944.jpg?=20180829"],
["⑮외심장", 73, 85, 10, "che_event_무쌍", [18163, 41883, 335313, 111011, 40531], 1, "36110cd.jpg?=20180826"],
["④박일아", 76, 10, 82, "che_event_격노", [36629, 45610, 75787, 324790, 3124], 1, "94d1ccd.gif?=20180830"],
["③시나", 80, 76, 10, "che_event_돌격", [109774, 335579, 26438, 84316, 19197], 1, "1216034.jpg?=20180901"],
["⑭비스마르크", 66, 10, 80, "che_event_징병", [8841, 5927, 2837, 72663, 14522], 0, "default.jpg"],
["⑦김주영", 76, 9, 73, "che_event_반계", [89339, 72214, 133312, 1112264, 36168], 1, "ecadeaf.png?=20190221"],
["⑦논암", 67, 12, 82, "che_event_집중", [0, 1973, 19227, 91194, 4098], 1, "cb0f7bc.jpg?=20190222"],
["⑤공주", 73, 65, 9, "che_event_징병", [89159, 330430, 37518, 95746, 12310], 1, "d5bbf17.jpg?=20181122"],
["⑥뭐지", 69, 87, 10, "che_event_격노", [25777, 186271, 6404, 54235, 5505], 0, "default.jpg"],
["⑤태극권", 86, 74, 11, "che_event_저격", [145719, 155059, 525887, 210483, 9137], 1, "e7183a7.jpg?=20181101"],
["⑥냐옹", 69, 10, 82, "che_event_신중", [0, 38119, 25027, 163919, 6220], 1, "1cfe7a.jpg?=20181204"],
["㉕강미정", 82, 59, 10, "che_event_징병", [1833, 5798, 17483, 4995, 109147], 1, "4794a22.jpg?=20200630"],
["㉖금위요동중갑대", 73, 16, 88, "che_event_반계", [14857, 25065, 4688, 115732, 10704], 0, "default.jpg"],
["⑧프로야구매니저", 72, 89, 10, "che_event_견고", [86067, 799568, 43017, 114030, 58358], 1, "2f51d31.jpg?=20190318"],
["②마츠자카 사토", 77, 10, 86, "che_event_신중", [48092, 110325, 118787, 835733, 46144], 1, "47260a1.jpg?=20180813"],
["⑭밀리언라이브", 81, 71, 10, "che_event_돌격", [17600, 20272, 270284, 102876, 8527], 0, "default.jpg"],
["⑧NoA", 71, 10, 90, "che_event_격노", [6112, 27636, 29525, 289306, 21273], 0, "default.jpg"],
["㉔닭둘기", 84, 67, 11, "che_event_필살", [15965, 173400, 37401, 38961, 28590], 1, "dcf6070.jpg?=20200514"],
["④각영의풍수사린네", 69, 10, 85, "che_event_신산", [22484, 11786, 14991, 78915, 5267], 1, "ec8b09d.jpg?=20181005"],
["⑥김나영", 71, 84, 10, "che_event_위압", [334671, 16092, 77932, 51721, 24046], 1, "2e8d570.jpg?=20180823"],
["⑭개미호랑이", 70, 10, 84, "che_event_필살", [13639, 16468, 44999, 337391, 63892], 1, "fccc37e.jpg?=20190718"],
["⑩Dynamix", 81, 63, 9, "che_event_저격", [0, 0, 0, 0, 253848], 1, "aafaa29.jpg?=20190509"],
["④페이트", 84, 68, 10, "che_event_기병", [31169, 68619, 59211, 64190, 19858], 1, "31f2522.jpg?=20180922"],
["⑬서새봄", 84, 70, 11, "che_event_징병", [11401, 62444, 368968, 66871, 27382], 1, "b5add3a.jpg?=20190815"],
["㉓아유", 76, 10, 87, "che_event_집중", [61104, 57942, 56480, 828942, 26841], 1, "ba186bf.gif?=20181220"],
["㉕펭수", 66, 10, 76, "che_event_척사", [38254, 31013, 31044, 584532, 20208], 1, "51540ea.jpg?=20200425"],
["⑨망나뇽", 70, 84, 12, "che_event_척사", [104938, 202890, 30644, 81780, 0], 0, "default.jpg"],
["⑨김불꽃", 70, 10, 88, "che_event_징병", [16304, 47170, 27329, 171851, 5404], 0, "default.jpg"],
["㉖수장", 84, 81, 15, "che_event_위압", [275020, 9883, 12463, 43924, 26502], 1, "190d7ff.jpg?=20200312"],
["⑪철푸덕", 70, 10, 88, "che_event_신중", [16840, 11570, 10514, 226394, 20204], 1, "dbe0759.jpg?=20190708"],
["①미스티", 87, 67, 10, "che_event_위압", [378651, 68043, 59721, 96238, 23956], 0, "default.jpg"],
["⑪반역의루돌프", 82, 66, 13, "che_event_무쌍", [190460, 21059, 34520, 61975, 3668], 0, "default.jpg"],
["⑤랜임살인마", 76, 63, 9, "che_event_궁병", [62454, 70517, 441757, 105519, 9895], 0, "default.jpg"],
["⑨개미호랑이", 72, 10, 88, "che_event_귀병", [52982, 45959, 84663, 774816, 28873], 1, "81a1d5f.jpg?=20190501"],
["⑪평민킬러", 71, 87, 10, "che_event_격노", [451835, 14094, 19512, 79452, 22849], 1, "fba3a6f.jpg?=20190620"],
["③후타바 안즈", 71, 11, 84, "che_event_환술", [29316, 44125, 41520, 383093, 34577], 1, "75c78e2.jpg?=20180907"],
["⑤아미노", 66, 72, 9, "che_event_견고", [81769, 261462, 29042, 83248, 15730], 1, "597abe5.png?=20181111"],
["①아소", 82, 10, 72, "che_event_징병", [14824, 66603, 34577, 274537, 17255], 1, "3d22492.jpg?=20180417"],
["⑥새장속의이상향", 73, 82, 10, "che_event_견고", [75684, 677918, 41624, 173500, 24281], 1, "83e3447.png?=20181025"],
["⑩미스티", 85, 57, 9, "che_event_저격", [0, 0, 0, 0, 362123], 1, "1aadcba.png?=20180908"],
["③EMP", 73, 10, 84, "che_event_격노", [25568, 91687, 11185, 650072, 47161], 0, "default.jpg"],
["①카오스피닉스", 71, 10, 83, "che_event_신중", [44331, 48376, 19179, 385408, 34139], 1, "2af0641.jpg?=20180706"],
["㉗김갑환", 84, 81, 16, "che_event_견고", [103424, 21271, 295071, 105466, 19801], 1, "dcff9fd.jpg?=20180823"],
["⑬템", 69, 11, 83, "che_event_신산", [23431, 14671, 35115, 270986, 3503], 0, "default.jpg"],
["①먹고튄다", 81, 72, 11, "che_event_돌격", [21814, 369560, 15221, 74569, 36015], 1, "c096284.jpg?=20180629"],
["④Card", 87, 69, 10, "che_event_필살", [26268, 337804, 19074, 85932, 19302], 1, "1dcceff.gif?=20180920"],
["㉖Nunsense", 84, 77, 15, "che_event_징병", [21737, 121669, 7073, 50092, 2716], 1, "8b9b041.jpg?=20200721"],
["⑬규일", 69, 10, 85, "che_event_귀병", [5417, 13795, 15760, 191640, 11158], 1, "a5022a1.jpg?=20190816"],
["㉕광순이", 63, 10, 81, "che_event_환술", [63771, 27976, 27041, 399189, 27199], 1, "9babce2.jpg?=20200616"],
["⑨퍼즐앤드래곤", 69, 10, 88, "che_event_척사", [39428, 21497, 29941, 285457, 25634], 0, "default.jpg"],
["⑨리플", 73, 84, 10, "che_event_의술", [345042, 37101, 130088, 93855, 20830], 1, "fb3c625.gif?=20190428"],
["㉕연기처럼", 80, 10, 68, "che_event_신산", [47388, 4723, 10459, 179074, 2474], 0, "default.jpg"],
["㉔펭수", 68, 11, 83, "che_event_필살", [8290, 5150, 3316, 134253, 17116], 1, "51540ea.jpg?=20200425"],
["㉖평민킬러", 75, 92, 15, "che_event_무쌍", [425157, 4564, 31773, 77457, 36426], 1, "fb23a32.jpg?=20190904"],
["④n아텐 누", 86, 72, 10, "che_event_격노", [368263, 44015, 127536, 120706, 11910], 1, "f52b563.png?=20180914"],
["⑬천통해서출세한삐약", 67, 84, 10, "che_event_필살", [7618, 17067, 67890, 13685, 2335], 1, "419b281.png?=20190816"],
["⑨노네임", 88, 72, 10, "che_event_척사", [39814, 296789, 25358, 134299, 5472], 0, "default.jpg"],
["㉗불반도", 74, 17, 92, "che_event_신중", [40918, 14960, 26391, 211317, 10188], 0, "default.jpg"],
["③새벽공방", 71, 10, 84, "che_event_필살", [22487, 25116, 49590, 316421, 25480], 1, "4d11903.jpg?=20180825"],
["㉖현피1을위하여", 73, 16, 83, "che_event_집중", [7590, 9754, 0, 82219, 6348], 0, "default.jpg"],
["⑥Pretty레아", 70, 84, 11, "che_event_필살", [17266, 61082, 270444, 92327, 16896], 0, "default.jpg"],
["⑩북오더", 85, 58, 9, "che_event_저격", [0, 0, 0, 0, 1496612], 1, "4939af6.gif?=20190609"],
["⑥하우젤", 87, 70, 10, "che_event_징병", [18556, 134909, 350604, 68436, 17448], 1, "dcff9fd.jpg?=20180823"],
["②홍재", 78, 10, 71, "che_event_필살", [0, 27863, 24031, 62919, 564], 1, "42a2b81.png?=20180724"],
["①호시노 루리", 85, 70, 11, "che_event_척사", [27525, 590785, 47483, 117144, 146758], 1, "e728603.jpg?=20180713"],
["⑰시그", 72, 84, 10, "che_event_격노", [361479, 3309, 49312, 40914, 46558], 1, "e397de6.jpg?=20191120"],
["㉒개장수", 82, 69, 10, "che_event_무쌍", [16732, 358774, 25853, 76195, 12038], 1, "df79d1c.jpg?=20200428"],
["③만샘", 86, 69, 10, "che_event_징병", [26947, 87453, 405727, 96793, 28901], 1, "37d189c.jpg?=20180420"],
["⑰제노에이지", 67, 12, 82, "che_event_징병", [1109, 23572, 0, 131049, 18895], 1, "ddd1fd7.jpg?=20190320"],
["⑮미야노", 75, 84, 10, "che_event_돌격", [39199, 433325, 32155, 53052, 19503], 1, "ab9cd7d.gif?=20190926"],
["⑦이피스", 90, 9, 9, "che_event_위압", [0, 0, 16459, 16126, 465934], 1, "9e64c3b.jpg?=20190201"],
["⑧카라", 76, 11, 83, "che_event_집중", [20418, 73928, 54682, 629102, 41595], 1, "64170a1.jpg?=20190406"],
["⑮카이스트", 72, 11, 84, "che_event_척사", [20818, 13317, 17376, 304792, 21070], 1, "9361ef8.jpg?=20180907"],
["④광삼이", 76, 10, 80, "che_event_저격", [31122, 54275, 38528, 198861, 12359], 1, "69fc07c.jpg?=20180926"],
["⑪유튜브친구들", 69, 10, 87, "che_event_징병", [31651, 35996, 26865, 232785, 7071], 1, "c213511.jpg?=20190628"],
["㉔카나리아", 69, 10, 84, "che_event_신중", [27037, 20893, 25685, 360770, 35575], 1, "2a52361.jpg?=20200514"],
["⑥해피너스", 72, 10, 84, "che_event_징병", [41029, 47386, 64024, 301987, 15478], 1, "ec883eb.jpg?=20181130"],
["⑥농장소녀", 69, 85, 10, "che_event_척사", [300279, 25273, 36052, 38969, 30195], 1, "d367ca5.jpg?=20181201"],
["⑰Navy마초", 71, 82, 10, "che_event_무쌍", [57935, 62932, 192921, 78948, 3194], 0, "default.jpg"],
["⑤앵서벌서", 76, 66, 9, "che_event_징병", [368363, 77168, 94439, 172024, 10765], 1, "4a0ff8e.gif?=20181117"],
["㉒네티", 83, 70, 11, "che_event_돌격", [20840, 62661, 437362, 63003, 30100], 1, "ac91b73.jpg?=20200423"],
["⑤WA2000", 60, 76, 9, "che_event_위압", [46281, 103947, 4803, 41325, 9234], 1, "786a5da.png?=20181026"],
["②다다", 80, 11, 81, "che_event_징병", [0, 12314, 22911, 106972, 7236], 0, "default.jpg"],
["㉒료우기시키", 82, 69, 10, "che_event_돌격", [26414, 293380, 31786, 83961, 25899], 1, "559083f.jpg?=20190905"],
["㉔행복했으면좋겠어", 69, 10, 83, "che_event_저격", [0, 4680, 3987, 79087, 17716], 0, "default.jpg"],
["⑱우웅...", 73, 10, 87, "che_event_척사", [42145, 49425, 75640, 748505, 70358], 1, "3fb3ee0.jpg?=20191211"],
["⑧쉐도우 핀드", 71, 10, 91, "che_event_집중", [21284, 30636, 28389, 333227, 17077], 1, "18dc25d.jpg?=20190405"],
["⑧광삼이", 73, 11, 80, "che_event_신산", [17853, 2489, 55595, 236139, 12880], 1, "69fc07c.jpg?=20180926"],
["⑧천괴금", 70, 88, 10, "che_event_위압", [9044, 5648, 93306, 20431, 5073], 1, "e15f4b5.png?=20181216"],
["①등갑병", 82, 67, 10, "che_event_돌격", [85376, 7149, 0, 46100, 11418], 1, "97aba2f.gif?=20180628"],
["⑦늘모", 66, 80, 9, "che_event_무쌍", [26031, 85563, 522279, 177658, 18222], 1, "e7c163.png?=20180801"],
["㉒카이스트", 71, 10, 84, "che_event_징병", [39400, 41549, 62758, 438213, 19053], 1, "9361ef8.jpg?=20180907"],
["㉒칸나", 83, 71, 10, "che_event_견고", [44524, 253186, 14678, 39692, 19215], 0, "default.jpg"],
["①뽀빠이", 68, 83, 10, "che_event_저격", [7460, 65561, 57381, 28194, 16111], 1, "2eb5589.png?=20180702"],
["⑨sifmd", 71, 10, 86, "che_event_신중", [29311, 26218, 78883, 341939, 20436], 1, "4d82adb.jpg?=20190325"],
["⑪극새ㅅ사이브ㄹ", 67, 10, 90, "che_event_신중", [10771, 10616, 14553, 202092, 36147], 1, "6e009d9.jpg?=20190620"],
["③신형만", 82, 71, 10, "che_event_징병", [20930, 76782, 325605, 113977, 16035], 1, "b46c3e1.jpg?=20180901"],
["⑱병리학적자세", 71, 10, 89, "che_event_귀병", [21659, 36277, 69559, 925284, 59133], 1, "3679089.jpg?=20180629"],
["③큰가슴의요정", 72, 86, 10, "che_event_위압", [439634, 20784, 35678, 82477, 33598], 0, "default.jpg"],
["④살수묵랑", 75, 80, 10, "che_event_무쌍", [14110, 49444, 295360, 64506, 3643], 1, "a9298fc.png?=20180628"],
["㉖언스", 73, 15, 89, "che_event_신산", [7236, 10797, 7100, 114171, 14910], 0, "default.jpg"],
["⑦노승혜", 79, 69, 9, "che_event_위압", [1222635, 50959, 133840, 216062, 61786], 1, "b3b0886.jpg?=20190219"],
["③제갈여포", 73, 11, 82, "che_event_신산", [58372, 65706, 106074, 587072, 22847], 1, "e8e8adc.jpg?=20180419"],
["⑨권은비", 71, 10, 86, "che_event_반계", [22858, 41805, 16289, 187804, 11011], 1, "65bff5e.jpg?=20190422"],
["⑨무흐흐경찰", 72, 10, 86, "che_event_신산", [23058, 30460, 62228, 435549, 13792], 1, "b2f7938.jpg?=20190315"],
["㉗평온의온도", 75, 15, 91, "che_event_신산", [11622, 21107, 35073, 496002, 24237], 1, "7a86b2f.webp?=20200826"],
["⑯샤를 드 골", 80, 10, 76, "che_event_환술", [6589, 5329, 4012, 96905, 17209], 1, "1386e61.jpg?=20191025"],
["⑮마왕", 72, 87, 11, "che_event_무쌍", [65490, 48577, 497774, 94481, 39755], 1, "eaa25e5.png?=20190910"],
["⑨늅늅", 72, 87, 10, "che_event_돌격", [44614, 44765, 489085, 125401, 18272], 0, "default.jpg"],
["⑧빠른폭탄", 69, 10, 92, "che_event_의술", [6992, 10388, 23628, 64442, 6521], 0, "default.jpg"],
["②니쉬", 64, 10, 83, "che_event_필살", [6119, 14553, 5864, 142135, 21461], 1, "aef78f7.jpg?=20180628"],
["④사운드볼텍스", 71, 84, 10, "che_event_무쌍", [15019, 14896, 276156, 48971, 31531], 1, "13a2ed0.png?=20180920"],
["㉑료우기시키", 81, 70, 10, "che_event_필살", [37897, 53513, 3116, 23685, 7194], 1, "559083f.jpg?=20190905"],
["⑩개미호랑이", 88, 9, 56, "che_event_저격", [0, 0, 0, 0, 1624312], 1, "ba5b648.jpg?=20190610"],
["④rwitch", 78, 10, 77, "che_event_격노", [19441, 41464, 56485, 244304, 12155], 1, "7b3330b.jpg?=20180920"],
["㉑김나영", 69, 86, 10, "che_event_필살", [11974, 40080, 343685, 89224, 15880], 1, "c90a3d4.jpg?=20190905"],
["⑦광삼이", 68, 9, 77, "che_event_귀병", [26255, 37934, 60910, 319547, 8750], 1, "69fc07c.jpg?=20180926"],
["㉓개미호랑이", 90, 10, 68, "che_event_징병", [24005, 37186, 41993, 87944, 134797], 1, "6ee69e8.jpg?=20191227"],
["㉕레벤", 75, 11, 83, "che_event_집중", [54256, 19340, 27550, 546810, 3674], 0, "default.jpg"],
["⑬충차아님", 87, 41, 35, "che_event_저격", [0, 0, 0, 16136, 181093], 1, "39ab9b2.jpg?=20190819"],
["㉕외심장", 67, 10, 80, "che_event_신산", [73815, 54819, 49480, 874449, 16360], 1, "36110cd.jpg?=20180826"],
["㉘킹구", 90, 78, 15, "che_event_위압", [12717, 412793, 13330, 30416, 6219], 1, "fb3c625.gif?=20190428"],
["⑧삼남매아빠", 71, 10, 92, "che_event_집중", [69023, 65980, 73118, 815914, 56232], 0, "default.jpg"],
["㉔춘몽", 67, 10, 86, "che_event_척사", [7441, 10288, 9545, 84376, 7837], 0, "default.jpg"],
["⑨이시리스", 70, 10, 89, "che_event_필살", [19121, 32305, 14316, 205254, 2988], 1, "d51af2d.jpg?=20190411"],
["㉖아르노 화포보병", 73, 15, 90, "che_event_귀병", [3277, 7431, 821, 112098, 8337], 0, "default.jpg"],
["⑦미나", 67, 9, 84, "che_event_징병", [86043, 110806, 97942, 866713, 35725], 1, "6d3a238.jpg?=20190213"],
["㉕킹구", 75, 10, 69, "che_event_신산", [66481, 24853, 38550, 894017, 12077], 1, "fb3c625.gif?=20190428"],
["④드림캐쳐", 68, 9, 88, "che_event_의술", [22724, 23998, 6663, 167555, 15471], 1, "dd09952.png?=20180920"],
["①김소정넘나이쁜것", 69, 11, 81, "che_event_징병", [16254, 39125, 4767, 122728, 23430], 1, "90d437a.jpg?=20180606"],
["②아라쉬", 85, 65, 11, "che_event_저격", [17356, 151274, 45140, 44126, 8993], 1, "6ee4951.jpg?=20180719"],
["⑩이드", 87, 57, 9, "che_event_저격", [0, 0, 0, 0, 1373926], 1, "94635d.jpg?=20190412"],
["②시타", 82, 69, 10, "che_event_돌격", [20852, 153098, 3751, 31836, 2657], 1, "3e9a72e.jpg?=20180719"],
["㉒smile", 70, 10, 84, "che_event_징병", [19418, 26454, 44611, 337945, 22507], 0, "default.jpg"],
["㉓독구", 76, 11, 88, "che_event_환술", [90929, 50662, 52549, 799878, 29052], 1, "ac701b0.jpg?=20180625"],
["⑮연우", 71, 10, 86, "che_event_징병", [26533, 26975, 93587, 344490, 24977], 0, "default.jpg"],
["③진실의 심연", 74, 83, 11, "che_event_돌격", [56662, 99503, 1119895, 181724, 53649], 0, "default.jpg"],
["㉑김갑환", 79, 72, 10, "che_event_돌격", [5165, 6397, 113326, 39268, 7284], 1, "dcff9fd.jpg?=20180823"],
["⑲개미호랑이", 82, 70, 11, "che_event_위압", [23725, 71821, 562276, 73460, 28186], 1, "6ee69e8.jpg?=20191227"],
["㉙낙지꿈", 71, 15, 87, "che_event_신중", [10409, 2823, 3628, 145487, 30121], 0, "default.jpg"],
["⑥만샘", 72, 83, 11, "che_event_돌격", [37015, 374296, 36656, 60899, 32807], 1, "4a206a1.jpg?=20181122"],
["㉕레오루", 75, 68, 10, "che_event_돌격", [54464, 91712, 333212, 113693, 21329], 1, "ed5f2c2.jpg?=20200604"],
["㉕뜨뜨뜨뜨", 70, 10, 74, "che_event_집중", [41344, 34023, 34073, 650064, 12677], 1, "a1ad420.jpg?=20200425"],
["㉕보스곰", 65, 10, 79, "che_event_반계", [61643, 30317, 33826, 553095, 11439], 1, "773556e.gif?=20190822"],
["③くま", 73, 11, 83, "che_event_필살", [113543, 46422, 129400, 604810, 25853], 0, "default.jpg"],
["⑨외심장", 70, 10, 88, "che_event_저격", [16895, 25101, 20493, 256737, 14980], 1, "36110cd.jpg?=20180826"],
["⑧캐서린", 70, 10, 89, "che_event_신산", [37269, 25685, 56910, 368524, 25667], 0, "default.jpg"],
["㉖Hide_D", 74, 15, 92, "che_event_척사", [32339, 30842, 12613, 323136, 30963], 1, "9f0a2a8.png?=20200106"],
["⑧김나영", 72, 87, 10, "che_event_척사", [46077, 39762, 435883, 92282, 13809], 1, "2e8d570.jpg?=20180823"],
["⑩료우기시키", 82, 59, 9, "che_event_저격", [0, 0, 0, 0, 893965], 1, "f1d936e.jpg?=20190217"],
["⑩Hide_D", 88, 9, 56, "che_event_저격", [0, 1516, 8616, 0, 1486882], 1, "ee5accd.jpg?=20190411"],
["⑳AP샤코", 71, 10, 87, "che_event_필살", [34077, 41068, 34664, 509392, 50481], 0, "default.jpg"],
["⑦개미호랑이", 67, 9, 83, "che_event_반계", [82018, 88841, 114731, 647083, 20661], 1, "e1d9077.png?=20190218"],
["③타케우치P", 72, 82, 11, "che_event_격노", [28009, 48039, 489550, 73789, 50493], 1, "abf3658.jpg?=20180910"],
["④나가토 유키", 71, 10, 83, "che_event_반계", [32533, 16472, 21878, 222186, 17906], 1, "1b4d5a9.jpg?=20180922"],
["㉖보스곰", 74, 15, 91, "che_event_집중", [49, 23974, 13619, 316891, 23608], 1, "773556e.gif?=20190822"],
["⑩교통경찰", 83, 57, 9, "che_event_저격", [0, 0, 0, 0, 248443], 1, "579af6f.jpg?=20190510"],
["⑥긴토키", 86, 68, 11, "che_event_척사", [67708, 327058, 28720, 96922, 10584], 1, "1336ea.jpg?=20181025"],
["㉑이드", 71, 83, 10, "che_event_위압", [93925, 127735, 47554, 43226, 16949], 1, "ee26023.jpg?=20200402"],
["⑧Rwitch", 69, 10, 90, "che_event_징병", [20740, 22425, 39676, 266463, 19117], 1, "4d82adb.jpg?=20190325"],
["⑧베니엔마", 86, 72, 11, "che_event_돌격", [295425, 14278, 52491, 93844, 26633], 1, "b5a5e4e.jpg?=20190405"],
["⑲삼모몰라요", 83, 70, 10, "che_event_돌격", [22100, 30046, 388989, 57810, 32699], 0, "default.jpg"],
["④눈의소리", 67, 10, 85, "che_event_격노", [7133, 21070, 15591, 135939, 10696], 0, "default.jpg"],
["⑨카이스트", 71, 10, 88, "che_event_신산", [11329, 15008, 16703, 273114, 26484], 1, "9361ef8.jpg?=20180907"],
["㉖연", 89, 76, 15, "che_event_필살", [0, 14, 20901, 5923, 189277], 1, "574aa5f.png?=20200802"],
["⑦카이스트", 69, 9, 78, "che_event_징병", [98286, 132555, 125719, 673684, 13393], 1, "9361ef8.jpg?=20180907"],
["③ㅇㄷㄷㄷ", 73, 82, 10, "che_event_무쌍", [78723, 322171, 50117, 104444, 12602], 0, "default.jpg"],
["⑦히카와 사요", 67, 82, 10, "che_event_무쌍", [1367, 9882, 117106, 16550, 0], 1, "574cb8d.png?=20190126"],
["㉘낙지꿈", 75, 16, 91, "che_event_환술", [49689, 24687, 49890, 372449, 4668], 0, "default.jpg"],
["⑦황우주", 73, 9, 73, "che_event_저격", [70906, 58833, 121731, 965893, 64148], 1, "f79fd07.gif?=20190224"],
["㉗김나영", 73, 92, 16, "che_event_위압", [59501, 279824, 20812, 68418, 87778], 1, "c90a3d4.jpg?=20190905"],
["㉙조승상", 87, 72, 16, "che_event_척사", [205872, 5254, 26663, 31698, 18211], 1, "bda7d37.jpg?=20200606"],
["⑲금사향", 70, 11, 83, "che_event_격노", [50385, 16064, 26920, 427418, 39780], 1, "4deb6fb.jpg?=20191229"],
["⑪Samo", 71, 85, 10, "che_event_저격", [263737, 8497, 45554, 58067, 17063], 1, "25449b0.png?=20190626"],
["⑨비올레타", 70, 85, 10, "che_event_필살", [45145, 193144, 10076, 60515, 10950], 1, "fe0c1d2.gif?=20190422"],
["⑩아리사", 84, 67, 10, "che_event_저격", [0, 0, 0, 0, 370368], 1, "5c5b7f.jpg?=20190509"],
["⑨제노에이지", 69, 10, 87, "che_event_징병", [20627, 23470, 63776, 279270, 3888], 1, "ddd1fd7.jpg?=20190320"],
["㉘숨질래", 75, 15, 94, "che_event_반계", [6674, 11915, 20850, 167781, 21523], 1, "7a86b2f.webp?=20200826"],
["㉘ZL", 83, 15, 76, "che_event_집중", [16143, 11560, 13934, 100111, 6971], 0, "default.jpg"],
["⑮오리온자리", 85, 74, 10, "che_event_필살", [18166, 246895, 30523, 58132, 7454], 1, "71c0d1f.jpg?=20190718"],
["①시이", 71, 9, 83, "che_event_저격", [15488, 55658, 62704, 272046, 17343], 1, "296c2e3.jpg?=20180711"],
["⑤랜덤is느낌표", 72, 9, 67, "che_event_환술", [70456, 23632, 67615, 380714, 18608], 1, "c15a1aa.jpg?=20181025"],
["㉖낙지꿈", 76, 16, 88, "che_event_신중", [17289, 23226, 23261, 258449, 23656], 0, "default.jpg"],
["①민트토끼", 72, 83, 10, "che_event_돌격", [48062, 337919, 28934, 77427, 29525], 1, "6f87652.jpg?=20180630"],
["㉒くま", 83, 68, 10, "che_event_위압", [29164, 3148, 5368, 19215, 299020], 1, "770f53.jpg?=20190718"],
["㉓딱 고정도", 76, 88, 10, "che_event_견고", [34333, 85930, 841701, 209968, 29277], 1, "ebd1bce.jpg?=20200130"],
["㉗미즈하라 치즈루", 93, 75, 15, "che_event_저격", [24606, 14798, 382590, 109486, 26909], 1, "29b7b6e.gif?=20200814"],
["⑬한동숙", 72, 84, 10, "che_event_저격", [494984, 14486, 34189, 63535, 30610], 1, "4718e6.jpg?=20190814"],
["⑥치카", 87, 67, 10, "che_event_징병", [80535, 370972, 34142, 60488, 17108], 1, "3e9a72e.jpg?=20180719"],
["④맥크리", 74, 82, 10, "che_event_의술", [24840, 59369, 248969, 77578, 6798], 1, "1565b7f.jpg?=20180920"],
["⑧카이스트", 72, 10, 88, "che_event_저격", [5220, 21659, 18515, 135336, 7131], 1, "9361ef8.jpg?=20180907"],
["⑥케이", 70, 87, 10, "che_event_돌격", [10479, 73808, 450815, 51107, 30857], 1, "d797798.jpg?=20181216"],
["⑦대천사하야미", 69, 9, 79, "che_event_징병", [106080, 86719, 144498, 965266, 29305], 1, "a72bf95.jpg?=20190221"],
["⑮아유", 68, 11, 89, "che_event_신산", [20127, 12574, 41074, 157816, 7139], 1, "ba186bf.gif?=20181220"],
["㉓아이즈원내놔", 91, 73, 11, "che_event_돌격", [73856, 132246, 1605504, 382781, 29452], 1, "99cd27f.gif?=20190606"],
["⑲Hide_D", 69, 12, 83, "che_event_필살", [39392, 30865, 9240, 261155, 14354], 1, "9f0a2a8.png?=20200106"],
["㉕독구", 82, 60, 10, "che_event_돌격", [10509, 17612, 10320, 28656, 733732], 1, "a7388f.png?=20200424"],
["⑭우주대스타나나양", 80, 76, 11, "che_event_저격", [537361, 32295, 74636, 120363, 28821], 1, "106e0d2.jpg?=20190918"],
["⑤드류", 62, 77, 9, "che_event_징병", [18350, 133925, 14123, 82371, 6921], 1, "507327d.png?=20181001"],
["⑭치약토끼", 71, 84, 10, "che_event_격노", [53358, 396148, 9361, 60086, 45003], 1, "6227d22.jpg?=20190905"],
["㉖건방진노예", 76, 90, 16, "che_event_무쌍", [23369, 239255, 17766, 38137, 33372], 1, "e1d250.png?=20200719"],
["⑧빵에건포도", 85, 76, 11, "che_event_견고", [4516, 25828, 295716, 66445, 19014], 1, "9e3f1c5.jpg?=20190313"],
["⑭스오우", 72, 10, 85, "che_event_신산", [50435, 28816, 32335, 457562, 22442], 1, "828a91e.jpg?=20190904"],
["⑤블루좀주세요", 64, 9, 77, "che_event_의술", [25799, 43944, 76290, 485586, 21028], 1, "bb7b3c6.jpg?=20181105"],
["⑦강예서", 81, 68, 9, "che_event_무쌍", [163897, 409545, 117136, 195025, 938815], 1, "431f85.png?=20190228"],
["①개발살", 69, 84, 10, "che_event_저격", [3555, 74423, 19016, 28167, 2612], 0, "default.jpg"],
["⑫반역의루돌프", 78, 74, 12, "che_event_척사", [203002, 16060, 10404, 46509, 10693], 0, "default.jpg"],
["④죽창", 85, 69, 11, "che_event_격노", [109441, 60768, 216413, 87920, 6109], 1, "1978b2c.jpg?=20180924"],
["⑪H2O", 79, 10, 74, "che_event_격노", [1214, 14059, 5289, 97232, 13343], 0, "default.jpg"],
["㉕엘사", 64, 80, 10, "che_event_위압", [622649, 45018, 48247, 102993, 23342], 1, "ee26023.jpg?=20200402"],
["⑥노이즈", 67, 11, 87, "che_event_집중", [6495, 7387, 2223, 100300, 7521], 1, "5ad5212.gif?=20181127"],
["⑰로리", 71, 10, 83, "che_event_집중", [17727, 7383, 24982, 317077, 34807], 1, "50794a6.jpg?=20190923"],
["⑩드류", 81, 35, 33, "che_event_저격", [0, 0, 0, 0, 724774], 1, "507327d.png?=20181001"],
["⑬카오스피닉스", 70, 10, 86, "che_event_집중", [18254, 15357, 23458, 244506, 27262], 1, "7f80b2f.jpg?=20190715"],
["③삼남매엄마", 72, 84, 10, "che_event_견고", [243893, 67510, 68501, 93681, 16285], 0, "default.jpg"],
["①소금ㄴㄴ염", 88, 68, 10, "che_event_격노", [14255, 33412, 198801, 39121, 243290], 1, "fb0d354.jpg?=20180629"],
["⑫외심장", 68, 10, 91, "che_event_척사", [21042, 20412, 2754, 118718, 9084], 1, "36110cd.jpg?=20180826"],
["⑩신호등", 81, 56, 9, "che_event_저격", [0, 0, 4756, 0, 408973], 1, "2959525.gif?=20190511"],
["①황제모니카", 70, 10, 86, "che_event_격노", [18951, 26179, 38251, 197419, 26191], 1, "f2d4b4.jpg?=20180629"],
["⑤줄리엣", 63, 77, 9, "che_event_필살", [24721, 174426, 26410, 63979, 7668], 1, "175639b.png?=20181025"],
["㉗슬라임", 87, 79, 15, "che_event_격노", [21520, 37308, 305010, 39429, 55017], 0, "default.jpg"],
["⑧아이린", 71, 89, 10, "che_event_돌격", [83939, 446705, 26572, 87511, 23890], 1, "8dac73c.jpg?=20180922"],
["⑬오버이지", 86, 68, 10, "che_event_저격", [6858, 22030, 289872, 35740, 15463], 1, "4a290e4.png?=20190815"],
["㉔김갑환", 81, 69, 10, "che_event_돌격", [7536, 40387, 145098, 33592, 14972], 1, "dcff9fd.jpg?=20180823"],
["④D.va", 69, 87, 10, "che_event_척사", [374470, 9464, 62295, 60333, 23264], 1, "d8ca176.jpg?=20180919"],
["㉒수장", 82, 70, 10, "che_event_의술", [362536, 15026, 65653, 105385, 20234], 1, "190d7ff.jpg?=20200312"],
["㉑야근요정헹이", 80, 10, 74, "che_event_집중", [13108, 15908, 25246, 187869, 16219], 1, "3db84f2.jpg?=20200405"],
["㉓료우기시키", 90, 75, 10, "che_event_척사", [74465, 507148, 24784, 184921, 21846], 1, "559083f.jpg?=20190905"],
["④니시키노 마키", 70, 10, 85, "che_event_반계", [19810, 14485, 24888, 149081, 4624], 1, "480aa7e.jpg?=20180919"],
["⑦외심장", 70, 9, 78, "che_event_격노", [59247, 80254, 73310, 572424, 27365], 1, "36110cd.jpg?=20180826"],
["⑥숨진사람", 68, 11, 81, "che_event_반계", [2243, 35303, 43827, 131885, 4405], 1, "bf4bcbf.jpg?=20180823"],
["㉕의협왕척신", 77, 74, 11, "che_event_격노", [65192, 12720, 85123, 57418, 5011], 0, "default.jpg"],
["④쿈", 69, 83, 10, "che_event_징병", [8740, 32514, 108713, 46967, 10238], 1, "7cdc752.jpg?=20180920"],
["⑧마법소녀매지컬모모", 75, 85, 10, "che_event_필살", [13025, 153114, 202856, 85880, 19029], 1, "ba6bfc5.jpg?=20190314"],
["④더깨끗해진참이슬", 69, 10, 81, "che_event_격노", [34585, 29646, 11430, 134968, 5943], 0, "default.jpg"],
["㉙잭프로스트", 75, 15, 86, "che_event_척사", [14350, 5835, 3653, 73127, 62457], 1, "733f3b9.jpg?=20201111"],
["⑩김여사", 83, 58, 9, "che_event_저격", [0, 2022, 1890, 0, 542604], 1, "bab8446.jpg?=20190607"],
["⑧란마 12", 74, 86, 10, "che_event_척사", [17450, 30753, 245817, 93589, 20808], 1, "2a88f2a.png?=20190318"],
["⑩무쇠다리사람", 69, 10, 78, "che_event_저격", [0, 0, 0, 0, 319561], 1, "1b4b6f7.jpg?=20190608"],
["㉑피에스타", 81, 73, 10, "che_event_무쌍", [5251, 18155, 215837, 45048, 6918], 1, "99cd27f.gif?=20190606"],
["⑧서든어택2", 72, 10, 88, "che_event_징병", [49923, 58641, 68806, 389356, 29018], 1, "22d133c.jpg?=20190317"],
["⑥아노리엔", 70, 84, 10, "che_event_격노", [16163, 10311, 233386, 79499, 9824], 0, "default.jpg"],
["③시마무라 우즈키", 71, 83, 10, "che_event_필살", [68247, 373352, 36736, 44526, 27060], 1, "11b6f0f.jpg?=20180829"],
["㉒데보라", 85, 68, 10, "che_event_견고", [47724, 452452, 29458, 79232, 33047], 1, "799e90c.jpg?=20200423"],
["㉘하루10분영어야나두", 76, 15, 90, "che_event_집중", [4293, 37851, 3682, 123181, 11194], 1, "c300801.jpg?=20200910"],
["⑲DRX Deft", 79, 72, 10, "che_event_돌격", [117502, 8118, 30454, 26519, 2219], 1, "c0c766b.jpg?=20190815"],
["⑤무지장", 65, 74, 9, "che_event_격노", [47632, 25974, 488333, 103368, 16886], 1, "6c2bf3f.jpg?=20181122"],
["①뒈코뭐리", 70, 10, 84, "che_event_신산", [44689, 36359, 28357, 313820, 41801], 1, "97ec8a1.png?=20180628"],
["⑩심심2", 85, 56, 9, "che_event_저격", [0, 0, 0, 0, 754724], 0, "default.jpg"],
["③♥女神소원♥", 74, 10, 73, "che_event_견고", [6102, 12457, 0, 86695, 0], 1, "6060a6c.jpg?=20180904"],
["⑩카라멜", 88, 9, 9, "che_event_저격", [0, 0, 0, 0, 318633], 1, "9739f48.png?=20190510"],
["④누누하라 캐비지", 70, 10, 82, "che_event_격노", [49090, 26284, 42547, 270649, 16572], 1, "30656e2.png?=20180920"],
["①코사카우미", 76, 10, 80, "che_event_신중", [13115, 14190, 4337, 61830, 357074], 1, "ccc7692.jpg?=20180628"],
["③트런들장인", 72, 79, 11, "che_event_징병", [125653, 13743, 58843, 35809, 6185], 1, "dbede87.png?=20180425"],
["⑤로키", 65, 9, 75, "che_event_징병", [80615, 12769, 74443, 314896, 17839], 1, "d95dd1d.jpg?=20181025"],
["⑯외심장", 67, 10, 88, "che_event_환술", [3948, 0, 2058, 109005, 25085], 1, "36110cd.jpg?=20180826"],
["㉒땅땅", 80, 72, 10, "che_event_저격", [10704, 77264, 216151, 57692, 27618], 1, "99cd27f.gif?=20190606"],
["③감흥", 73, 84, 10, "che_event_필살", [63621, 88372, 607386, 144303, 8012], 1, "eb2cf45.jpg?=20180805"],
["⑥리플", 70, 10, 85, "che_event_신중", [20314, 40644, 24757, 319229, 25505], 1, "fbe44ca.jpg?=20181207"],
["⑲보스곰", 70, 10, 84, "che_event_집중", [8795, 33311, 14253, 280835, 14748], 1, "773556e.gif?=20190822"],
["③쟈이젠 토키코", 78, 9, 78, "che_event_필살", [16208, 26047, 26985, 293544, 14426], 1, "30bafb0.jpg?=20180823"],
["⑩SSS급페라르기니", 83, 10, 59, "che_event_저격", [0, 3867, 10944, 2294, 1211752], 0, "default.jpg"],
["㉗외심장", 76, 15, 91, "che_event_집중", [26982, 17222, 17164, 403788, 16445], 1, "36110cd.jpg?=20180826"],
["㉕륜", 63, 10, 80, "che_event_환술", [71120, 34275, 32861, 476732, 20942], 1, "25c0eb.jpg?=20200607"],
["⑪개촐ㄹ랒필거야 ?", 81, 75, 10, "che_event_격노", [43710, 4749, 0, 18360, 48021], 1, "1b8a3df.jpg?=20190620"],
["⑤뫄뫄", 63, 77, 9, "che_event_징병", [168985, 33880, 273144, 91062, 23927], 1, "9f034b.jpg?=20181030"],
["③인기리", 81, 10, 69, "che_event_신산", [21182, 10179, 50046, 143587, 5778], 0, "default.jpg"],
["⑨다유", 71, 86, 10, "che_event_필살", [47995, 58088, 330058, 111502, 11977], 1, "f489a2a.jpg?=20181226"],
["③카오스피닉스", 72, 10, 85, "che_event_신중", [81854, 50759, 66916, 403873, 30301], 1, "2af0641.jpg?=20180706"],
["②카오스피닉스", 76, 85, 10, "che_event_견고", [30925, 124061, 542844, 92328, 36481], 1, "2af0641.jpg?=20180706"],
["⑧나마즈오", 71, 10, 88, "che_event_신중", [36479, 20869, 74232, 340024, 10479], 1, "b86fc4c.jpg?=20190314"],
["④くま", 72, 10, 83, "che_event_반계", [45619, 49464, 47425, 437741, 22690], 1, "73d75c2.jpg?=20180923"],
["②Hide_D", 75, 10, 86, "che_event_집중", [33038, 129250, 102881, 409778, 20624], 1, "21d378f.jpg?=20180630"],
["⑦★女神소원★", 70, 9, 72, "che_event_반계", [36272, 51928, 58967, 277019, 2763], 1, "6060a6c.jpg?=20180904"],
["⑨파이", 79, 81, 10, "che_event_격노", [40333, 49825, 483081, 128506, 30387], 1, "da3bc86.jpg?=20190318"],
["⑤삼남매엄마", 64, 9, 73, "che_event_귀병", [45545, 24700, 21758, 338849, 9439], 0, "default.jpg"],
["④화난용", 88, 65, 13, "che_event_저격", [24951, 7279, 22790, 29047, 407099], 1, "8b16622.jpg?=20180921"],
["⑤지용갓", 65, 9, 74, "che_event_반계", [29833, 35963, 48671, 328830, 15074], 1, "1f3a6e6.png?=20180926"],
["㉕아벤느채연", 67, 75, 10, "che_event_무쌍", [33562, 23970, 813284, 102045, 15434], 1, "7e512d4.jpg?=20200605"],
["⑦RFB", 68, 9, 79, "che_event_귀병", [62346, 23889, 43049, 598267, 35610], 1, "d0d2db1.jpg?=20190203"],
["⑥메르시", 68, 10, 86, "che_event_신중", [2825, 28774, 38382, 182999, 9831], 1, "8f27f31.jpg?=20181129"],
["②모하지맨", 77, 86, 10, "che_event_무쌍", [11076, 1205715, 121716, 246889, 55700], 0, "default.jpg"],
["⑫스카디", 74, 86, 10, "che_event_견고", [641266, 24147, 31764, 147351, 19158], 1, "112eb1b.jpg?=20190719"],
["⑬오리온자리", 81, 72, 10, "che_event_견고", [30079, 212423, 14163, 41093, 19533], 1, "71c0d1f.jpg?=20190718"],
["⑤객체지향", 75, 66, 9, "che_event_무쌍", [527294, 24804, 96714, 119528, 32968], 1, "bc26a2c.png?=20181121"],
["⑥30초장", 67, 10, 86, "che_event_신중", [12503, 29176, 16675, 87106, 0], 0, "default.jpg"],
["㉖요동중갑대", 73, 15, 88, "che_event_반계", [590, 28842, 14593, 147346, 13927], 0, "default.jpg"],
["⑱사스케", 71, 10, 84, "che_event_격노", [16574, 13483, 11953, 159780, 23209], 1, "986348a.jpg?=20190815"],
["⑨심심", 70, 86, 11, "che_event_저격", [21467, 69808, 318577, 86261, 15195], 0, "default.jpg"],
["⑱DDDD", 69, 11, 80, "che_event_징병", [43052, 16985, 44625, 295243, 24102], 1, "a5c75e7.jpg?=20191223"],
["㉘광성자", 94, 73, 15, "che_event_견고", [17184, 38044, 170768, 46389, 127582], 1, "1c59b40.jpg?=20200929"],
["③진리", 71, 10, 85, "che_event_집중", [80271, 40644, 82521, 322505, 18306], 1, "2055a9d.jpg?=20180828"],
["⑤외심장", 64, 9, 76, "che_event_반계", [36551, 32920, 59862, 345185, 26241], 1, "36110cd.jpg?=20180826"],
["⑮바늘", 70, 12, 82, "che_event_신산", [24864, 18515, 46770, 151469, 0], 0, "default.jpg"],
["⑮병리학적자세", 70, 10, 88, "che_event_징병", [42981, 24141, 22065, 487563, 49719], 1, "3679089.jpg?=20180629"],
["⑪줄리엣", 70, 10, 86, "che_event_환술", [9486, 9392, 8991, 263816, 23308], 1, "70f470f.png?=20190620"],
["⑫여중생", 84, 66, 10, "che_event_무쌍", [3360, 0, 70005, 43002, 6620], 1, "ad8b295.gif?=20190724"],
["㉓등화", 95, 66, 11, "che_event_공성", [6320, 2735, 10597, 47686, 1468280], 1, "f11922a.png?=20200218"],
["㉕헹지", 64, 10, 84, "che_event_신산", [117500, 62426, 46691, 949200, 25920], 1, "ff77e01.jpg?=20200704"],
["③오이치노카타", 71, 10, 85, "che_event_필살", [81889, 26780, 72198, 276316, 13843], 0, "default.jpg"],
["⑳독구", 68, 11, 83, "che_event_신산", [746, 15118, 7000, 229242, 20329], 1, "ac701b0.jpg?=20180625"],
["④보스곰", 78, 10, 79, "che_event_격노", [31668, 58588, 59586, 495181, 22887], 1, "76bf7f6.jpg?=20181007"],
["⑥임사영", 72, 84, 11, "che_event_저격", [453787, 131689, 60736, 154219, 11121], 1, "a0bc4b2.jpg?=20180628"],
["㉒김갑환", 83, 70, 10, "che_event_위압", [27152, 34572, 288182, 78367, 15344], 1, "dcff9fd.jpg?=20180823"],
["⑬강찬밥", 70, 83, 10, "che_event_위압", [247009, 13139, 44297, 49940, 6243], 1, "4b9e4bf.jpg?=20190815"],
["⑩조승상", 75, 61, 9, "che_event_저격", [0, 0, 0, 1978, 242428], 1, "9842c07.jpg?=20190420"],
["⑥Lenn", 87, 67, 11, "che_event_돌격", [27376, 0, 2114, 17210, 149846], 1, "81b73bc.jpg?=20181130"],
["⑤박일아", 66, 74, 9, "che_event_위압", [45084, 46785, 538337, 76826, 21607], 1, "461add7.gif?=20181022"],
["⑳피에스타", 83, 73, 11, "che_event_위압", [649019, 50416, 43493, 129140, 29805], 1, "99cd27f.gif?=20190606"],
["⑪댕찌율", 74, 10, 83, "che_event_집중", [44779, 74127, 29893, 633958, 27449], 1, "99cd27f.gif?=20190606"],
["⑯개미호랑이", 70, 10, 86, "che_event_환술", [6243, 5774, 6185, 119857, 71213], 1, "fccc37e.jpg?=20190718"],
["⑪이쓰미", 73, 10, 84, "che_event_신중", [38767, 30673, 9890, 436618, 19295], 1, "fd6a0fd.jpg?=20181212"],
["⑤라이트유저", 62, 9, 77, "che_event_척사", [40748, 18147, 63785, 240724, 8400], 1, "f546e30.jpg?=20181028"],
["③잉잉", 69, 88, 10, "che_event_돌격", [27436, 24503, 371526, 95074, 25887], 1, "6cfa7ae.jpg?=20180909"],
["⑤SARS", 68, 10, 79, "che_event_신중", [11573, 3397, 13072, 65999, 7509], 1, "b84944.jpg?=20180829"],
["⑨죽창", 67, 87, 10, "che_event_위압", [80117, 25708, 30919, 35669, 10267], 1, "2491818.jpg?=20190411"],
["③미야와키 사쿠라", 82, 71, 11, "che_event_위압", [130054, 515248, 42059, 100575, 18117], 1, "ca1b72d.png?=20180827"],
["㉗네시", 75, 15, 89, "che_event_공성", [6498, 11184, 23694, 186586, 95806], 0, "default.jpg"],
["㉘평민킬러", 78, 90, 15, "che_event_위압", [384279, 9522, 26489, 71101, 38383], 1, "fb23a32.jpg?=20190904"],
["⑤늘모", 66, 9, 75, "che_event_집중", [38156, 31129, 95327, 437701, 14748], 1, "e7c163.png?=20180801"],
["①Card", 83, 70, 10, "che_event_무쌍", [0, 217788, 26117, 41134, 24765], 1, "30f386f.jpg?=20180629"],
["⑧류다희", 72, 87, 11, "che_event_무쌍", [8234, 365821, 6935, 70268, 27470], 1, "781ef76.jpg?=20190317"],
["④실링", 69, 10, 83, "che_event_척사", [38180, 17524, 13330, 117898, 1780], 0, "default.jpg"],
["⑥에스테반 공작", 70, 85, 11, "che_event_저격", [355574, 39777, 71318, 102171, 1225], 1, "d5a955e.jpg?=20181130"],
["⑪외심장", 72, 11, 84, "che_event_의술", [61498, 53874, 39951, 651861, 35599], 1, "36110cd.jpg?=20180826"],
["⑨죽창(+5)", 69, 11, 89, "che_event_격노", [20804, 50624, 42309, 353331, 37462], 1, "1aca7f8.gif?=20190315"],
["㉑리에", 88, 10, 11, "che_event_저격", [14405, 3558, 16877, 43867, 151156], 0, "default.jpg"],
["⑥레이첼가드너", 68, 11, 85, "che_event_필살", [0, 22201, 21582, 83369, 0], 1, "b4bd1a6.png?=20181129"],
["㉓윤세리", 73, 10, 87, "che_event_신중", [63220, 65027, 50981, 570634, 40325], 1, "7753703.png?=20200131"],
["③Body", 77, 10, 80, "che_event_필살", [31681, 51808, 20458, 343215, 21437], 1, "b223c26.gif?=20180830"],
["⑨임사영", 71, 10, 88, "che_event_격노", [27007, 27902, 32753, 476546, 44815], 1, "643224f.gif?=20190314"],
["①제노에이지", 69, 10, 87, "che_event_반계", [6970, 66356, 31132, 249008, 37834], 1, "e90f675.jpg?=20180713"],
["⑫나나", 68, 11, 91, "che_event_귀병", [10737, 13858, 12857, 128824, 24926], 1, "fd86da.jpg?=20190718"],
["⑭천괴금", 85, 68, 10, "che_event_필살", [129422, 18357, 255904, 93132, 41365], 1, "2b239af.png?=20190824"],
["⑥아이린", 72, 84, 10, "che_event_척사", [27803, 76774, 414839, 144309, 16917], 1, "8dac73c.jpg?=20180922"],
["⑨아이린", 71, 87, 10, "che_event_격노", [36892, 325798, 32259, 69414, 12842], 1, "8dac73c.jpg?=20180922"],
["⑭제노에이지", 71, 10, 87, "che_event_반계", [57722, 61231, 21598, 406558, 18736], 1, "ddd1fd7.jpg?=20190320"],
["⑲기", 84, 69, 10, "che_event_무쌍", [19225, 11674, 0, 6389, 206616], 1, "2cf4b70.jpg?=20200120"],
["⑨미도라시", 68, 82, 10, "che_event_무쌍", [6896, 4321, 122388, 23299, 5246], 0, "default.jpg"],
["③플스4수리함", 70, 12, 82, "che_event_필살", [56407, 14933, 45814, 393607, 25664], 0, "default.jpg"],
["⑲rwitch", 71, 10, 83, "che_event_격노", [29318, 23210, 18618, 267372, 11428], 1, "dcc638d.jpg?=20190725"],
["⑧색무새호무새망무새", 80, 10, 76, "che_event_척사", [31525, 30791, 65605, 206908, 12987], 1, "4bc0d69.png?=20190327"],
["㉖다람쥐", 92, 74, 16, "che_event_견고", [48907, 185871, 19335, 52775, 22066], 1, "1161424.gif?=20200718"],
["㉑천조장호무새", 77, 69, 12, "che_event_기병", [11516, 7667, 130823, 14884, 4184], 1, "3649856.png?=20190815"],
["⑬니케", 68, 10, 86, "che_event_귀병", [0, 7275, 26040, 107223, 11850], 1, "fb7addd.jpg?=20190816"],
["⑭Satan", 71, 82, 10, "che_event_징병", [9665, 10441, 204323, 92751, 5284], 1, "eaa25e5.png?=20190910"],
["③테라포밍마스", 71, 13, 83, "che_event_저격", [15572, 64212, 88229, 571580, 21017], 1, "a02d500.jpg?=20180828"],
["③고먐미", 79, 77, 10, "che_event_징병", [456486, 34566, 238111, 68589, 62524], 1, "c353891.jpg?=20180828"],
["㉙둘리", 74, 15, 87, "che_event_집중", [14960, 17290, 17739, 141582, 21609], 1, "df25f02.jpg?=20201114"],
["㉕땅땅이", 77, 66, 10, "che_event_격노", [720463, 28468, 66597, 190037, 30796], 1, "99cd27f.gif?=20190606"],
["㉔레아실비아", 71, 83, 10, "che_event_무쌍", [20636, 0, 106424, 20319, 28302], 1, "3b13bb3.jpg?=20200515"],
["⑩v무광v", 85, 56, 9, "che_event_저격", [0, 0, 0, 0, 690965], 1, "181fefa.jpg?=20190414"],
["②도리도리반도리도리", 83, 83, 10, "che_event_무쌍", [28706, 144837, 1653358, 196769, 88686], 1, "79cffda.png?=20180805"],
["②황금요정", 98, 10, 66, "che_event_신중", [8468, 11651, 18740, 36830, 203763], 1, "d8b3178.jpg?=20180719"],
["㉙게르티카", 82, 75, 17, "che_event_격노", [0, 77819, 720, 20825, 6208], 0, "default.jpg"],
["④인공지능미사일", 71, 12, 83, "che_event_격노", [39981, 34586, 49048, 443571, 33676], 0, "default.jpg"],
["⑤스노우화이트", 66, 9, 75, "che_event_징병", [82923, 27480, 131878, 513649, 26700], 1, "ef55d2c.jpg?=20181115"],
["⑳오비도비", 69, 10, 78, "che_event_저격", [7083, 19452, 0, 98337, 1640], 1, "decc791.jpg?=20200310"],
["⑳네반", 73, 10, 83, "che_event_징병", [28193, 31215, 29802, 695096, 22066], 1, "74df23f.jpg?=20200314"],
["⑫늙고병든활쟁이", 74, 83, 10, "che_event_격노", [356225, 38109, 18885, 95317, 21965], 0, "default.jpg"],
["⑬적벽대전", 69, 10, 80, "che_event_집중", [14189, 0, 7777, 81812, 6477], 0, "default.jpg"],
["⑧이리스 유마", 90, 10, 68, "che_event_저격", [9396, 0, 25842, 35904, 371388], 1, "c46a5b8.jpg?=20190401"],
["⑬애니", 87, 67, 11, "che_event_위압", [27097, 540228, 16576, 48074, 12900], 1, "78c51c9.jpg?=20190818"],
["①조민", 85, 69, 12, "che_event_무쌍", [54691, 398024, 31027, 89301, 24687], 1, "55617fd.png?=20180630"],
["④악동", 86, 71, 10, "che_event_징병", [125889, 193423, 137441, 80164, 12568], 1, "227b2fc.jpg?=20180920"],
["⑨밥벌레", 71, 11, 88, "che_event_반계", [38954, 32856, 60051, 501882, 97392], 1, "297847c.gif?=20190428"],
["⑦북오더", 83, 38, 39, "che_event_집중", [7249, 10603, 3342, 6660, 181084], 1, "c0276e1.gif?=20180917"],
["⑧삼겹살", 83, 11, 77, "che_event_징병", [17715, 80338, 56255, 435695, 17447], 1, "6796cf4.gif?=20190314"],
["③아소", 80, 10, 75, "che_event_반계", [31365, 18211, 58737, 352058, 26051], 1, "3d22492.jpg?=20180417"],
["㉖rwitch", 74, 15, 92, "che_event_신산", [24905, 18221, 25429, 311908, 25534], 1, "dcc638d.jpg?=20190725"],
["⑧후지와라 치카", 92, 67, 10, "che_event_무쌍", [10484, 137314, 23353, 19702, 13417], 1, "5485ec4.gif?=20190319"],
["④아나", 72, 9, 83, "che_event_신산", [20790, 49714, 55572, 298812, 22823], 1, "1216a23.jpg?=20180920"],
["③센", 71, 10, 84, "che_event_의술", [51427, 86980, 59749, 424655, 12064], 1, "d26a001.png?=20180902"],
["①독고다이", 70, 84, 10, "che_event_징병", [7805, 52596, 264410, 79684, 23412], 0, "default.jpg"],
["⑪협동전 하고싶다", 73, 10, 86, "che_event_척사", [69376, 76252, 56149, 716566, 35042], 1, "a2215be.png?=20190625"],
["⑲파크헤트", 84, 69, 10, "che_event_돌격", [36866, 256143, 8644, 55229, 12676], 1, "a18c11b.jpg?=20200109"],
["③김나영", 78, 77, 10, "che_event_무쌍", [47434, 375495, 18135, 111235, 25663], 1, "2e8d570.jpg?=20180823"],
["⑫낙지꿈", 75, 10, 86, "che_event_저격", [62642, 42823, 35801, 500075, 25085], 0, "default.jpg"],
["㉗10068", 75, 15, 90, "che_event_환술", [6406, 18783, 34572, 302122, 39794], 0, "default.jpg"],
["⑨정근", 70, 10, 88, "che_event_신산", [38194, 72460, 30069, 446034, 23656], 1, "f4474c0.jpg?=20190422"],
["⑬치ㅋㄹ타", 85, 69, 11, "che_event_돌격", [5750, 37860, 240155, 53294, 21034], 1, "4188f16.jpg?=20190823"],
["⑬퇴물조승상", 68, 11, 84, "che_event_반계", [24392, 22532, 19322, 173077, 1775], 1, "bf1f881.jpg?=20190707"],
["⑥에르제", 70, 86, 10, "che_event_격노", [14194, 39191, 248560, 84299, 21649], 1, "73c4134.png?=20181130"],
["㉒프로야구개막한다", 70, 11, 82, "che_event_반계", [11776, 0, 15153, 109867, 19282], 1, "ff65fbb.png?=20200423"],
["③빨간망토", 81, 67, 10, "che_event_견고", [34251, 214170, 37906, 50037, 10711], 0, "default.jpg"],
["⑧모니카MK.2", 70, 88, 10, "che_event_징병", [35717, 42019, 45077, 27710, 10968], 1, "250c1a.jpg?=20190312"],
["⑨아시나 겐이치로", 85, 71, 10, "che_event_공성", [22398, 6701, 7811, 17538, 168756], 1, "9842c07.jpg?=20190420"],
["㉑이즈미 쿄카", 71, 10, 83, "che_event_집중", [35839, 3927, 32670, 388117, 43917], 1, "51d4831.jpg?=20200408"],
["③달", 74, 10, 84, "che_event_집중", [68123, 29604, 149395, 506294, 25165], 1, "131b8fd.jpg?=20180824"],
["⑬KZ Deft", 71, 10, 83, "che_event_환술", [12191, 42044, 7441, 266477, 26508], 1, "c0c766b.jpg?=20190815"],
["㉖카이스트", 74, 16, 91, "che_event_징병", [8190, 4741, 13118, 87470, 10919], 1, "9361ef8.jpg?=20180907"],
["⑬견자희", 69, 85, 10, "che_event_격노", [14079, 16931, 358285, 59450, 26994], 1, "316e4e2.jpg?=20190815"],
["④DZTO", 79, 10, 74, "che_event_반계", [39972, 23486, 29842, 333054, 7626], 0, "default.jpg"],
["⑥쉬원찮은남자", 79, 10, 71, "che_event_필살", [27720, 13209, 36067, 245856, 38958], 0, "default.jpg"],
["①불꽃", 68, 10, 84, "che_event_집중", [5796, 40106, 10953, 67198, 2456], 0, "default.jpg"],
["①삼남매아빠", 87, 68, 10, "che_event_견고", [3011, 57971, 168061, 51030, 28879], 0, "default.jpg"],
["④최$ 지존맨 $강", 72, 83, 10, "che_event_척사", [22495, 43959, 362156, 91567, 21610], 1, "2e31c25.jpg?=20181006"],
["④네로", 72, 83, 10, "che_event_견고", [422829, 24798, 49723, 44665, 28785], 1, "b2decfe.png?=20180927"],
["㉓랜덤박스", 84, 80, 10, "che_event_견고", [669433, 26630, 39784, 28854, 23232], 1, "b00a07b.gif?=20200130"],
["⑤스켈레톤", 78, 65, 9, "che_event_위압", [25874, 54281, 781840, 142038, 26036], 1, "50454b0.png?=20181119"],
["⑩이시리스", 85, 55, 9, "che_event_저격", [0, 0, 0, 0, 773039], 0, "default.jpg"],
["④343", 85, 70, 10, "che_event_무쌍", [39943, 52285, 308148, 84021, 140610], 0, "default.jpg"],
["④카이", 83, 71, 10, "che_event_척사", [40798, 263812, 39221, 58508, 9028], 0, "default.jpg"],
["⑯로리", 69, 10, 87, "che_event_저격", [452, 6693, 3634, 222854, 41580], 1, "50794a6.jpg?=20190923"],
["⑲하우젤", 71, 82, 10, "che_event_궁병", [21407, 172082, 2448, 55796, 2828], 1, "dcff9fd.jpg?=20180823"],
["⑤쉬원찮은남자", 64, 10, 75, "che_event_척사", [50767, 18231, 76602, 321282, 17227], 0, "default.jpg"],
["②승우", 70, 79, 11, "che_event_무쌍", [205793, 38430, 31151, 59231, 6872], 1, "af48e72.jpg?=20180721"],
["⑩추레라", 84, 58, 9, "che_event_저격", [0, 2797, 1948, 0, 1199554], 1, "5011028.jpg?=20190515"],
["①꽁치", 69, 86, 10, "che_event_돌격", [8159, 57830, 144615, 32030, 17901], 1, "ca6391e.jpg?=20180711"],
["⑦료우기시키", 73, 77, 9, "che_event_징병", [50710, 183331, 733831, 257065, 13445], 1, "f1d936e.jpg?=20190217"],
["⑫메디브", 83, 78, 10, "che_event_저격", [4560, 3486, 151198, 47860, 18120], 1, "def2895.jpg?=20190719"],
["⑱보스곰", 72, 10, 86, "che_event_신중", [34109, 44140, 26164, 459541, 20593], 1, "773556e.gif?=20190822"],
["㉕아자젤", 65, 10, 79, "che_event_환술", [36772, 50933, 35054, 732544, 5960], 1, "fef67ff.jpg?=20200604"],
["㉔외심장", 71, 10, 82, "che_event_척사", [12208, 24165, 18013, 266992, 15865], 1, "36110cd.jpg?=20180826"],
["㉑킹구", 68, 10, 85, "che_event_징병", [9469, 23365, 15061, 213345, 18004], 1, "fb3c625.gif?=20190428"],
["⑦우수한", 69, 10, 77, "che_event_징병", [83847, 79541, 122440, 965570, 50153], 1, "5dbfd3e.png?=20190125"],
["⑰오포이스", 70, 82, 10, "che_event_필살", [14949, 560068, 24674, 43531, 38159], 1, "c112237.jpg?=20191201"],
["⑨HAL 9000", 71, 10, 88, "che_event_격노", [19845, 42025, 32166, 312439, 22452], 1, "7be52e4.gif?=20190430"],
["㉖루시드", 75, 16, 90, "che_event_집중", [34893, 4052, 20235, 198230, 17599], 1, "c0a402b.jpg?=20200716"],
["㉘전각 9글자", 79, 15, 91, "che_event_집중", [40738, 23533, 54122, 555877, 16563], 0, "default.jpg"],
["⑳차비씨", 71, 10, 86, "che_event_저격", [48941, 44962, 36968, 543977, 34113], 1, "7f3203a.jpg?=20200302"],
["⑯땅땅이", 67, 11, 87, "che_event_집중", [4695, 9101, 4197, 103974, 16629], 1, "99cd27f.gif?=20190606"],
["④호야미야자야", 78, 69, 10, "che_event_저격", [5288, 14058, 92003, 13529, 15310], 1, "fa57127.jpg?=20180926"],
["⑫나나야 시키", 73, 10, 78, "che_event_필살", [11961, 14403, 6858, 117752, 2945], 1, "5e5263e.jpg?=20190314"],
["⑱우대주스나타나양", 72, 10, 87, "che_event_신중", [40102, 69621, 82773, 888404, 73100], 1, "14889af.jpg?=20191221"],
["⑳천괴금", 15, 84, 67, "che_event_견고", [9781, 44346, 179944, 28254, 11020], 1, "2b239af.png?=20190824"],
["⑨돼지바베큐", 72, 10, 85, "che_event_격노", [37411, 16307, 63997, 493576, 22347], 1, "cd72025.png?=20190411"],
["㉘1", 85, 80, 16, "che_event_위압", [21956, 52106, 129707, 47076, 8879], 0, "default.jpg"],
["④곰돌이푸", 69, 10, 87, "che_event_저격", [10531, 38201, 44673, 266221, 21410], 0, "default.jpg"],
["⑤쿠로", 90, 70, 10, "che_event_견고", [30730, 92691, 538411, 97540, 15791], 1, "4154997.jpg?=20181025"],
["⑲평민킬러", 70, 10, 85, "che_event_징병", [16379, 44459, 36106, 424448, 31811], 1, "fb23a32.jpg?=20190904"],
["③whan", 73, 82, 10, "che_event_척사", [76846, 443383, 49310, 87172, 39968], 0, "default.jpg"],
["㉘smile", 75, 15, 92, "che_event_신산", [8491, 27827, 14724, 146641, 6234], 0, "default.jpg"],
["⑤아이린", 64, 75, 9, "che_event_견고", [26936, 54688, 348801, 69258, 13273], 1, "8dac73c.jpg?=20180922"],
["⑪손인", 71, 87, 10, "che_event_무쌍", [30019, 55845, 346588, 137689, 11577], 1, "9382fb7.png?=20190620"],
["⑦돈까스", 68, 84, 9, "che_event_격노", [92530, 791025, 80473, 205908, 22070], 0, "default.jpg"],
["⑬스타킹", 76, 78, 10, "che_event_기병", [6083, 38681, 215713, 55303, 18230], 1, "c995144.png?=20190815"],
["⑯사스케", 81, 10, 10, "che_event_필살", [0, 0, 0, 0, 139769], 1, "986348a.jpg?=20190815"],
["⑭사스케", 89, 10, 10, "che_event_격노", [3241, 5953, 5711, 13410, 306554], 1, "986348a.jpg?=20190815"],
["⑮실", 68, 10, 83, "che_event_귀병", [11627, 6686, 10912, 117722, 3003], 0, "default.jpg"],
["①피피미", 80, 73, 10, "che_event_견고", [33230, 77710, 232993, 54304, 42731], 1, "aeb592.png?=20180629"],
["⑥안가면", 68, 11, 85, "che_event_집중", [14956, 20064, 21876, 146525, 14499], 1, "f0c755.jpg?=20181129"],
["㉖플라", 69, 20, 92, "che_event_집중", [18861, 19765, 15628, 224742, 15548], 1, "816bde9.jpg?=20200716"],
["③라피나", 71, 84, 10, "che_event_척사", [124576, 418214, 41289, 50646, 46622], 1, "cce4134.jpg?=20180823"],
["㉘레벤", 87, 80, 15, "che_event_저격", [16579, 48214, 462530, 89288, 23546], 0, "default.jpg"],
["③리체", 72, 10, 79, "che_event_필살", [30485, 38529, 27709, 313912, 22388], 1, "65f364d.gif?=20180904"],
["⑳수장", 70, 82, 10, "che_event_위압", [17799, 46820, 253293, 106371, 8450], 1, "190d7ff.jpg?=20200312"],
["⑤한국시리즈", 74, 84, 10, "che_event_무쌍", [118574, 549111, 36776, 243158, 15202], 1, "be3b157.jpg?=20181112"],
["⑦마법소년", 81, 67, 9, "che_event_징병", [1186065, 116003, 99628, 209861, 60678], 1, "8c54e0e.jpg?=20190304"],
["①김참치", 57, 25, 83, "che_event_격노", [7096, 26770, 44472, 248922, 15995], 0, "default.jpg"],
["⑧즐라트코", 81, 78, 10, "che_event_저격", [2600, 29273, 166343, 59856, 7804], 1, "4ee6d0c.jpg?=20190318"],
["④민토트끼", 83, 72, 10, "che_event_무쌍", [27848, 48733, 429545, 135358, 14754], 1, "3465a26.jpg?=20181007"],
["⑮실접못하는평킬", 70, 83, 10, "che_event_필살", [353508, 3047, 35475, 54777, 31918], 1, "fb23a32.jpg?=20190904"],
["②시이", 78, 84, 11, "che_event_돌격", [19131, 183642, 1018943, 144288, 55133], 1, "296c2e3.jpg?=20180711"],
["②여고생쨩", 80, 80, 10, "che_event_무쌍", [47843, 132489, 396254, 53683, 45789], 1, "ebd8fab.jpg?=20180728"],
["⑤랜덤의지배자", 64, 9, 76, "che_event_반계", [58094, 33217, 108516, 495066, 40260], 1, "2816ad.jpg?=20181118"],
["①마검", 69, 82, 10, "che_event_저격", [4734, 179503, 20491, 15979, 36975], 0, "default.jpg"],
["⑭돌아온파이", 71, 81, 10, "che_event_견고", [45066, 155344, 2405, 20122, 22247], 1, "f76fa74.jpg?=20190629"],
["⑥아유", 69, 10, 86, "che_event_신산", [4270, 53467, 53560, 289571, 23364], 1, "a8dfd2a.gif?=20181218"],
["㉕아유", 74, 10, 85, "che_event_신산", [47627, 68864, 56976, 887918, 17872], 1, "ba186bf.gif?=20181220"],
["③피카츄", 72, 10, 84, "che_event_필살", [64752, 32178, 43290, 410480, 33542], 0, "default.jpg"],
["⑮료우기시키", 86, 73, 10, "che_event_저격", [91683, 131511, 44904, 53821, 10432], 1, "559083f.jpg?=20190905"],
["③하우젤", 86, 70, 10, "che_event_위압", [576062, 45623, 151100, 112698, 33882], 1, "dcff9fd.jpg?=20180823"],
["㉙외심장", 75, 16, 86, "che_event_징병", [34513, 24662, 17991, 176754, 16421], 1, "36110cd.jpg?=20180826"],
["⑲정채연", 72, 81, 10, "che_event_무쌍", [21345, 303015, 14380, 71163, 23601], 1, "9705097.jpg?=20191001"],
["⑥꾸러기수비대", 70, 11, 80, "che_event_신산", [29277, 10735, 39806, 196613, 7700], 1, "683a086.jpg?=20181204"],
["⑩음주단속", 86, 57, 9, "che_event_저격", [0, 1828, 0, 0, 770309], 1, "71e9cfe.jpg?=20190510"],
["⑨소열제유비", 71, 87, 10, "che_event_척사", [8028, 41684, 306780, 74420, 17002], 1, "a3b1bbb.png?=20180927"],
["⑭낙지꿈", 69, 10, 84, "che_event_저격", [40117, 16579, 31305, 304913, 12195], 0, "default.jpg"],
["④개미", 75, 80, 12, "che_event_돌격", [60996, 446177, 34714, 118909, 20454], 0, "default.jpg"],
["㉙코브라", 75, 15, 87, "che_event_저격", [16519, 4181, 8506, 208875, 17207], 1, "1102963.jpg?=20201112"],
["㉖바넬로피", 88, 78, 15, "che_event_척사", [8120, 31088, 388876, 61107, 28456], 1, "1aef988.jpg?=20200717"],
["⑥심심", 72, 10, 84, "che_event_신산", [26809, 44036, 88607, 498135, 115572], 0, "default.jpg"],
["⑧와이파이", 70, 11, 86, "che_event_징병", [6344, 20753, 9263, 169502, 5044], 1, "da3bc86.jpg?=20190318"],
["⑫정채연", 73, 83, 10, "che_event_격노", [53425, 367655, 10095, 52426, 9710], 1, "bdf89f5.jpg?=20190722"],
["④멀린", 85, 71, 11, "che_event_견고", [23882, 53211, 326979, 84335, 23714], 1, "3451bfb.jpg?=20180920"],
["⑤오펠리아", 64, 9, 76, "che_event_집중", [39501, 6665, 25601, 333480, 9405], 1, "dc83434.jpg?=20181103"],
["㉕카이스트", 66, 10, 82, "che_event_집중", [87729, 60765, 52202, 992377, 25594], 1, "9361ef8.jpg?=20180907"],
["⑪임사영", 68, 10, 89, "che_event_신산", [12085, 26669, 1671, 172397, 7362], 1, "e08aca7.jpg?=20190512"],
["③평민킬러", 74, 82, 10, "che_event_돌격", [758887, 79866, 66446, 170759, 25109], 1, "6add0f.jpg?=20180515"],
["⑤장수는랜덤", 65, 9, 71, "che_event_의술", [45721, 16497, 46701, 265329, 8018], 0, "default.jpg"],
["②모리야 스와코", 70, 10, 91, "che_event_필살", [26582, 59174, 36283, 158109, 12491], 1, "2f1c0c9.gif?=20180718"],
["⑱KOSPI", 74, 86, 10, "che_event_필살", [23897, 461349, 30162, 117749, 15187], 0, "default.jpg"],
["⑧주인장", 73, 85, 10, "che_event_무쌍", [19718, 184736, 83325, 99873, 5162], 0, "default.jpg"],
["①니쉬", 70, 84, 10, "che_event_저격", [58525, 40543, 122529, 31799, 27288], 1, "aef78f7.jpg?=20180628"],
["⑧서른즈음에", 72, 10, 87, "che_event_신중", [51983, 38427, 54410, 367637, 26358], 1, "beb642e.jpg?=20190327"],
["⑤살수묵랑", 67, 73, 9, "che_event_저격", [33956, 358487, 35804, 136415, 9525], 1, "a9298fc.png?=20180628"],
["㉙로리", 73, 15, 88, "che_event_집중", [0, 6291, 2373, 186549, 35041], 1, "50794a6.jpg?=20190923"],
["㉖검호매의눈미호크", 85, 79, 16, "che_event_무쌍", [353639, 0, 26849, 68746, 21657], 1, "284e288.jpg?=20200716"],
["④스타킹", 85, 71, 11, "che_event_궁병", [58425, 260325, 39493, 101338, 17065], 1, "a7d4ef2.png?=20180920"],
["⑨니콜라스 퓨리", 79, 11, 80, "che_event_반계", [68853, 52328, 82188, 613604, 24223], 1, "9cca522.jpg?=20190412"],
["⑪늘모", 64, 82, 18, "che_event_필살", [12380, 65266, 289182, 99172, 10579], 1, "e7c163.png?=20180801"],
["⑬미스티", 68, 11, 83, "che_event_반계", [2621, 15744, 19374, 163340, 8854], 1, "1aadcba.png?=20180908"],
["②갓토갓구미", 70, 77, 10, "che_event_견고", [39635, 128411, 12654, 26474, 6574], 1, "47adc85.jpg?=20180725"],
["㉑아유", 73, 10, 80, "che_event_격노", [17393, 7835, 17966, 117053, 5992], 1, "ba186bf.gif?=20181220"],
["③이드", 73, 84, 10, "che_event_필살", [11360, 97115, 463605, 115796, 30269], 1, "91ca696.jpg?=20180825"],
["⑮스즈나", 71, 88, 10, "che_event_저격", [857144, 15171, 35963, 32322, 33807], 1, "d89ad53.png?=20190925"],
["⑥삐부띵", 69, 10, 80, "che_event_필살", [4733, 4610, 33243, 91410, 6147], 1, "427de4f.jpg?=20181129"],
["④만샘", 70, 11, 84, "che_event_집중", [34403, 32371, 29022, 379080, 16770], 1, "37d189c.jpg?=20180420"],
["⑤참새", 66, 10, 84, "che_event_징병", [373, 17959, 3292, 107285, 17097], 0, "default.jpg"],
["⑩江Yeah서", 85, 66, 10, "che_event_저격", [0, 0, 0, 0, 331990], 0, "default.jpg"],
["⑤북오더", 63, 74, 10, "che_event_징병", [169217, 25652, 100926, 70500, 5765], 1, "c0276e1.gif?=20180917"],
["㉒임사영", 84, 68, 10, "che_event_징병", [185808, 38525, 79234, 47655, 8275], 1, "7f9473e.gif?=20200211"],
["⑮Hide_D", 71, 10, 88, "che_event_격노", [38894, 18427, 54896, 217536, 15474], 1, "ee5accd.jpg?=20190411"],
["⑬짭벌스", 72, 11, 76, "che_event_척사", [14703, 11222, 1337, 102347, 6671], 0, "default.jpg"],
["②제노에이지", 74, 10, 90, "che_event_반계", [61581, 159259, 137652, 602992, 24288], 1, "f80e777.jpg?=20180811"],
["⑥안유진", 68, 10, 87, "che_event_반계", [45338, 37770, 41650, 227356, 9753], 1, "b81ece5.jpg?=20181130"],
["③다비", 72, 85, 10, "che_event_무쌍", [17826, 86197, 325383, 81229, 20807], 1, "2a49cb.jpg?=20180906"],
["⑨싸우지말고삼모해", 70, 10, 87, "che_event_신산", [12208, 24947, 23451, 334364, 18004], 0, "default.jpg"],
["③도트조아", 87, 70, 10, "che_event_필살", [162469, 381037, 58151, 42061, 21131], 1, "61a436c.gif?=20180830"],
["①살수묵랑", 71, 85, 10, "che_event_징병", [17803, 62496, 303653, 88778, 29276], 1, "a9298fc.png?=20180628"],
["⑧유나", 81, 78, 11, "che_event_격노", [56454, 37455, 175917, 65818, 21514], 1, "ed77ad2.jpg?=20190404"],
["㉔ㅇㄷㄷㄷㄷㄷㄷ", 68, 84, 11, "che_event_위압", [47947, 5960, 77638, 20699, 19562], 0, "default.jpg"],
["⑤가라샤", 62, 9, 75, "che_event_격노", [33550, 14210, 35966, 161720, 15502], 1, "7985767.jpg?=20181025"],
["⑯멘헤라짱", 80, 75, 10, "che_event_격노", [137417, 4251, 6085, 6924, 39388], 1, "9e75693.gif?=20190908"],
["⑧무흐흐경찰", 71, 10, 86, "che_event_척사", [6245, 43674, 14905, 165377, 6296], 1, "b2f7938.jpg?=20190315"],
["⑩로드킬", 82, 9, 57, "che_event_저격", [0, 0, 1475, 0, 689089], 1, "4c5a1ed.jpg?=20190510"],
["㉙Vicpie", 72, 16, 87, "che_event_신중", [0, 11476, 10520, 155774, 24653], 1, "c4a956e.png?=20201120"],
["③슈롭셔", 71, 10, 82, "che_event_척사", [35989, 48501, 93463, 250673, 2633], 1, "dcc6251.jpg?=20180825"],
["⑦차기준", 69, 9, 75, "che_event_신중", [69256, 34073, 104499, 688675, 55871], 1, "224e088.jpg?=20190210"],
["②덕장", 85, 75, 10, "che_event_무쌍", [23265, 90235, 366352, 99925, 20239], 0, "default.jpg"],
["㉕치킨조아", 78, 65, 10, "che_event_돌격", [633941, 33282, 117676, 139227, 8707], 1, "798476b.jpg?=20200604"],
["㉗시뉴카린스택계산기", 78, 15, 89, "che_event_저격", [28753, 22731, 42592, 361818, 40488], 1, "59fd00.png?=20200717"],
["②긴토키", 78, 82, 11, "che_event_저격", [14475, 123674, 523310, 122490, 22883], 1, "e2aac86.jpg?=20180719"],
["㉑죄악의 안젤리카", 70, 83, 10, "che_event_돌격", [79, 12424, 248474, 42158, 29102], 1, "7196fd9.jpg?=20200402"],
["⑬귀찮", 67, 84, 10, "che_event_척사", [7460, 6455, 238534, 37280, 16915], 0, "default.jpg"],
["④수비강화", 69, 80, 10, "che_event_필살", [28878, 96940, 12902, 62974, 7189], 0, "default.jpg"],
["⑨광삼이", 76, 10, 79, "che_event_귀병", [14774, 28090, 19856, 158459, 10895], 1, "69fc07c.jpg?=20180926"],
["⑩녹차병", 58, 47, 47, "che_event_저격", [0, 0, 0, 0, 224041], 1, "38960cb.jpg?=20190511"],
["⑤대충함", 87, 69, 10, "che_event_보병", [377167, 65924, 53304, 232141, 5255], 0, "default.jpg"],
["㉔땅땅땅땅", 83, 70, 10, "che_event_격노", [8162, 60987, 278719, 21921, 46219], 1, "99cd27f.gif?=20190606"],
["②whan", 94, 68, 11, "che_event_징병", [97824, 521689, 154313, 109751, 21372], 0, "default.jpg"],
["⑫장수명", 71, 16, 86, "che_event_의술", [21468, 17550, 9632, 164528, 30997], 0, "default.jpg"],
["㉘카이스트", 75, 15, 91, "che_event_신중", [13005, 10025, 31132, 243434, 26760], 1, "9361ef8.jpg?=20180907"],
["⑦꾸드", 78, 10, 70, "che_event_귀병", [13671, 29276, 33355, 120964, 0], 0, "default.jpg"],
["⑪치요", 87, 67, 10, "che_event_격노", [23831, 165268, 0, 81230, 15362], 1, "61db7ca.jpg?=20190620"],
["②IDW", 85, 74, 10, "che_event_필살", [18880, 89425, 380762, 75768, 21020], 1, "e9924eb.gif?=20180720"],
["⑲엔딩요정", 69, 83, 10, "che_event_징병", [8624, 34146, 318730, 54793, 26595], 1, "b568f56.jpg?=20200106"],
["⑫태양탄", 78, 80, 11, "che_event_궁병", [36449, 375355, 16129, 56424, 19445], 0, "default.jpg"],
["㉔유메미 리아무", 74, 79, 10, "che_event_저격", [8690, 76733, 28599, 16019, 28974], 1, "30d9ff0.jpg?=20200514"],
["④타마모노마에", 73, 10, 83, "che_event_격노", [17017, 51116, 82061, 314769, 12197], 1, "4ea7385.jpg?=20181007"],
["③서버열린지모른달팽", 67, 10, 84, "che_event_집중", [17058, 12474, 10035, 105428, 1974], 1, "fb0d354.jpg?=20180629"],
["⑮진짜뉴비", 79, 11, 76, "che_event_반계", [20088, 25510, 5186, 97690, 2411], 0, "default.jpg"],
["㉓병리학적자세", 76, 10, 88, "che_event_귀병", [79534, 68624, 59571, 1156513, 49205], 1, "3679089.jpg?=20180629"],
["⑧G41", 70, 11, 90, "che_event_격노", [4090, 3953, 20278, 71465, 10855], 1, "273580b.gif?=20190402"],
["⑫료우기시키", 71, 10, 88, "che_event_의술", [25047, 22269, 15994, 480555, 22925], 1, "f1d936e.jpg?=20190217"],
["㉘Hide_D", 75, 16, 90, "che_event_척사", [3714, 16503, 9420, 133889, 58423], 1, "9f0a2a8.png?=20200106"],
["⑮아들", 72, 90, 10, "che_event_징병", [53211, 71629, 1058140, 50643, 40919], 1, "97d501f.jpg?=20190926"],
["⑪꽃삔", 71, 11, 86, "che_event_격노", [50429, 60535, 46173, 489285, 28523], 1, "fc715bf.jpg?=20190620"],
["⑪조용히해주실래요?", 81, 68, 10, "che_event_저격", [34935, 259917, 26321, 82577, 6010], 1, "bf1f881.jpg?=20190707"],
["②민트토끼", 77, 11, 87, "che_event_필살", [28684, 108898, 163577, 1255037, 61469], 1, "976f3f5.gif?=20180809"],
["②Card", 91, 67, 10, "che_event_징병", [75236, 481508, 110116, 119890, 36199], 1, "416a697.jpg?=20180720"],
["⑦사키", 70, 76, 9, "che_event_저격", [25748, 113840, 532850, 204611, 14626], 1, "e43844d.gif?=20181113"],
["⑪리안", 69, 10, 87, "che_event_필살", [13785, 19145, 14669, 153616, 13975], 0, "default.jpg"],
["㉘클로토", 76, 91, 15, "che_event_견고", [22608, 17732, 418884, 64188, 16802], 1, "c0a2d90.jpg?=20200910"],
["⑪병리학적자세", 70, 10, 88, "che_event_저격", [31215, 27626, 29999, 309583, 14674], 1, "3679089.jpg?=20180629"],
["㉕smile", 66, 10, 79, "che_event_집중", [56287, 39419, 47404, 604863, 11707], 0, "default.jpg"],
["①평민킬러", 71, 83, 11, "che_event_돌격", [399767, 36713, 64437, 81230, 20257], 1, "6add0f.jpg?=20180515"],
["㉑트리니티", 85, 68, 10, "che_event_무쌍", [802, 23235, 144392, 34851, 93154], 0, "default.jpg"],
["⑯정채연", 67, 85, 10, "che_event_척사", [8858, 95151, 4068, 7657, 12249], 1, "9705097.jpg?=20191001"],
["⑩스즈나", 85, 57, 9, "che_event_저격", [0, 0, 0, 0, 889347], 1, "1945e60.jpg?=20190513"],
["㉔연채홍", 68, 10, 84, "che_event_척사", [0, 6658, 18873, 101277, 26492], 0, "default.jpg"],
["㉑수장", 82, 71, 12, "che_event_격노", [21861, 209481, 4810, 20593, 40147], 1, "190d7ff.jpg?=20200312"],
["⑥모코코", 70, 11, 85, "che_event_신산", [34225, 30063, 32596, 288053, 18890], 1, "7d0a397.jpg?=20181218"],
["㉘시뉴카린임포스터", 76, 90, 15, "che_event_돌격", [243109, 19191, 21407, 75205, 30235], 1, "59fd00.png?=20200717"],
["⑤슬라임", 64, 76, 9, "che_event_척사", [22637, 46018, 221505, 75650, 15658], 1, "f061c0e.jpg?=20181105"],
["⑨병리학적자세", 72, 10, 88, "che_event_저격", [20646, 69902, 45518, 382993, 11007], 1, "3679089.jpg?=20180629"],
["㉘제로원", 75, 86, 15, "che_event_무쌍", [39183, 135884, 18796, 67457, 5973], 1, "64b105a.jpg?=20200921"],
["②아저씨쨩", 78, 10, 85, "che_event_집중", [16085, 170462, 111393, 1062153, 54363], 1, "dff1ae6.jpg?=20180729"],
["㉕한국전력공사", 60, 10, 82, "che_event_신산", [34672, 25221, 14248, 424199, 21471], 1, "48ea511.jpg?=20200607"],
["㉔경계의 거주자", 86, 68, 10, "che_event_돌격", [7501, 144604, 6967, 33641, 11209], 1, "7c6608.jpg?=20200514"],
["⑲올때메로나", 80, 10, 72, "che_event_징병", [31626, 11884, 40946, 310670, 16965], 0, "default.jpg"],
["③도적", 71, 84, 10, "che_event_저격", [13924, 67488, 444614, 114951, 25573], 0, "default.jpg"],
["⑳박일아", 74, 86, 10, "che_event_돌격", [1052480, 17654, 15193, 20597, 18727], 1, "8608979.gif?=20191024"],
["③아유", 72, 84, 10, "che_event_돌격", [83293, 544334, 37522, 173364, 42717], 1, "8645fa9.gif?=20180906"],
["⑩삽가면", 86, 66, 11, "che_event_저격", [0, 0, 9135, 0, 761899], 1, "f0c755.jpg?=20181129"],
["⑩숨질래", 73, 9, 60, "che_event_저격", [0, 0, 0, 0, 409488], 1, "b2f7938.jpg?=20190315"],
["⑪드류", 70, 10, 83, "che_event_신중", [16000, 21348, 10760, 123265, 5447], 1, "507327d.png?=20181001"],
["④진리", 82, 72, 10, "che_event_위압", [12470, 32238, 230186, 76320, 20857], 1, "7d6190a.jpg?=20180926"],
["⑮장손신희", 66, 11, 85, "che_event_반계", [15890, 2858, 6680, 79399, 10993], 1, "e7a5bde.jpg?=20190329"],
["⑫뒹굴뒹굴", 70, 10, 84, "che_event_귀병", [33960, 3235, 22837, 184288, 2221], 0, "default.jpg"],
["⑮륜", 70, 10, 90, "che_event_징병", [19534, 19688, 7029, 201790, 2672], 1, "6561977.jpg?=20190904"],
["㉗낙지꿈", 74, 16, 91, "che_event_신산", [45995, 10026, 20324, 258916, 10631], 0, "default.jpg"],
["④병리학적자세", 81, 10, 76, "che_event_집중", [19719, 43710, 35952, 368533, 31949], 1, "3679089.jpg?=20180629"],
["⑬얀핀", 77, 79, 10, "che_event_위압", [55131, 362929, 31351, 65031, 24738], 1, "c25ad46.jpg?=20190816"],
["⑰개미호랑이", 69, 10, 83, "che_event_격노", [13306, 10700, 19979, 153411, 94562], 1, "fccc37e.jpg?=20190718"],
["②Google", 74, 10, 86, "che_event_환술", [23750, 61822, 156962, 496477, 35361], 1, "7ee028e.gif?=20180721"],
["⑤아노리엔", 73, 88, 10, "che_event_위압", [38676, 45064, 560901, 163416, 23959], 0, "default.jpg"],
["①새장속의 이상향", 71, 10, 83, "che_event_신중", [36541, 68008, 106667, 616052, 54180], 0, "default.jpg"],
["⑤거베라", 65, 73, 9, "che_event_척사", [93382, 580590, 71425, 118121, 24082], 1, "e826340.jpg?=20181115"],
["⑭메디브", 83, 75, 10, "che_event_의술", [27646, 49106, 449314, 53533, 18439], 1, "def2895.jpg?=20190719"],
["⑤영원한랜임", 60, 9, 79, "che_event_격노", [36277, 32636, 9351, 190876, 7650], 0, "default.jpg"],
["⑥로리", 73, 10, 84, "che_event_필살", [51453, 33727, 28591, 372598, 18141], 1, "445a7f4.jpg?=20181129"],
["⑧외심장", 71, 10, 87, "che_event_척사", [16060, 41612, 41494, 352823, 21193], 1, "36110cd.jpg?=20180826"],
["①무한파워안경", 85, 70, 10, "che_event_위압", [41570, 28860, 303636, 71899, 39164], 1, "b7e1a1f.jpg?=20180525"],
["⑮덕장", 72, 10, 87, "che_event_집중", [60279, 37048, 48083, 478787, 29076], 0, "default.jpg"],
["㉘외심장", 76, 15, 90, "che_event_신중", [16021, 6026, 21154, 177875, 16952], 1, "36110cd.jpg?=20180826"],
["⑭룬레이", 86, 66, 11, "che_event_저격", [48674, 308432, 13523, 95716, 14939], 1, "bd6cc98.png?=20190905"],
["⑪아유", 71, 10, 82, "che_event_징병", [59952, 16393, 27530, 293398, 15155], 1, "ba186bf.gif?=20181220"],
["⑥휘인", 86, 68, 10, "che_event_견고", [3960, 17805, 230997, 29824, 38750], 1, "8bec25d.jpg?=20181129"],
["⑪쀼웃", 70, 10, 82, "che_event_필살", [36225, 14104, 11461, 210431, 9964], 1, "8996332.jpg?=20190315"],
["⑭나루호도", 79, 76, 10, "che_event_저격", [225616, 13935, 20803, 72916, 9349], 1, "e9b556b.png?=20190905"],
["④이바라키도지", 75, 78, 11, "che_event_저격", [58062, 275499, 13534, 77392, 12635], 1, "6735612.png?=20180920"],
["⑦경국지색소교", 64, 81, 9, "che_event_필살", [4009, 16739, 236415, 153934, 10254], 0, "default.jpg"],
["⑤황약사", 66, 80, 12, "che_event_기병", [5739, 0, 106579, 33299, 5129], 1, "189adc9.jpg?=20181115"],
["⑨쌀벌레", 69, 11, 87, "che_event_신중", [14989, 7754, 10289, 216518, 25579], 1, "e9a3054.jpg?=20190315"],
["③유미", 71, 10, 84, "che_event_집중", [18734, 49532, 51115, 382886, 25317], 1, "bf4bcbf.jpg?=20180823"],
["㉘병리학적자세", 75, 15, 93, "che_event_집중", [33042, 13166, 8892, 229034, 32235], 1, "3679089.jpg?=20180629"],
["⑦김나영", 68, 77, 9, "che_event_징병", [490784, 39405, 166629, 221483, 11314], 1, "2e8d570.jpg?=20180823"],
["③오펠리아", 74, 10, 82, "che_event_척사", [62649, 52183, 137323, 423634, 29394], 1, "c20959.jpg?=20180906"],
["⑤호도르", 63, 9, 77, "che_event_의술", [51745, 10578, 71204, 367610, 25425], 1, "4a206a1.jpg?=20181122"],
["⑫로 리", 71, 10, 89, "che_event_필살", [72041, 48050, 34209, 599557, 36300], 1, "1ec690f.jpg?=20190720"],
["⑥궁Yeah!", 80, 10, 76, "che_event_저격", [2261, 29859, 13428, 138751, 8782], 1, "cc5e216.jpg?=20181129"],
["④Barista", 70, 10, 86, "che_event_귀병", [26275, 10925, 43265, 269277, 13386], 1, "9c445c8.gif?=20180921"],
["③아이돌도서관협회장", 72, 84, 10, "che_event_필살", [410570, 38607, 72735, 95230, 31749], 1, "76b0000.gif?=20180909"],
["⑮PA15", 76, 82, 11, "che_event_필살", [20647, 62275, 485064, 88074, 31181], 1, "415b256.png?=20190927"],
["⑨줄리엣", 71, 84, 10, "che_event_척사", [46512, 359054, 37499, 85767, 17706], 1, "175639b.png?=20181025"],
["⑧두나", 72, 10, 88, "che_event_신중", [9198, 32485, 49266, 372136, 6485], 1, "bf0c572.png?=20181114"],
["⑭KZ Deft", 81, 72, 10, "che_event_견고", [165500, 2408, 18711, 58319, 9120], 1, "c0c766b.jpg?=20190815"],
["㉗코드블루", 75, 15, 90, "che_event_집중", [3436, 23852, 39886, 465912, 42245], 1, "5c60836.jpg?=20200813"],
["⑪학식대장", 82, 74, 10, "che_event_저격", [94798, 49581, 20829, 31553, 15007], 1, "49f13e6.png?=20190520"],
["⑳평민킬러", 77, 81, 10, "che_event_무쌍", [598943, 23647, 54300, 175331, 24829], 1, "fb23a32.jpg?=20190904"],
["①홈", 71, 10, 84, "che_event_반계", [63332, 52361, 18238, 258378, 22237], 1, "fc4c099.jpg?=20180630"],
["⑲퀸안나", 75, 10, 77, "che_event_신산", [18595, 16646, 10793, 147830, 12837], 1, "99e4505.jpg?=20200110"],
["④5분장", 71, 10, 86, "che_event_저격", [43685, 31600, 35551, 391767, 25294], 0, "default.jpg"],
["⑦힝힝", 63, 72, 9, "che_event_돌격", [211569, 4705, 22236, 126718, 10296], 1, "c5ca50e.jpg?=20190130"],
["⑫사스케", 92, 10, 10, "che_event_위압", [7623, 3550, 5654, 5813, 430100], 1, "bab8446.jpg?=20190607"],
["㉒탈모카린", 68, 12, 80, "che_event_반계", [6717, 11861, 47508, 203607, 19801], 1, "aaacec4.jpg?=20200427"],
["⑧Rumi", 71, 89, 10, "che_event_무쌍", [40825, 66807, 482496, 97742, 29745], 1, "84fb262.png?=20190314"],
["⑩마요이", 72, 77, 10, "che_event_저격", [0, 0, 0, 0, 254812], 1, "3d319b5.png?=20190422"],
["⑬유리빠돌이", 68, 10, 80, "che_event_환술", [12077, 28522, 24466, 132326, 4504], 0, "default.jpg"],
["⑮조말론", 72, 10, 85, "che_event_집중", [13503, 6726, 52720, 211475, 3670], 1, "e12bfb7.jpg?=20191001"],
["②밤비에타", 75, 78, 14, "che_event_돌격", [235193, 44649, 43754, 65854, 16736], 1, "2840769.jpg?=20180730"],
["①고긔가먹고싶", 70, 10, 86, "che_event_징병", [52092, 20109, 7138, 239333, 16712], 1, "70c5cf6.jpg?=20180623"],
["㉘아리아", 74, 93, 15, "che_event_척사", [221784, 5449, 32195, 72028, 16420], 1, "f7358b.jpg?=20200822"],
["⑥복숭아좋아", 71, 10, 85, "che_event_격노", [15127, 12876, 23291, 165544, 14204], 0, "default.jpg"],
["㉓이초홍", 80, 83, 10, "che_event_견고", [117853, 925037, 44641, 215616, 25081], 1, "670e1e3.jpg?=20200130"],
["⑧슬라임", 82, 78, 10, "che_event_저격", [83530, 17089, 36823, 18948, 5846], 1, "3408568.jpg?=20190317"],
["⑭짹짹이", 71, 11, 85, "che_event_반계", [34498, 32833, 41851, 437972, 20612], 1, "4cabf8f.png?=20190905"],
["⑦조밧", 79, 64, 9, "che_event_필살", [47533, 219940, 24251, 121558, 6468], 1, "f2d4b4.jpg?=20180629"],
["⑦평민킬러", 69, 80, 9, "che_event_격노", [871962, 81066, 132754, 251904, 21580], 1, "2816ad.jpg?=20181118"],
["⑧치자피즈", 71, 84, 10, "che_event_저격", [51019, 227855, 31933, 60646, 21474], 1, "6103eff.png?=20190316"],
["㉖시라이", 76, 89, 15, "che_event_척사", [240284, 6177, 33912, 39817, 18943], 1, "9f7aca2.jpg?=20200718"],
["⑧박초롱", 68, 91, 10, "che_event_징병", [115639, 1707, 13001, 22302, 11315], 1, "4cb21c1.gif?=20190318"],
["⑲yapyap30", 70, 11, 83, "che_event_신산", [20455, 17279, 18888, 303560, 40713], 0, "default.jpg"],
["④SARS", 71, 11, 83, "che_event_저격", [18250, 5782, 8007, 119595, 21967], 1, "b84944.jpg?=20180829"],
["⑰카이스트", 70, 10, 83, "che_event_징병", [29383, 31445, 41054, 455449, 40166], 1, "9361ef8.jpg?=20180907"],
["⑲료우기시키", 81, 72, 10, "che_event_척사", [24607, 86986, 6449, 25518, 56464], 1, "559083f.jpg?=20190905"],
["⑦우동게", 67, 9, 81, "che_event_환술", [23513, 24340, 76353, 409890, 21438], 1, "6eb775f.jpg?=20190125"],
["⑦잠입", 62, 14, 77, "che_event_신중", [7407, 5962, 38798, 90749, 932], 0, "default.jpg"],
["㉕ㅇ", 66, 79, 10, "che_event_저격", [39814, 59167, 549078, 83500, 21298], 0, "default.jpg"],
["④현랑호로", 72, 83, 10, "che_event_저격", [44807, 154200, 35108, 73324, 4977], 1, "eeb3cdb.jpg?=20180807"],
["⑤쭀", 62, 9, 76, "che_event_척사", [48317, 31939, 28204, 293075, 15064], 0, "default.jpg"],
["②미래의얼굴", 73, 11, 89, "che_event_격노", [29279, 108903, 115516, 434119, 37203], 1, "fcd3a25.jpg?=20180720"],
["⑥제갈여포", 74, 10, 82, "che_event_필살", [50285, 39362, 42881, 519681, 27609], 1, "e8e8adc.jpg?=20180419"],
["㉗일이석우", 74, 15, 90, "che_event_신산", [22677, 3107, 22321, 169066, 21499], 1, "9c2026b.jpg?=20200527"],
["㉕멸망을 노래하는 자", 80, 64, 10, "che_event_돌격", [614171, 40420, 68970, 136172, 14291], 1, "179a0e6.jpg?=20200606"],
["⑨장손신희", 72, 84, 10, "che_event_격노", [499965, 41999, 118131, 143615, 14149], 1, "e7a5bde.jpg?=20190329"],
["㉒타임머신", 81, 71, 10, "che_event_위압", [32266, 297661, 24425, 82058, 32824], 1, "75f9605.png?=20200423"],
["⑦소열제유비", 65, 77, 11, "che_event_기병", [18517, 21523, 314133, 151096, 13824], 1, "a3b1bbb.png?=20180927"],
["⑥빙글빙글", 83, 70, 10, "che_event_돌격", [72217, 188944, 147171, 115643, 8365], 1, "6550a50.gif?=20181129"],
["③타카가키 카에데", 80, 9, 75, "che_event_격노", [46938, 63264, 24115, 351906, 82507], 1, "22f307c.jpg?=20180825"],
["㉘Per", 87, 81, 15, "che_event_위압", [31187, 494344, 3253, 110288, 24963], 1, "2fa4389.jpg?=20200910"],
["④미스티", 72, 10, 83, "che_event_집중", [21473, 41944, 28143, 213512, 14562], 1, "1aadcba.png?=20180908"],
["㉓Mihimaru", 71, 88, 10, "che_event_견고", [49075, 36022, 327879, 72023, 16446], 0, "default.jpg"],
["⑧모니카", 72, 10, 85, "che_event_신중", [0, 15212, 19800, 132450, 10780], 1, "def3814.jpg?=20190314"],
["③올턴휴식", 70, 10, 81, "che_event_집중", [20583, 43380, 43612, 225552, 16872], 0, "default.jpg"],
["⑦하하", 86, 71, 11, "che_event_척사", [30726, 41020, 124894, 108334, 8266], 0, "default.jpg"],
["③뇨와킨", 79, 72, 10, "che_event_돌격", [83406, 49598, 108612, 26937, 438], 1, "f3f2f.png?=20180830"],
["㉗크류", 75, 89, 17, "che_event_척사", [6631, 27238, 154457, 40746, 48676], 1, "51540ea.jpg?=20200425"],
["⑥늘모", 70, 84, 11, "che_event_저격", [28753, 192603, 19107, 42129, 15068], 1, "e7c163.png?=20180801"],
["②신포", 90, 70, 10, "che_event_필살", [282732, 113659, 57028, 72492, 7779], 0, "default.jpg"],
["⑮김나영", 73, 85, 10, "che_event_저격", [315693, 11806, 41408, 69215, 16465], 1, "c90a3d4.jpg?=20190905"],
["㉑개미호랑이렐리아", 86, 66, 10, "che_event_격노", [14878, 23905, 151410, 56369, 5684], 1, "9e0429e.jpg?=20200313"],
["④(9글자 이내)", 71, 82, 10, "che_event_격노", [151173, 26297, 45329, 46550, 1978], 0, "default.jpg"],
["⑯병리학적자세", 67, 10, 90, "che_event_집중", [6833, 3988, 0, 92463, 20098], 1, "3679089.jpg?=20180629"],
["⑫아바브와", 75, 86, 10, "che_event_격노", [6173, 21407, 151848, 49781, 13133], 0, "default.jpg"],
["④체비", 67, 11, 86, "che_event_징병", [25005, 27745, 12597, 199998, 0], 1, "fd20d5d.png?=20180724"],
["⑭로리", 72, 11, 84, "che_event_집중", [45950, 43822, 39507, 685740, 32444], 1, "e1e175f.jpg?=20190921"],
["③ㅊㅂ", 88, 62, 15, "che_event_저격", [215952, 9669, 67013, 60043, 6176], 1, "fd20d5d.png?=20180724"],
["㉓ㅇㄷ", 75, 88, 10, "che_event_견고", [38610, 110956, 783518, 158714, 35048], 0, "default.jpg"],
["⑮나도 모르겠다", 70, 10, 88, "che_event_집중", [5670, 10089, 16154, 145569, 4204], 0, "default.jpg"],
["⑩과학 5호", 86, 10, 57, "che_event_저격", [0, 0, 0, 0, 1695198], 0, "default.jpg"],
["㉗건방진노예", 75, 15, 89, "che_event_신중", [27021, 12661, 21393, 286784, 10487], 0, "default.jpg"],
["㉙I", 74, 88, 15, "che_event_보병", [113568, 3852, 31882, 12954, 7743], 0, "default.jpg"],
["⑨두나", 70, 10, 87, "che_event_환술", [142, 19581, 6691, 130481, 6677], 1, "bf0c572.png?=20181114"],
["④뇽", 77, 9, 79, "che_event_반계", [12627, 29940, 63638, 234693, 12687], 0, "default.jpg"],
["㉕김나영", 68, 77, 10, "che_event_돌격", [35094, 69704, 739190, 96183, 14499], 1, "c90a3d4.jpg?=20190905"],
["㉒사이언스베슬", 68, 10, 84, "che_event_집중", [42415, 44878, 38642, 306662, 10603], 1, "58d2ea1.jpg?=20200425"],
["⑱기절중...zzZ", 91, 11, 10, "che_event_돌격", [0, 0, 17379, 15434, 394838], 0, "default.jpg"],
["⑨아노리엔", 73, 81, 10, "che_event_저격", [34286, 308787, 18993, 117144, 8219], 1, "99d6aca.png?=20190320"],
["⑩카오스피닉스", 82, 34, 33, "che_event_저격", [0, 1231, 2677, 2398, 507552], 1, "2af0641.jpg?=20180706"],
["⑥캐릭캐릭체인지", 71, 11, 83, "che_event_격노", [47781, 22334, 68661, 324566, 17359], 1, "27c35d6.jpg?=20181214"],
["⑥황약사", 77, 80, 10, "che_event_징병", [28633, 364869, 147731, 159828, 18197], 1, "189adc9.jpg?=20181115"],
["②커피", 77, 86, 10, "che_event_격노", [18743, 85671, 834631, 182915, 40817], 1, "1c44e87.png?=20180723"],
["㉙Hide_D", 74, 16, 85, "che_event_필살", [12332, 7552, 7410, 104948, 22629], 1, "9f0a2a8.png?=20200106"],
["⑬망나뇽", 81, 69, 10, "che_event_척사", [113772, 16450, 22026, 32283, 393], 1, "d879a26.png?=20190427"],
["㉑KOSPI", 71, 84, 10, "che_event_척사", [382638, 19921, 31751, 99042, 14992], 0, "default.jpg"],
["⑳슬라임", 83, 10, 75, "che_event_환술", [46881, 90041, 38303, 460956, 16917], 1, "2fd8b5.png?=20191222"],
["㉖외심장", 73, 16, 93, "che_event_신산", [22637, 8581, 19454, 231309, 29120], 1, "36110cd.jpg?=20180826"],
["⑤G41", 64, 9, 74, "che_event_저격", [65903, 13670, 51029, 298740, 22131], 1, "363358d.jpg?=20181114"],
["⑭로또", 73, 82, 10, "che_event_징병", [362999, 29258, 44191, 150706, 15492], 1, "db84779.jpg?=20190906"],
["②농다리", 77, 89, 10, "che_event_척사", [13872, 1432650, 138020, 178094, 84040], 1, "eb2cf45.jpg?=20180805"],
["⑨버즈", 88, 71, 10, "che_event_척사", [655070, 17681, 103562, 120047, 36401], 1, "1168f40.jpg?=20190415"],
["⑪아련그자체", 70, 10, 87, "che_event_신산", [14597, 12254, 16162, 168069, 37863], 1, "57e4a39.jpg?=20190620"],
["②륜", 69, 10, 82, "che_event_반계", [9465, 65135, 21658, 174995, 24232], 1, "96061d5.jpg?=20180708"],
["⑧아노리엔", 87, 70, 11, "che_event_저격", [169509, 165658, 57039, 96205, 18393], 1, "99d6aca.png?=20190320"],
["⑱개미호랑이", 90, 10, 66, "che_event_신산", [43580, 55879, 26226, 77467, 242248], 1, "6ee69e8.jpg?=20191227"],
["⑨5분장", 71, 10, 86, "che_event_신중", [9133, 75389, 76279, 562259, 18920], 0, "default.jpg"],
["⑧동백쨔응", 73, 11, 88, "che_event_척사", [33009, 51680, 51617, 471256, 25827], 1, "c07de23.jpg?=20190314"],
["⑮개미호랑이", 69, 10, 88, "che_event_징병", [45439, 38894, 26057, 483691, 40161], 1, "fccc37e.jpg?=20190718"],
["⑥라이토", 74, 78, 11, "che_event_무쌍", [4988, 43961, 107676, 32400, 11792], 1, "876997e.png?=20181210"],
["⑫꽃핀", 75, 10, 89, "che_event_격노", [36802, 34787, 39921, 764517, 28955], 1, "587c5f5.jpg?=20190808"],
["㉑임사영", 69, 11, 86, "che_event_의술", [2685, 5472, 22873, 138521, 21509], 1, "7f9473e.gif?=20200211"],
["④꼬기", 69, 10, 85, "che_event_척사", [86146, 30092, 14847, 297025, 35395], 1, "507327d.png?=20181001"],
["㉖사뉴카린시등분", 74, 15, 92, "che_event_집중", [12029, 16754, 14228, 338646, 8933], 1, "326e427.jpg?=20200723"],
["㉘임사영", 74, 15, 93, "che_event_척사", [1197, 9238, 13259, 117696, 7349], 1, "7f9473e.gif?=20200211"],
["㉒로리", 69, 11, 83, "che_event_격노", [25181, 28815, 30911, 302016, 30085], 1, "50794a6.jpg?=20190923"],
["⑪BAKBAK", 68, 10, 88, "che_event_귀병", [14349, 18743, 13345, 217520, 25666], 0, "default.jpg"],
["⑳카이스트", 71, 10, 87, "che_event_저격", [31142, 60635, 21934, 454495, 28863], 1, "9361ef8.jpg?=20180907"],
["③조밧", 69, 87, 10, "che_event_무쌍", [13843, 48487, 362135, 100164, 19893], 0, "default.jpg"],
["⑤두나", 65, 9, 75, "che_event_필살", [23003, 30467, 47824, 373566, 15048], 1, "bf0c572.png?=20181114"],
["②참치김치볶음밥", 71, 10, 88, "che_event_격노", [21811, 88823, 45531, 257006, 18713], 1, "6372946.gif?=20180721"],
["⑩내사랑미아누나", 80, 68, 10, "che_event_저격", [23694, 0, 0, 1507, 250180], 1, "90cb92c.jpg?=20190509"],
["⑮유지장", 84, 10, 74, "che_event_필살", [14624, 9108, 20946, 310070, 11803], 0, "default.jpg"],
["⑩과속방지턱", 92, 56, 9, "che_event_저격", [0, 0, 0, 0, 1390908], 1, "d092f20.jpg?=20190511"],
["④장수는랜덤", 80, 72, 10, "che_event_징병", [35957, 64864, 9254, 36779, 1713], 0, "default.jpg"],
["⑥광삼이", 73, 11, 80, "che_event_필살", [3867, 18504, 3970, 105291, 2892], 1, "69fc07c.jpg?=20180926"],
["⑫아이린", 70, 88, 11, "che_event_견고", [38283, 282325, 11817, 92874, 4907], 1, "8dac73c.jpg?=20180922"],
["⑧단비", 71, 10, 91, "che_event_신중", [38793, 35317, 109465, 494629, 13140], 1, "851c59d.jpg?=20190314"],
["⑦강서유서", 69, 10, 68, "che_event_척사", [9400, 8643, 23642, 74193, 2291], 1, "66b9506.png?=20180830"],
["⑤국밥공주", 73, 65, 9, "che_event_돌격", [407270, 65686, 113947, 98671, 20582], 1, "e4be574.gif?=20181030"],
["⑪김바보", 68, 10, 84, "che_event_반계", [17268, 6821, 11879, 134061, 16116], 1, "3a195b8.jpg?=20190619"],
["⑤기업형페이트", 72, 10, 85, "che_event_신중", [42696, 19270, 97397, 508628, 15738], 1, "812ee4a.jpg?=20181024"],
["⑪만샘", 69, 10, 85, "che_event_필살", [8908, 8446, 6166, 124244, 10448], 1, "4a206a1.jpg?=20181122"],
["⑨아무것도안함", 85, 74, 10, "che_event_돌격", [42228, 41862, 374326, 102631, 26438], 0, "default.jpg"],
["⑩蟲茶", 89, 38, 38, "che_event_저격", [0, 0, 0, 0, 789148], 0, "default.jpg"],
["⑨포모나", 85, 72, 10, "che_event_위압", [12399, 31416, 244093, 72763, 8748], 1, "dd71cf2.png?=20190419"],
["⑤마작왕", 63, 9, 76, "che_event_저격", [27123, 17881, 55490, 181274, 6913], 1, "e43844d.gif?=20181113"],
["⑧노진구", 87, 74, 10, "che_event_위압", [49603, 99857, 550127, 167534, 54584], 1, "ce1319d.jpg?=20190317"],
["⑨미스티", 70, 10, 87, "che_event_귀병", [17866, 21578, 61771, 244374, 8006], 1, "1aadcba.png?=20180908"],
["②광삼이", 78, 10, 79, "che_event_척사", [75688, 132949, 110908, 326694, 8949], 1, "bcdfa8.jpg?=20180630"],
["④아이린", 72, 84, 10, "che_event_위압", [8217, 35068, 257484, 49860, 26662], 1, "8dac73c.jpg?=20180922"],
["⑰병리학적자세", 69, 10, 83, "che_event_반계", [34669, 26393, 44197, 444149, 38031], 1, "3679089.jpg?=20180629"],
["⑬비스마르크", 68, 85, 11, "che_event_궁병", [22648, 309742, 10454, 46404, 14676], 0, "default.jpg"],
["⑮정채연", 70, 84, 10, "che_event_징병", [13973, 19417, 182561, 89692, 2873], 1, "9705097.jpg?=20191001"],
["⑧분위기갑자기소전", 75, 87, 10, "che_event_필살", [379006, 37780, 88218, 138232, 11145], 1, "fb2dc0b.gif?=20190403"],
["④머핀", 70, 10, 84, "che_event_척사", [39961, 25048, 35221, 413026, 19237], 1, "b2fcd42.jpg?=20180924"],
["⑳KF94", 73, 84, 10, "che_event_저격", [59787, 635335, 34870, 117677, 38682], 1, "577e1ee.png?=20200305"],
["④민트 초코 칩", 71, 11, 85, "che_event_반계", [42486, 70935, 64600, 411450, 15692], 1, "50a2a41.jpg?=20180929"],
["③블랙말랑카우", 74, 83, 10, "che_event_위압", [378803, 19414, 39915, 108153, 21918], 1, "4c98466.jpg?=20180829"],
["⑦위즐튼 공작", 70, 9, 77, "che_event_환술", [74751, 84870, 102534, 732669, 16567], 1, "d2dac4.jpg?=20190126"],
["④666", 83, 10, 72, "che_event_신중", [31198, 29394, 52062, 426875, 32534], 1, "382883b.jpg?=20181004"],
["㉒펭수병", 71, 83, 10, "che_event_의술", [63310, 83480, 367912, 71061, 24302], 1, "c364f77.png?=20200423"],
["⑰정채연", 69, 10, 82, "che_event_집중", [6033, 5938, 4945, 99986, 2771], 1, "9705097.jpg?=20191001"],
["④개노잼놀아라", 80, 9, 78, "che_event_격노", [40121, 50294, 32375, 226732, 29318], 0, "default.jpg"],
["⑦마검", 69, 10, 81, "che_event_필살", [17350, 14250, 17497, 210401, 5460], 0, "default.jpg"],
["⑤엉엉", 65, 75, 9, "che_event_척사", [420738, 25425, 55150, 134625, 17382], 1, "e6d276a.jpg?=20181115"],
["②월척", 74, 10, 86, "che_event_필살", [73531, 90553, 105078, 314674, 6039], 0, "default.jpg"],
["⑤메브", 82, 9, 9, "che_event_위압", [2431, 3211, 7090, 7353, 168984], 1, "eece100.jpg?=20181117"],
["⑩킹구갓구", 83, 67, 11, "che_event_저격", [0, 0, 0, 0, 239757], 1, "fb3c625.gif?=20190428"],
["⑨미친과학", 70, 84, 10, "che_event_돌격", [285082, 12813, 54280, 78370, 15688], 0, "default.jpg"],
["⑪카이스트", 71, 10, 87, "che_event_신중", [45950, 29151, 48395, 462648, 17053], 1, "9361ef8.jpg?=20180907"],
["⑧하우젤", 87, 73, 10, "che_event_돌격", [9862, 59705, 252234, 84529, 14100], 1, "dcff9fd.jpg?=20180823"],
["③sifmd", 69, 10, 79, "che_event_신중", [53200, 6685, 49759, 111012, 2297], 0, "default.jpg"],
["㉕갈근", 69, 76, 10, "che_event_격노", [82489, 768170, 41695, 154426, 14217], 0, "default.jpg"],
["⑦양복을입은아저씨", 83, 63, 9, "che_event_돌격", [28774, 80517, 729490, 262923, 31777], 1, "4da7fe2.jpg?=20190225"],
["⑧등갑병", 89, 67, 10, "che_event_무쌍", [156041, 10152, 30691, 6481, 8628], 1, "97aba2f.gif?=20180628"],
["⑬랜임맨", 74, 10, 82, "che_event_격노", [16402, 19235, 33672, 242417, 23437], 1, "1fc6462.jpg?=20190816"],
["⑬메디브", 69, 10, 86, "che_event_반계", [12775, 16148, 24006, 220874, 15032], 1, "def2895.jpg?=20190719"],
["⑮땅땅이", 68, 10, 80, "che_event_신중", [13491, 1728, 30861, 101347, 5165], 1, "99cd27f.gif?=20190606"],
["⑮임사영", 67, 10, 81, "che_event_징병", [4514, 4290, 2148, 87652, 2744], 1, "d5dc381.gif?=20190719"],
["②주사위논리학", 91, 69, 11, "che_event_돌격", [17548, 189620, 32739, 48952, 13972], 0, "default.jpg"],
["⑨그루트", 72, 10, 86, "che_event_척사", [61332, 47538, 56486, 524399, 40000], 1, "10265d3.png?=20190422"],
["⑬연초봄", 69, 83, 10, "che_event_돌격", [21589, 20116, 308862, 61105, 23434], 1, "2bfad16.jpg?=20190815"],
["㉔Already", 69, 10, 84, "che_event_반계", [12230, 3497, 0, 101675, 18386], 0, "default.jpg"],
["⑤くま", 63, 9, 76, "che_event_척사", [36871, 14634, 77483, 336560, 26839], 1, "73d75c2.jpg?=20180923"],
["⑩연두는말안드뤄", 82, 60, 9, "che_event_저격", [0, 48125, 0, 0, 859020], 1, "892fca8.jpg?=20190509"],
["①월척", 70, 10, 85, "che_event_저격", [0, 9940, 11445, 102947, 13740], 0, "default.jpg"],
["②리안", 73, 11, 87, "che_event_필살", [47566, 123130, 72468, 344925, 50693], 0, "default.jpg"],
["㉖이초홍", 74, 16, 91, "che_event_반계", [7995, 32637, 25701, 268670, 73323], 1, "ac758a0.jpg?=20200716"],
["⑮휴식이드", 71, 10, 88, "che_event_필살", [19986, 33521, 41345, 246607, 8612], 1, "1503444.jpg?=20190926"],
["⑦안나", 76, 9, 73, "che_event_신산", [84551, 79684, 137728, 674656, 22848], 1, "1593d39.gif?=20190129"],
["①whan", 85, 69, 10, "che_event_필살", [20271, 311096, 38315, 50035, 20590], 0, "default.jpg"],
["④평민킬러", 72, 83, 11, "che_event_척사", [550555, 17547, 55128, 133675, 29418], 1, "6add0f.jpg?=20180515"],
["⑤민트 초코 칩", 66, 9, 72, "che_event_저격", [43273, 10727, 56284, 260973, 274258], 1, "50a2a41.jpg?=20180929"],
["⑳두꺼비", 87, 69, 10, "che_event_견고", [467683, 20432, 46133, 122117, 27681], 1, "cc0eb9d.jpg?=20200305"],
["㉓랜갈덤근", 75, 10, 90, "che_event_집중", [49776, 47364, 62258, 1567830, 62682], 0, "default.jpg"],
["⑫뽀짝이", 71, 10, 87, "che_event_격노", [20676, 17333, 2013, 221014, 23172], 1, "99cd27f.gif?=20190606"],
["②아사이 나나미", 75, 10, 85, "che_event_집중", [26285, 139731, 125010, 702859, 34369], 1, "364468c.jpg?=20180813"],
["㉒보스곰", 69, 10, 83, "che_event_신산", [38667, 17481, 18532, 212359, 8174], 1, "773556e.gif?=20190822"],
["⑥쿠요", 68, 87, 10, "che_event_의술", [0, 19555, 117974, 23292, 16011], 1, "13a2ed0.png?=20180920"],
["⑨무팡", 72, 10, 85, "che_event_반계", [51618, 45671, 19445, 453861, 7872], 1, "181fefa.jpg?=20190414"],
["⑥소열제유비", 71, 84, 11, "che_event_기병", [20238, 81932, 337663, 51541, 24300], 1, "a3b1bbb.png?=20180927"],
["⑩Defender", 86, 57, 9, "che_event_저격", [0, 0, 389, 0, 1035933], 1, "622da6b.jpg?=20190511"],
["⑯Samo", 70, 77, 10, "che_event_저격", [0, 62448, 992, 7962, 33864], 1, "25449b0.png?=20190626"],
["⑥Lupia", 73, 10, 82, "che_event_신산", [48730, 114337, 97992, 654997, 27183], 0, "default.jpg"],
["⑭김나영", 70, 85, 10, "che_event_필살", [15203, 18707, 202373, 61922, 9885], 1, "c90a3d4.jpg?=20190905"],
["⑬로리", 81, 11, 71, "che_event_필살", [20555, 11529, 13800, 237232, 20171], 1, "1ec690f.jpg?=20190720"],
["⑧n아텐 누", 68, 11, 89, "che_event_집중", [6671, 27291, 23215, 156747, 8632], 1, "862bd8e.gif?=20181028"],
["⑩S9100", 81, 69, 11, "che_event_저격", [0, 0, 0, 0, 187535], 0, "default.jpg"],
["③병리학적자세", 71, 9, 86, "che_event_집중", [79770, 44334, 72236, 375876, 24444], 1, "3679089.jpg?=20180629"],
["⑫김나영", 70, 89, 10, "che_event_징병", [455057, 15416, 39763, 39666, 45299], 1, "aa2d228.jpg?=20190718"],
["㉑슬라임", 70, 10, 83, "che_event_신중", [46096, 20731, 31052, 284182, 10074], 1, "c40f9f7.jpg?=20200222"],
["㉙시딱마! 옥끼토끼!", 75, 15, 87, "che_event_징병", [18865, 19779, 8284, 120765, 17694], 1, "c4e8391.gif?=20201111"],
["⑩이쓰미", 68, 9, 73, "che_event_저격", [0, 0, 0, 0, 618683], 1, "fd6a0fd.jpg?=20181212"],
["㉗구독독구해주세요", 75, 15, 90, "che_event_신중", [5145, 8560, 35205, 203873, 35600], 0, "default.jpg"],
["⑥n아텐 누", 71, 10, 86, "che_event_저격", [54786, 22938, 44900, 329477, 9925], 1, "862bd8e.gif?=20181028"],
["㉖.", 88, 72, 17, "che_event_무쌍", [9135, 5495, 96509, 36569, 43690], 0, "default.jpg"],
["④가로쉬 헬스크림", 71, 85, 10, "che_event_척사", [222758, 34960, 65948, 47798, 25852], 1, "d8828c4.jpg?=20181004"],
["⑭ㅇㄷ", 69, 86, 10, "che_event_필살", [24924, 29465, 467317, 91384, 34258], 0, "default.jpg"],
["㉗.", 89, 76, 16, "che_event_척사", [381657, 27108, 30955, 88955, 28368], 0, "default.jpg"],
["⑥두땅크", 86, 70, 10, "che_event_견고", [356622, 26933, 63360, 74019, 10610], 0, "default.jpg"],
["⑤카이스트", 63, 9, 76, "che_event_신중", [55518, 15320, 85546, 468666, 30792], 1, "9361ef8.jpg?=20180907"],
["③요리타 요시노", 71, 11, 81, "che_event_반계", [6318, 13459, 23813, 240649, 25776], 1, "e15d37e.png?=20180828"],
["⑧갈색마(+5)", 76, 81, 10, "che_event_무쌍", [17917, 41769, 350794, 108527, 21423], 1, "88598ee.jpg?=20190321"],
["⑭Myo", 70, 82, 10, "che_event_의술", [8903, 132955, 0, 22766, 18222], 1, "7072487.jpg?=20190905"],
["㉕임사영", 65, 10, 79, "che_event_신중", [40061, 45278, 27016, 456215, 19157], 1, "7f9473e.gif?=20200211"],
["②연휘령", 76, 10, 85, "che_event_징병", [10659, 121889, 133504, 818722, 69286], 0, "default.jpg"],
["⑧hoopeh T", 70, 10, 88, "che_event_척사", [52458, 53500, 69228, 322424, 7765], 0, "default.jpg"],
["⑭미스티", 69, 10, 86, "che_event_신산", [16443, 19713, 27689, 225456, 2076], 1, "1aadcba.png?=20180908"],
["⑪이리시스", 72, 10, 86, "che_event_필살", [53487, 59065, 13263, 554253, 26487], 0, "default.jpg"],
["②이드", 88, 73, 10, "che_event_무쌍", [24372, 145423, 345963, 83233, 32665], 1, "e3835ee.jpg?=20180720"],
["㉔코리안코커", 81, 71, 10, "che_event_척사", [9640, 36774, 137952, 59674, 11942], 1, "f44d79c.png?=20200503"],
["⑨나랑", 90, 41, 37, "che_event_저격", [24055, 5348, 4407, 9933, 464917], 1, "8ee3feb.png?=20190501"],
["⑭엔야스", 58, 23, 86, "che_event_집중", [34502, 38573, 20294, 274472, 7465], 0, "default.jpg"],
["①늘모", 82, 72, 10, "che_event_무쌍", [0, 82347, 145492, 28974, 45695], 0, "default.jpg"],
["⑬배박수양아지", 78, 74, 10, "che_event_견고", [4984, 54145, 210891, 31778, 14156], 1, "d572e5f.jpg?=20190815"],
["⑥규이르", 73, 83, 10, "che_event_위압", [66364, 266420, 221194, 123622, 19922], 1, "bc45418.jpg?=20181214"],
["⑥먀삼거리에서먀된먀", 69, 10, 86, "che_event_반계", [4419, 42658, 22179, 218223, 16095], 0, "default.jpg"],
["⑲말걸지마라", 85, 69, 10, "che_event_저격", [350581, 22056, 40768, 58089, 43213], 1, "e465f23.gif?=20200109"],
["㉖페이트", 75, 16, 87, "che_event_신중", [10917, 33823, 4849, 155197, 7651], 1, "65f1662.jpg?=20200721"],
["⑭닥터유", 76, 77, 10, "che_event_견고", [14182, 350375, 14952, 85605, 20607], 1, "6b371ec.jpg?=20190909"],
["⑫질투의꼬리", 69, 10, 83, "che_event_신중", [17509, 38845, 3523, 144261, 4892], 0, "default.jpg"],
["⑲병리학적자세", 68, 11, 84, "che_event_격노", [53626, 35180, 33899, 476134, 24310], 1, "3679089.jpg?=20180629"],
["⑩아유", 87, 56, 9, "che_event_저격", [0, 0, 0, 0, 1201097], 1, "ba186bf.gif?=20181220"],
["㉙이드", 74, 86, 15, "che_event_견고", [9274, 9709, 200170, 31005, 20057], 1, "641883f.jpg?=20200910"],
["①야포", 69, 11, 83, "che_event_반계", [4541, 25297, 18959, 190505, 33514], 0, "default.jpg"],
["⑥ARES군주", 69, 88, 10, "che_event_돌격", [14205, 38414, 182708, 36323, 24505], 1, "ca0b15e.gif?=20181003"],
["㉕병리학적자세", 63, 10, 79, "che_event_필살", [60017, 40019, 45362, 462049, 14087], 1, "3679089.jpg?=20180629"],
["①춤추는곰돌이강슬기", 71, 85, 10, "che_event_돌격", [14623, 44238, 364737, 71354, 52162], 1, "214e682.jpg?=20180709"],
["②바리", 95, 12, 11, "che_event_격노", [0, 0, 0, 11173, 158671], 1, "e4e2524.gif?=20180803"],
["⑪드림캐쳐", 73, 84, 10, "che_event_견고", [780295, 42451, 44529, 59837, 28091], 1, "6bca7d1.jpg?=20190620"],
["⑫모건", 93, 13, 10, "che_event_필살", [0, 245, 0, 0, 127902], 1, "b023aa0.jpg?=20190731"],
["⑩송강", 79, 36, 37, "che_event_저격", [0, 0, 323, 0, 291417], 1, "e7a5bde.jpg?=20190329"],
["⑮그저늅늅", 68, 10, 91, "che_event_집중", [28798, 17683, 39254, 258926, 11547], 1, "5cedbb3.jpg?=20190817"],
["①갓갓갓", 71, 11, 82, "che_event_신산", [17059, 77159, 7063, 220467, 33070], 0, "default.jpg"],
["⑤스타킹", 72, 67, 9, "che_event_돌격", [30286, 21658, 498047, 103640, 30440], 1, "a7d4ef2.png?=20180920"],
["④화계장터마스코트", 72, 10, 85, "che_event_신산", [59486, 61241, 19399, 507079, 36389], 1, "24cd9f7.gif?=20180927"],
["⑬악의", 71, 10, 82, "che_event_의술", [22978, 21176, 8118, 174097, 16142], 0, "default.jpg"],
["⑥평민킬러", 72, 84, 10, "che_event_척사", [479800, 21874, 115333, 90822, 28351], 1, "2816ad.jpg?=20181118"],
["④호랑나비", 72, 10, 84, "che_event_격노", [24118, 44226, 39462, 302726, 16729], 1, "6afbac.gif?=20180927"],
["㉘Dok2", 76, 15, 92, "che_event_신산", [15645, 18476, 29407, 458343, 22932], 1, "41f83a4.jpg?=20200910"],
["⑫만샘", 71, 10, 88, "che_event_신중", [54581, 47171, 18478, 493039, 12480], 1, "4a206a1.jpg?=20181122"],
["⑬묘", 68, 81, 11, "che_event_무쌍", [10448, 9412, 132952, 45874, 1649], 0, "default.jpg"],
["㉙기", 89, 72, 15, "che_event_필살", [11265, 12229, 159370, 39591, 20039], 0, "default.jpg"],
["⑨타마키", 87, 71, 11, "che_event_필살", [9600, 21828, 325212, 59128, 31101], 1, "91ddaa3.jpg?=20190411"],
["⑩장수는랜덤", 81, 10, 69, "che_event_저격", [0, 0, 0, 69, 161506], 0, "default.jpg"],
["⑯료우기시키", 82, 74, 11, "che_event_무쌍", [147053, 1652, 2956, 15505, 21748], 1, "559083f.jpg?=20190905"],
["⑨로키", 74, 87, 10, "che_event_견고", [36243, 67776, 652322, 135848, 24967], 1, "a90d1e5.jpg?=20190411"],
["⑥카오스피닉스", 71, 10, 83, "che_event_의술", [9628, 18400, 9802, 166715, 25921], 1, "2af0641.jpg?=20180706"],
["⑧경국지색소교", 66, 15, 89, "che_event_필살", [59600, 21149, 51649, 315626, 16613], 0, "default.jpg"],
["㉖카린", 87, 77, 16, "che_event_위압", [26763, 505723, 16575, 15005, 19076], 1, "fb3c625.gif?=20190428"],
["㉒킹구", 79, 68, 12, "che_event_징병", [3313, 7816, 95991, 26196, 13158], 1, "fb3c625.gif?=20190428"],
["⑳료우기시키", 84, 71, 10, "che_event_무쌍", [61730, 186941, 2328, 51745, 24916], 1, "559083f.jpg?=20190905"],
["⑫비스마르크", 68, 83, 10, "che_event_격노", [7859, 30790, 165023, 66664, 9657], 0, "default.jpg"],
["㉒돌격", 85, 67, 11, "che_event_견고", [30917, 43421, 478831, 118418, 31757], 0, "default.jpg"],
["①くま", 69, 10, 83, "che_event_격노", [1553, 27232, 36544, 166497, 18822], 0, "default.jpg"],
["⑯낙지꾸미", 70, 10, 83, "che_event_필살", [0, 6343, 0, 108740, 24196], 0, "default.jpg"],
["③ARES군주", 77, 78, 11, "che_event_징병", [66167, 36648, 596058, 114620, 32974], 1, "68432f1.jpg?=20180811"],
["⑤리플", 65, 74, 9, "che_event_견고", [389791, 12682, 131145, 125222, 19270], 1, "cb8bb48.jpg?=20181026"],
["⑬달이차오른다", 81, 70, 12, "che_event_의술", [11268, 167974, 11018, 24398, 15801], 0, "default.jpg"],
["⑨착한아이", 70, 85, 10, "che_event_필살", [239192, 30787, 48133, 40099, 16394], 1, "f2d4b4.jpg?=20180629"],
["⑦줄리엣", 64, 9, 85, "che_event_격노", [41429, 23665, 44175, 431802, 16734], 1, "175639b.png?=20181025"],
["㉗좌우쌍욍검유비", 88, 76, 15, "che_event_공성", [1930, 49445, 350730, 51476, 127117], 1, "b9cb94e.jpg?=20200805"],
["⑦뉴비는아님암튼아님", 66, 81, 10, "che_event_징병", [45342, 88158, 461180, 141536, 15817], 1, "3168f43.jpg?=20190304"],
["㉙도우너", 75, 85, 15, "che_event_돌격", [126988, 10011, 1820, 27450, 13133], 1, "55b37a2.jpg?=20201111"],
["⑭국영꾼", 66, 79, 11, "che_event_기병", [35630, 11809, 25456, 32327, 472], 0, "default.jpg"],
["㉕학계의 정설", 69, 77, 10, "che_event_공성", [0, 6129, 18229, 14241, 216508], 0, "default.jpg"],
["①사이키쿠스오", 69, 85, 10, "che_event_격노", [30845, 277427, 23274, 46460, 20044], 1, "76bcc11.jpg?=20180628"],
["⑬엔야스", 71, 80, 10, "che_event_필살", [25798, 88115, 4837, 15831, 13802], 0, "default.jpg"],
["②띵호잉루", 70, 84, 10, "che_event_견고", [30958, 318139, 28499, 73436, 14548], 1, "4f071f2.jpg?=20180719"],
["③민트초코칩", 71, 11, 84, "che_event_척사", [40969, 61338, 60004, 518999, 46093], 1, "f53316b.jpg?=20180903"],
["⑧에바", 73, 10, 83, "che_event_척사", [8489, 11233, 19493, 191543, 13372], 0, "default.jpg"],
["⑤긴토키", 63, 73, 10, "che_event_돌격", [10654, 40895, 325308, 98362, 17484], 1, "1336ea.jpg?=20181025"],
["⑭톡없음", 72, 11, 82, "che_event_집중", [43225, 22229, 47813, 368082, 17573], 0, "default.jpg"],
["⑥모로코", 85, 69, 10, "che_event_돌격", [17144, 66049, 258186, 55494, 13071], 1, "75ca81d.jpg?=20181130"],
["④사스케", 83, 72, 10, "che_event_위압", [182798, 8687, 43431, 73283, 6055], 1, "4c58102.jpg?=20180927"],
["⑤집요정", 62, 9, 78, "che_event_필살", [37830, 19696, 54110, 312879, 14940], 1, "2e01216.jpg?=20181117"],
["⑧무한파워안경", 87, 73, 10, "che_event_저격", [7612, 38039, 319500, 125980, 16469], 1, "b7e1a1f.jpg?=20180525"],
["⑧만샘", 74, 10, 88, "che_event_격노", [54133, 62113, 69298, 461122, 46887], 1, "4a206a1.jpg?=20181122"],
["④요정", 70, 85, 11, "che_event_격노", [25374, 44461, 480161, 117902, 26024], 1, "a2e5934.png?=20181005"],
["⑨치카", 71, 11, 86, "che_event_반계", [36669, 52152, 42682, 556462, 37493], 1, "3e3566c.png?=20190411"],
["⑬제노에이지", 67, 10, 87, "che_event_신산", [6120, 22097, 23220, 142690, 13093], 1, "ddd1fd7.jpg?=20190320"],
["⑭모망삼다했", 69, 10, 82, "che_event_신산", [7268, 30557, 5025, 163972, 0], 0, "default.jpg"],
["⑥미스티", 81, 10, 69, "che_event_의술", [39688, 35243, 44939, 252746, 4939], 1, "1aadcba.png?=20180908"],
["⑥외심장", 72, 10, 83, "che_event_저격", [30132, 36066, 33808, 220057, 1945], 1, "36110cd.jpg?=20180826"],
["㉓레다", 85, 10, 77, "che_event_의술", [93401, 45647, 31469, 631498, 50236], 1, "cf762d0.jpg?=20200130"],
["㉖손견문대", 75, 15, 85, "che_event_신중", [12649, 4871, 7148, 128067, 19301], 0, "default.jpg"],
["㉕백구이야기", 86, 77, 10, "che_event_척사", [1020358, 47414, 149009, 179378, 14621], 1, "f82ea71.jpg?=20200611"],
["⑥집합장 1", 80, 75, 10, "che_event_견고", [41759, 659504, 102688, 101509, 56255], 1, "2c05c61.jpg?=20181218"],
["⑪나가사와 마리나", 81, 10, 76, "che_event_척사", [54962, 43922, 11725, 380426, 24276], 1, "ea91789.png?=20190620"],
["⑦Febrile", 71, 10, 81, "che_event_신산", [6562, 0, 6354, 435, 164621], 0, "default.jpg"],
["⑬킹구없이는못살아", 68, 82, 11, "che_event_격노", [15354, 165963, 15169, 43889, 15012], 1, "fb3c625.gif?=20190428"],
["⑬호무새의속마음", 82, 71, 10, "che_event_견고", [285203, 12734, 30117, 54477, 11184], 1, "3afd99b.png?=20190815"],
["⑪살인사건", 71, 85, 10, "che_event_징병", [4975, 27521, 145474, 46272, 12402], 0, "default.jpg"],
["⑨무사태평자", 70, 10, 88, "che_event_집중", [32253, 29065, 33306, 417577, 25348], 0, "default.jpg"],
["④카오스피닉스", 71, 10, 85, "che_event_척사", [33109, 36307, 62363, 297314, 13179], 1, "2af0641.jpg?=20180706"],
["⑯Navy마초", 68, 87, 10, "che_event_무쌍", [318, 153674, 0, 8703, 36201], 0, "default.jpg"],
["⑥고긔가먹고싶", 69, 10, 86, "che_event_의술", [11134, 17201, 6071, 133570, 0], 1, "ae8cbdd.gif?=20181114"],
["④숨쉴래", 70, 10, 86, "che_event_저격", [20132, 14705, 35671, 245664, 9817], 1, "bf4bcbf.jpg?=20180823"],
["⑤광삼이", 67, 9, 72, "che_event_집중", [27930, 11078, 64011, 273666, 10928], 1, "69fc07c.jpg?=20180926"],
["⑪?", 71, 11, 83, "che_event_필살", [33010, 27167, 47333, 365515, 16514], 1, "4384752.jpg?=20190620"],
["⑥화사", 69, 87, 10, "che_event_견고", [207353, 12833, 3396, 30873, 25654], 1, "6c3e3fb.jpg?=20181129"],
["④지용갓", 68, 10, 85, "che_event_집중", [17975, 11960, 38663, 134064, 1512], 1, "1f3a6e6.png?=20180926"],
["㉑くま", 85, 70, 10, "che_event_견고", [32539, 42900, 489119, 132317, 53684], 1, "770f53.jpg?=20190718"],
["㉕수장", 69, 80, 10, "che_event_돌격", [47345, 125147, 12986, 26316, 6589], 1, "190d7ff.jpg?=20200312"],
["⑥ⓟ럭키", 73, 10, 84, "che_event_저격", [41919, 56516, 88869, 585514, 37656], 1, "8e57858.gif?=20181217"],
["㉖일이석우", 78, 85, 15, "che_event_공성", [0, 10700, 0, 12824, 469750], 1, "9c2026b.jpg?=20200527"],
["⑰KOSPI", 71, 80, 10, "che_event_징병", [28079, 23655, 196404, 35678, 49318], 0, "default.jpg"],
["⑦Raccoon", 66, 9, 76, "che_event_척사", [56964, 39442, 69425, 485863, 15438], 0, "default.jpg"],
["⑥박보검", 69, 10, 85, "che_event_반계", [16116, 21102, 14876, 124846, 15207], 1, "649311b.jpg?=20181129"],
["⑤팬티", 61, 9, 78, "che_event_집중", [26713, 39279, 33881, 319243, 4400], 1, "7389d8c.png?=20181025"],
["⑰땅땅이", 81, 72, 10, "che_event_격노", [9308, 62309, 265274, 77751, 20948], 1, "99cd27f.gif?=20190606"],
["㉘10068", 74, 92, 15, "che_event_격노", [117983, 38024, 27309, 42003, 7518], 0, "default.jpg"],
["㉙야가다마스터", 77, 85, 15, "che_event_저격", [0, 22627, 155612, 31020, 23839], 0, "default.jpg"],
["㉗료우기시키", 88, 78, 15, "che_event_돌격", [27311, 232365, 152840, 73882, 35427], 1, "559083f.jpg?=20190905"],
["⑧덕장", 90, 69, 10, "che_event_위압", [50713, 371021, 76129, 119567, 23845], 0, "default.jpg"],
["⑳로리", 73, 10, 86, "che_event_집중", [10915, 6830, 14574, 846843, 40297], 1, "50794a6.jpg?=20190923"],
["⑦류조", 66, 9, 72, "che_event_저격", [76911, 60995, 53687, 342677, 8025], 1, "7351863.gif?=20190221"],
["⑦만샘", 67, 9, 82, "che_event_신산", [38852, 103656, 96057, 643898, 7324], 1, "4a206a1.jpg?=20181122"],
["⑥스파르타쿠스", 76, 78, 10, "che_event_격노", [38894, 75704, 395324, 156155, 11812], 1, "e491277.jpg?=20181129"],
["㉙임사영", 76, 15, 83, "che_event_척사", [6867, 1520, 16793, 123043, 8178], 1, "7f9473e.gif?=20200211"],
["②늘모", 78, 10, 83, "che_event_징병", [65631, 160477, 115617, 536931, 19508], 1, "e7c163.png?=20180801"],
["⑤로얄밀크티", 78, 62, 9, "che_event_저격", [52935, 559654, 62533, 94207, 47166], 1, "5b28b38.png?=20181115"],
["⑯평민킬러", 69, 86, 10, "che_event_무쌍", [210597, 4618, 15688, 13134, 28916], 1, "fb23a32.jpg?=20190904"],
["⑦두나", 65, 9, 82, "che_event_저격", [14963, 2731, 19710, 304635, 9567], 1, "bf0c572.png?=20181114"],
["⑦제노에이지", 63, 9, 79, "che_event_반계", [20368, 17249, 50083, 245920, 7312], 0, "default.jpg"],
["㉘네시", 75, 15, 92, "che_event_신중", [32073, 24821, 24183, 347579, 33880], 0, "default.jpg"],
["⑳보이루", 72, 10, 86, "che_event_환술", [34577, 49924, 38667, 673301, 52180], 1, "a9b0507.jpg?=20200217"],
["㉙일이석우", 72, 15, 89, "che_event_저격", [4208, 2648, 3852, 123975, 26703], 1, "9c2026b.jpg?=20200527"],
["㉑아라한", 80, 73, 11, "che_event_필살", [20260, 32461, 231775, 27320, 17654], 1, "a66f117.jpg?=20200402"],
["㉖카라라트리", 75, 90, 15, "che_event_무쌍", [13974, 26882, 355365, 39113, 27730], 1, "848a454.jpg?=20200717"],
["⑥태극권", 78, 70, 10, "che_event_돌격", [17408, 59492, 42268, 17857, 0], 1, "e7183a7.jpg?=20181101"],
["⑤소열제유비", 67, 73, 9, "che_event_척사", [25089, 42398, 408605, 108503, 15546], 1, "a3b1bbb.png?=20180927"],
["⑨호크아이", 72, 87, 10, "che_event_격노", [116602, 426109, 28348, 89696, 16420], 1, "bd83469.jpg?=20190412"],
["⑭째깐둥이", 81, 10, 72, "che_event_징병", [18501, 11115, 4928, 191235, 10681], 1, "f8d36e2.png?=20190912"],
["②진리", 88, 72, 10, "che_event_필살", [61758, 536541, 75409, 178932, 13414], 1, "36d49fa.jpg?=20180725"],
["⑦서울대의대", 67, 9, 76, "che_event_신중", [49168, 54947, 55861, 547782, 19853], 0, "default.jpg"],
["⑦아이린", 67, 81, 9, "che_event_돌격", [87746, 628250, 25622, 184653, 36668], 1, "8dac73c.jpg?=20180922"],
["⑤FFFFFF", 79, 62, 9, "che_event_돌격", [335531, 50054, 111408, 128229, 17623], 1, "e0b1d99.jpg?=20181028"],
["③덕장", 70, 84, 11, "che_event_저격", [358619, 29859, 50037, 78140, 19263], 1, "c281d38.jpg?=20180906"],
["③민트토끼", 73, 10, 84, "che_event_신중", [76160, 38268, 119597, 607557, 27717], 1, "976f3f5.gif?=20180809"],
["④료우기시키", 71, 84, 10, "che_event_징병", [235381, 25279, 49702, 62311, 11071], 1, "21b50f8.jpg?=20180628"],
["②DZTO", 72, 10, 85, "che_event_격노", [29064, 65309, 78396, 143127, 9180], 1, "753cf5d.jpg?=20180723"],
["⑫리즈나광팬672번", 77, 82, 10, "che_event_저격", [63223, 52779, 284357, 123335, 13228], 1, "3f21be0.jpg?=20190719"],
["⑪헹이", 71, 85, 10, "che_event_징병", [54900, 321923, 4800, 95585, 21977], 0, "default.jpg"],
["⑩로비", 81, 33, 33, "che_event_저격", [0, 0, 0, 0, 434871], 1, "59b6a1d.jpg?=20180802"],
["㉙개미호랑이", 72, 16, 88, "che_event_반계", [9264, 9816, 6247, 102975, 6095], 1, "9e0429e.jpg?=20200313"],
["④탄산", 71, 10, 83, "che_event_필살", [41397, 25110, 45954, 321664, 3344], 1, "b6c1067.png?=20180925"],
["③크르르", 71, 10, 84, "che_event_반계", [42714, 31336, 86465, 346386, 15710], 0, "default.jpg"],
["⑧소열제유비", 72, 88, 10, "che_event_기병", [2739, 25885, 271234, 121472, 11110], 1, "a3b1bbb.png?=20180927"],
["⑪의욕없는규일", 73, 85, 10, "che_event_무쌍", [86480, 635227, 39771, 86433, 16507], 0, "default.jpg"],
["⑬보스곰", 68, 11, 86, "che_event_집중", [14708, 12208, 20068, 225041, 15913], 1, "773556e.gif?=20190822"],
["⑨노가가", 90, 68, 10, "che_event_돌격", [442090, 41636, 59251, 85789, 26487], 1, "b8b96da.png?=20190421"],
["㉓임사영", 75, 10, 87, "che_event_환술", [32395, 28399, 35947, 372813, 16176], 1, "7f9473e.gif?=20200211"],
["①광삼이", 74, 10, 79, "che_event_필살", [19805, 70942, 31060, 330337, 23527], 1, "bcdfa8.jpg?=20180630"],
["⑫리안", 70, 10, 90, "che_event_척사", [9005, 3957, 6810, 120993, 24207], 1, "db0cb7a.jpg?=20190719"],
["⑤n아텐 누", 63, 9, 78, "che_event_신중", [27648, 40231, 66665, 446428, 21807], 1, "862bd8e.gif?=20181028"],
["⑮묘", 69, 81, 11, "che_event_징병", [15197, 106299, 1381, 18759, 17719], 1, "7072487.jpg?=20190905"],
["⑪삐약이", 74, 83, 10, "che_event_격노", [14542, 317000, 7269, 51955, 26625], 1, "10f5cdc.jpg?=20190708"],
["㉕미르칼라", 68, 10, 79, "che_event_환술", [46006, 28020, 67235, 987828, 9470], 1, "959346e.jpg?=20200604"],
["⑨평민킬러", 69, 89, 10, "che_event_위압", [268642, 12321, 25583, 33873, 20140], 1, "2816ad.jpg?=20181118"],
["⑧ㅇㄷ", 68, 10, 86, "che_event_반계", [5602, 26719, 24419, 212082, 21756], 0, "default.jpg"],
["㉘으헹", 79, 15, 89, "che_event_귀병", [21536, 27306, 5198, 230579, 13902], 1, "a1ad420.jpg?=20200425"],
["⑩rwitch", 82, 9, 57, "che_event_저격", [0, 4341, 0, 0, 672844], 1, "4d82adb.jpg?=20190325"],
["⑱박일아", 75, 87, 10, "che_event_무쌍", [28858, 116303, 1138468, 138739, 50122], 1, "8608979.gif?=20191024"],
["⑨문학소녀", 70, 10, 86, "che_event_필살", [61277, 26560, 44044, 354133, 19828], 1, "bf40507.jpg?=20190411"],
["㉓Hide_D", 84, 11, 79, "che_event_환술", [105917, 55852, 68709, 1133732, 69051], 1, "9f0a2a8.png?=20200106"],
["①리즈나", 71, 85, 10, "che_event_무쌍", [154806, 160366, 170422, 34193, 57545], 1, "6ef3386.jpg?=20180630"],
["⑮Navy마초", 80, 73, 10, "che_event_무쌍", [59999, 322796, 25521, 85651, 14200], 0, "default.jpg"],
["⑯아무말이나들어줌", 76, 79, 12, "che_event_기병", [242, 10369, 105455, 6820, 16329], 1, "df7a778.jpg?=20191024"],
["㉓박일아", 76, 11, 88, "che_event_환술", [72117, 80050, 99539, 1576763, 70108], 1, "8608979.gif?=20191024"],
["④모니카열전좀", 67, 84, 10, "che_event_척사", [72560, 52721, 86782, 42917, 8302], 1, "f2d4b4.jpg?=20180629"],
["⑩아이린", 68, 72, 9, "che_event_저격", [0, 0, 139266, 0, 298447], 1, "8dac73c.jpg?=20180922"],
["⑫아침에먹는아욱국", 87, 10, 72, "che_event_반계", [50904, 28941, 28962, 391378, 17309], 0, "default.jpg"],
["④막둥이", 71, 10, 85, "che_event_저격", [29509, 24998, 25017, 226267, 21364], 0, "default.jpg"],
["⑪소열제유비", 71, 85, 10, "che_event_무쌍", [9604, 20194, 148418, 38737, 13348], 1, "a3b1bbb.png?=20180927"],
["⑤상냥한요승상", 69, 9, 71, "che_event_신중", [53942, 34447, 52894, 481462, 18575], 1, "76bf7f6.jpg?=20181007"],
["㉑광순이", 69, 10, 84, "che_event_환술", [16081, 27260, 31971, 304663, 42187], 1, "ec3db27.gif?=20200402"],
["⑨스트레인지", 76, 84, 10, "che_event_위압", [577828, 74354, 106284, 101865, 20770], 1, "80af794.jpg?=20190411"],
["⑬반역의루돌프", 77, 76, 10, "che_event_필살", [126617, 11098, 22934, 45129, 6469], 0, "default.jpg"],
["⑫꼬리♡", 73, 10, 90, "che_event_척사", [24478, 27771, 20496, 454170, 34509], 1, "844a93b.gif?=20190717"],
["⑲현피0번남음", 66, 10, 82, "che_event_신산", [8797, 10337, 7072, 96532, 2641], 0, "default.jpg"],
["⑪정체모를트롤", 70, 10, 88, "che_event_신산", [17709, 16045, 4894, 328633, 7794], 0, "default.jpg"],
["②위대한로렌초", 75, 90, 10, "che_event_위압", [61360, 1367383, 110301, 196998, 40255], 1, "71e4063.jpg?=20180720"],
["⑰토미에", 69, 10, 82, "che_event_격노", [0, 15001, 28747, 179707, 5613], 1, "f4cbfb3.jpg?=20191112"],
["⑤5분장멍펫", 69, 72, 9, "che_event_징병", [96251, 287301, 113313, 111606, 12310], 1, "e3085b8.jpg?=20181025"],
["⑥이쓰미", 73, 11, 83, "che_event_격노", [57119, 45569, 69154, 503433, 30065], 1, "fd6a0fd.jpg?=20181212"],
["㉖천재", 76, 15, 91, "che_event_집중", [10256, 4662, 5250, 141527, 9519], 0, "default.jpg"],
["⑥김독자", 80, 70, 11, "che_event_돌격", [36729, 159102, 29345, 41846, 2295], 0, "default.jpg"],
["⑤임사영", 62, 9, 76, "che_event_필살", [23995, 25621, 50553, 238485, 19982], 1, "a0bc4b2.jpg?=20180628"],
["㉖슬라임", 86, 79, 15, "che_event_필살", [314323, 10320, 35875, 44562, 63783], 0, "default.jpg"],
["㉖김나영", 76, 90, 16, "che_event_돌격", [26107, 6147, 379712, 75224, 21867], 1, "c90a3d4.jpg?=20190905"],
["⑳어스퀘이크", 70, 11, 79, "che_event_반계", [14886, 23540, 9213, 212215, 41598], 0, "default.jpg"],
["㉕전투기계우르곳", 64, 10, 78, "che_event_의술", [42558, 36646, 38474, 336696, 13546], 1, "b5fd8fc.gif?=20200604"],
["⑧Aika", 85, 75, 11, "che_event_척사", [24592, 460397, 106214, 152832, 39933], 1, "dc67ee2.png?=20190316"],
["⑥강도", 72, 80, 10, "che_event_위압", [2612, 41326, 192017, 65111, 14392], 1, "384aa86.jpg?=20181204"],
["⑩따라큐", 84, 58, 9, "che_event_저격", [0, 0, 0, 0, 950865], 1, "d879a26.png?=20190427"],
["⑪심심3", 70, 10, 86, "che_event_저격", [8374, 11064, 17965, 76450, 5463], 0, "default.jpg"],
["⑨누렁", 89, 70, 10, "che_event_척사", [418684, 13913, 76175, 126112, 29697], 1, "4bbfa36.jpg?=20190411"],
["⑧이쓰미", 70, 10, 89, "che_event_격노", [4114, 0, 5450, 83715, 11188], 1, "fd6a0fd.jpg?=20181212"],
["②북오더", 70, 10, 78, "che_event_견고", [27543, 4207, 13568, 43439, 33269], 1, "4e393e3.gif?=20180804"],
["㉗RGB", 76, 15, 88, "che_event_신중", [48528, 21254, 41438, 279400, 11089], 1, "91651d1.webp?=20200813"],
["④whan", 75, 83, 10, "che_event_무쌍", [42469, 571227, 60657, 206894, 20573], 1, "68aacd2.jpg?=20180929"],
["㉕낙지꿈", 63, 10, 80, "che_event_집중", [69901, 30004, 15248, 428603, 22347], 0, "default.jpg"],
["㉒개미호랑이즈리얼", 82, 70, 10, "che_event_척사", [20817, 32123, 213097, 37954, 338], 1, "9e0429e.jpg?=20200313"],
["⑫갸꿀띠", 71, 10, 81, "che_event_신중", [36546, 6850, 8508, 224169, 5418], 1, "5b0cf23.jpg?=20190625"],
["⑭미니미니쿠키", 81, 10, 74, "che_event_필살", [24115, 5118, 20145, 229329, 16423], 0, "default.jpg"],
["⑲Elsa", 71, 83, 10, "che_event_저격", [8241, 26929, 263995, 71163, 15037], 1, "5041f99.jpg?=20200121"],
["⑦리플", 69, 9, 78, "che_event_반계", [70054, 81157, 113080, 546483, 8095], 1, "fbe44ca.jpg?=20181207"],
["⑩반역의루돌프", 74, 63, 9, "che_event_저격", [0, 1493, 0, 0, 481951], 0, "default.jpg"],
["⑤죽창", 79, 69, 10, "che_event_척사", [16220, 58006, 78666, 37730, 9111], 1, "1978b2c.jpg?=20180924"],
["④땅크땅크", 88, 68, 11, "che_event_징병", [106071, 393969, 45358, 148749, 14287], 0, "default.jpg"],
["⑳Hide_D", 73, 10, 86, "che_event_집중", [18327, 18100, 21069, 746536, 27042], 1, "9f0a2a8.png?=20200106"],
["⑤제갈여포", 63, 9, 78, "che_event_반계", [58703, 44761, 29432, 322250, 12465], 1, "e8e8adc.jpg?=20180419"],
["①치카", 69, 11, 84, "che_event_징병", [10747, 65866, 9451, 259530, 34691], 1, "a5d05a5.jpg?=20180525"],
["③말랑말", 73, 82, 11, "che_event_돌격", [44396, 50436, 605940, 141042, 18937], 1, "6094d05.jpg?=20180823"],
["⑨화타", 71, 10, 81, "che_event_귀병", [1069, 35178, 17139, 155776, 869], 0, "default.jpg"],
["①DZTO", 85, 67, 10, "che_event_돌격", [0, 135326, 41808, 17914, 27379], 1, "9c05100.jpg?=20180706"],
["④또늦은달팽", 71, 10, 82, "che_event_저격", [55396, 32352, 40683, 324836, 6977], 1, "fb0d354.jpg?=20180629"],
["㉖료우기시키", 88, 78, 15, "che_event_저격", [15924, 26289, 316555, 71475, 21754], 1, "559083f.jpg?=20190905"],
["⑲현피1번남음", 69, 10, 79, "che_event_집중", [2759, 33410, 16327, 189518, 9871], 0, "default.jpg"],
["⑫캬루", 70, 11, 90, "che_event_징병", [23352, 12035, 12716, 241607, 22213], 1, "54e940f.png?=20190717"],
["⑧모니ㅇ카", 69, 10, 89, "che_event_저격", [3072, 22003, 26577, 165160, 10339], 1, "f937029.jpg?=20190321"],
["⑥제노에이지", 70, 10, 85, "che_event_징병", [13387, 48508, 47367, 223293, 4529], 1, "af4fec8.png?=20180818"],
["⑥충차", 85, 66, 11, "che_event_필살", [101228, 404556, 24920, 81754, 37546], 1, "392683c.jpg?=20181129"],
["①륜", 71, 10, 85, "che_event_징병", [44874, 18503, 19522, 190353, 10757], 1, "96061d5.jpg?=20180708"],
["⑭호렌버핏", 68, 10, 83, "che_event_집중", [25963, 20264, 19798, 203071, 15925], 1, "3649856.png?=20190815"],
["⑩외심장", 84, 55, 9, "che_event_저격", [0, 0, 0, 0, 232710], 1, "36110cd.jpg?=20180826"],
["④외심장", 69, 10, 86, "che_event_귀병", [31495, 34269, 25585, 198002, 15797], 1, "36110cd.jpg?=20180826"],
["㉑나타", 86, 68, 10, "che_event_필살", [0, 7735, 0, 8889, 395546], 1, "5930c00.jpg?=20200416"],
["⑦토야마 카스미", 66, 82, 9, "che_event_무쌍", [43415, 535427, 58781, 216485, 33927], 1, "966f765.gif?=20190208"],
["⑥수장", 78, 80, 10, "che_event_저격", [48228, 54821, 428238, 95633, 26597], 1, "d74c9d2.png?=20181115"],
["⑧평민킬러", 75, 85, 10, "che_event_징병", [109538, 591087, 72141, 140242, 29420], 1, "2816ad.jpg?=20181118"],
["⑦미나토 유키나", 71, 9, 81, "che_event_척사", [120039, 148134, 167708, 1150978, 24905], 1, "6050d87.jpg?=20190305"],
["⑪개미호랑이", 72, 10, 87, "che_event_신산", [46734, 88676, 54169, 676154, 20093], 1, "b0969be.jpg?=20190705"],
["⑥큭큭", 68, 11, 80, "che_event_척사", [0, 12439, 6722, 179478, 5991], 1, "525bf94.jpg?=20181129"],
["①료우기시키", 70, 83, 11, "che_event_의술", [334576, 19445, 27287, 45017, 33727], 1, "21b50f8.jpg?=20180628"],
["⑥팬티", 68, 10, 87, "che_event_집중", [10474, 31256, 40568, 300216, 31300], 1, "8496c4b.jpg?=20181217"],
["⑥할말이있어", 71, 10, 86, "che_event_필살", [22667, 56016, 34216, 302191, 11604], 0, "default.jpg"],
["④1288965", 71, 86, 10, "che_event_위압", [5324, 24407, 342321, 87536, 17810], 0, "default.jpg"],
["㉖smile", 87, 78, 17, "che_event_궁병", [9483, 179209, 1068, 30166, 0], 0, "default.jpg"],
["⑱Anna", 72, 11, 88, "che_event_신산", [37553, 45612, 50708, 689014, 45414], 1, "7ad20e3.jpg?=20191212"],
["⑭료우기시키", 69, 10, 84, "che_event_집중", [19398, 32547, 17222, 297079, 6760], 1, "559083f.jpg?=20190905"],
["⑪케프리", 91, 65, 10, "che_event_무쌍", [2041, 5571, 4024, 2044, 111189], 1, "8032c27.jpg?=20190707"],
["㉒뜨뜨뜨뜨", 80, 10, 74, "che_event_집중", [26040, 24307, 60622, 300433, 14495], 1, "a1ad420.jpg?=20200425"],
["⑥민트 초코 칩", 69, 10, 86, "che_event_척사", [33431, 23582, 32861, 300258, 27816], 1, "50a2a41.jpg?=20180929"],
["⑩비스마르크", 81, 9, 61, "che_event_저격", [0, 0, 0, 0, 1451378], 0, "default.jpg"],
["⑦김혜나", 67, 9, 79, "che_event_징병", [66140, 59095, 84458, 706601, 40086], 1, "68b1a10.jpg?=20190126"],
["①아사기리 겐", 69, 11, 86, "che_event_신중", [11990, 55291, 25184, 247169, 40450], 0, "default.jpg"],
["⑳외심장", 69, 10, 85, "che_event_척사", [44338, 40148, 20505, 387032, 14206], 1, "36110cd.jpg?=20180826"],
["⑰평민킬러", 68, 85, 10, "che_event_징병", [234601, 84, 46727, 48450, 25516], 1, "fb23a32.jpg?=20190904"],
["⑩로미오", 87, 69, 10, "che_event_저격", [0, 0, 0, 0, 473300], 1, "806de31.png?=20181025"],
["④사서고생함", 73, 10, 84, "che_event_척사", [33551, 33670, 24840, 209202, 10158], 0, "default.jpg"],
["㉒에포트", 67, 10, 83, "che_event_척사", [12586, 10006, 12316, 89094, 6212], 0, "default.jpg"],
["⑭카오스피닉스", 70, 11, 85, "che_event_척사", [30585, 25093, 31441, 275734, 21548], 1, "7f80b2f.jpg?=20190715"],
["⑥뉴턴", 78, 76, 10, "che_event_무쌍", [38128, 266202, 11882, 73466, 27854], 1, "5331d91.jpg?=20181210"],
["⑧드류", 71, 86, 10, "che_event_저격", [22697, 180107, 19626, 89342, 9390], 0, "default.jpg"],
["②숨질래", 69, 10, 86, "che_event_집중", [21005, 59642, 44662, 293602, 31661], 1, "ad6c46e.jpg?=20180719"],
["㉘일이석우", 76, 15, 92, "che_event_집중", [30266, 22715, 36369, 421288, 8146], 1, "9c2026b.jpg?=20200527"],
["㉒Hide_D", 71, 11, 84, "che_event_신산", [13563, 53427, 60592, 507150, 75293], 1, "9f0a2a8.png?=20200106"],
["㉑사스케", 85, 70, 10, "che_event_의술", [441684, 12578, 75704, 83916, 44226], 1, "61e6754.jpg?=20200402"],
["③늘모", 74, 80, 11, "che_event_위압", [655094, 53450, 116891, 169239, 23325], 1, "e7c163.png?=20180801"],
["⑮지금은전쟁중", 83, 73, 12, "che_event_필살", [10411, 41449, 302872, 62141, 32191], 0, "default.jpg"],
["②갓갓갓", 75, 84, 10, "che_event_돌격", [52703, 66088, 366729, 95014, 13828], 0, "default.jpg"],
["③히카와 사요", 70, 10, 80, "che_event_척사", [45128, 19589, 38320, 260724, 6226], 1, "a1f2916.gif?=20180831"],
["⑬n아텐 누", 83, 67, 11, "che_event_척사", [6634, 26831, 250642, 48396, 19348], 1, "862bd8e.gif?=20181028"],
["③멍미", 74, 10, 82, "che_event_척사", [78837, 35170, 110746, 597750, 36805], 0, "default.jpg"],
["⑧페레로로쉐", 79, 10, 81, "che_event_신산", [23472, 79263, 86712, 593316, 36511], 1, "e9b8542.jpg?=20190314"],
["⑥태수", 71, 10, 84, "che_event_척사", [13951, 14418, 40743, 246476, 35264], 1, "ee74461.gif?=20181214"],
["⑧킹유신", 81, 75, 12, "che_event_격노", [265391, 28657, 93749, 72500, 7091], 0, "default.jpg"],
["⑧옷파란색사고싶어", 71, 10, 87, "che_event_귀병", [63656, 18474, 64801, 246952, 16084], 0, "default.jpg"],
["⑨실전압축5분장", 82, 71, 10, "che_event_징병", [41477, 156362, 190265, 99143, 16794], 0, "default.jpg"],
["①이치코", 72, 80, 10, "che_event_저격", [205187, 17782, 46838, 53075, 15721], 1, "ef6aac8.jpg?=20180628"],
["⑲갈근", 71, 10, 82, "che_event_저격", [42372, 29647, 19974, 369504, 28762], 0, "default.jpg"],
["⑪초코우유", 69, 10, 86, "che_event_징병", [13430, 41887, 30225, 296060, 21612], 1, "db0f3b9.jpg?=20190702"],
["⑥귀여운세젤예키라님", 69, 85, 10, "che_event_척사", [20810, 134904, 8758, 23306, 15808], 1, "175639b.png?=20181025"],
["④킹조", 70, 10, 85, "che_event_필살", [18493, 21504, 33565, 163226, 8343], 0, "default.jpg"],
["③보랏빛여교사", 70, 10, 83, "che_event_격노", [48590, 36154, 56898, 301112, 18918], 1, "5d30092.png?=20180825"],
["㉖떼껄룩", 87, 78, 16, "che_event_징병", [3935, 173187, 1046, 18528, 50162], 0, "default.jpg"],
["⑩키라누나", 81, 67, 11, "che_event_저격", [0, 0, 0, 4185, 242971], 1, "deca144.png?=20190509"],
["⑩파이", 80, 61, 9, "che_event_저격", [0, 0, 0, 0, 651088], 1, "da3bc86.jpg?=20190318"],
["㉒행복", 69, 10, 83, "che_event_필살", [26695, 31584, 29926, 280659, 15752], 0, "default.jpg"],
["⑨리안", 86, 71, 10, "che_event_돌격", [25240, 66573, 570513, 136227, 15160], 0, "default.jpg"],
["⑲연애중앵벌스", 69, 80, 10, "che_event_징병", [155779, 21490, 27629, 39317, 0], 1, "4b82261.jpg?=20200114"],
["⑩만샘", 84, 66, 10, "che_event_저격", [0, 0, 0, 0, 178116], 1, "4a206a1.jpg?=20181122"],
["②료우기시키", 75, 85, 10, "che_event_필살", [100136, 559337, 70857, 148532, 22239], 1, "21b50f8.jpg?=20180628"],
["⑫엔틱", 68, 11, 88, "che_event_환술", [25840, 9592, 9228, 155721, 16123], 1, "781ef76.jpg?=20190317"],
["⑦박초롱", 66, 82, 9, "che_event_돌격", [563181, 18370, 42099, 254318, 22067], 1, "18e88f0.jpg?=20190223"],
["⑤이드", 65, 9, 74, "che_event_신산", [53996, 40052, 42005, 357858, 8117], 1, "ff87fce.jpg?=20181025"],
["⑦절대삐부해", 65, 9, 77, "che_event_신산", [31226, 21656, 59173, 435065, 8444], 1, "427de4f.jpg?=20181129"],
["④구다코", 72, 11, 83, "che_event_신산", [19646, 45434, 77733, 650079, 33890], 1, "1528b91.png?=20180920"],
["⑭프로젝트애쉬", 70, 85, 10, "che_event_저격", [52526, 350916, 13632, 97413, 19280], 1, "84923d1.jpg?=20190905"],
["⑩모니카", 87, 9, 56, "che_event_저격", [0, 0, 0, 0, 1527225], 1, "2bc52ba.jpg?=20190509"],
["㉖모루좀뽑아라", 75, 16, 91, "che_event_신산", [9373, 20400, 7815, 499558, 13989], 0, "default.jpg"],
["⑮킹구없이는못살아", 81, 75, 10, "che_event_격노", [446417, 16719, 30705, 36664, 18893], 1, "fb3c625.gif?=20190428"],
["④삼남매아빠", 85, 69, 10, "che_event_저격", [29974, 47859, 301751, 101873, 7317], 0, "default.jpg"],
["⑨슬라임", 71, 10, 86, "che_event_필살", [44971, 39437, 44401, 363250, 23876], 1, "3408568.jpg?=20190317"],
["⑬이취홍", 68, 86, 10, "che_event_무쌍", [9968, 40597, 308958, 60079, 17264], 1, "f25b1d7.jpg?=20190815"],
["⑦햄", 68, 9, 78, "che_event_저격", [39493, 84732, 96362, 681147, 25133], 1, "c73e24c.jpg?=20190125"],
["⑩소열제유비", 73, 68, 9, "che_event_저격", [0, 0, 0, 0, 489102], 1, "a3b1bbb.png?=20180927"],
["②도적", 68, 83, 10, "che_event_의술", [9477, 155276, 17280, 21509, 4784], 0, "default.jpg"],
["㉘건방진노예", 77, 15, 91, "che_event_신중", [33795, 36995, 30258, 369123, 5936], 0, "default.jpg"],
["㉖시딱마", 75, 15, 90, "che_event_신산", [17590, 0, 17110, 251531, 17269], 0, "default.jpg"],
["②명가의 수호자", 76, 12, 85, "che_event_신산", [35756, 111668, 199214, 1205047, 67252], 0, "default.jpg"],
["⑦매화날리기", 84, 52, 30, "che_event_무쌍", [67222, 374154, 80916, 152585, 18879], 1, "3ca7b77.jpg?=20190127"],
["⑪킹구갓구", 69, 88, 10, "che_event_척사", [11503, 191630, 3212, 41610, 26047], 1, "fb3c625.gif?=20190428"],
["⑥Newbie", 73, 10, 84, "che_event_신중", [56567, 63968, 76268, 579864, 16418], 1, "c8682f4.jpg?=20181204"],
["㉒김나영", 69, 10, 83, "che_event_반계", [23531, 29495, 30668, 242239, 4849], 1, "c90a3d4.jpg?=20190905"],
["⑪히비야", 69, 10, 86, "che_event_반계", [21254, 39045, 21437, 317321, 17548], 1, "28a8e8.png?=20190620"],
["⑲카이스트", 70, 10, 82, "che_event_귀병", [38657, 21232, 21186, 332069, 43094], 1, "9361ef8.jpg?=20180907"],
["⑳청랑구", 72, 86, 10, "che_event_격노", [36241, 67702, 469047, 91809, 38419], 1, "d6ebfb8.jpg?=20200305"],
["③ウサギ ミント", 77, 76, 10, "che_event_척사", [50200, 29776, 350766, 102238, 9821], 0, "default.jpg"],
["⑦카오스피닉스", 69, 9, 78, "che_event_저격", [76465, 70449, 108410, 864572, 22386], 1, "2af0641.jpg?=20180706"],
["㉙네시", 73, 15, 88, "che_event_신중", [3770, 13065, 3003, 125064, 16567], 0, "default.jpg"],
["⑧카오스피닉스", 71, 10, 85, "che_event_징병", [2628, 7785, 16722, 239656, 27758], 1, "2af0641.jpg?=20180706"],
["⑭땅땅이", 67, 10, 83, "che_event_저격", [22215, 12003, 21145, 153493, 1780], 1, "99cd27f.gif?=20190606"],
["⑭날떠나가지말아요", 67, 10, 86, "che_event_집중", [10291, 16394, 25056, 280012, 17274], 0, "default.jpg"],
["⑥타테미야 아야노", 68, 10, 83, "che_event_척사", [10118, 20754, 64516, 290459, 28211], 1, "f116a4.jpg?=20181213"],
["㉕고블린슬레이어", 79, 64, 10, "che_event_보병", [1072097, 44889, 110607, 199452, 11936], 1, "2943712.png?=20200604"],
["⑤료우기시키", 65, 76, 9, "che_event_견고", [288785, 13549, 163241, 132159, 6912], 1, "72a549f.png?=20181025"],
["⑦하야미 카나데", 65, 9, 79, "che_event_신중", [14693, 34950, 77407, 413112, 7739], 1, "c9d2dd5.gif?=20190215"],
["㉙초코맛국수", 75, 16, 84, "che_event_저격", [3978, 9405, 8903, 141528, 48094], 1, "4bcd3f5.jpg?=20200904"],
["⑫도추", 78, 10, 77, "che_event_반계", [33424, 17039, 34502, 362905, 23849], 0, "default.jpg"],
["①마스", 91, 11, 10, "che_event_무쌍", [2333, 3055, 0, 2330, 452014], 1, "5f739d7.gif?=20180712"],
["④사슴플라티나", 69, 11, 83, "che_event_신중", [17612, 9180, 41640, 151555, 12779], 1, "998925.jpg?=20180920"],
["③이브", 91, 11, 10, "che_event_징병", [29497, 5407, 16123, 15312, 211945], 1, "7f9f9c6.jpg?=20180831"],
["㉒만샘", 70, 82, 10, "che_event_위압", [14761, 376699, 29132, 70792, 31310], 1, "4a206a1.jpg?=20181122"],
["㉘초코맛국수", 74, 15, 94, "che_event_반계", [22968, 8249, 6181, 279734, 40911], 1, "4bcd3f5.jpg?=20200904"],
["⑧북오더", 69, 84, 10, "che_event_척사", [4803, 14424, 272800, 18411, 12429], 1, "c0276e1.gif?=20180917"],
["⑭페르난도", 70, 84, 10, "che_event_징병", [198412, 9954, 38902, 59777, 10115], 0, "default.jpg"],
["⑫카이스트", 68, 11, 87, "che_event_징병", [10773, 19837, 18001, 161161, 49652], 1, "9361ef8.jpg?=20180907"],
["㉘카류", 84, 83, 16, "che_event_위압", [7089, 55927, 500966, 75676, 28165], 1, "1a5835b.jpg?=20200905"],
["②렉사이", 74, 86, 10, "che_event_무쌍", [50200, 426369, 63501, 68528, 16438], 1, "6cc3ccf.jpg?=20180719"],
["③엔틱", 78, 68, 10, "che_event_견고", [187104, 22131, 43742, 45094, 18542], 0, "default.jpg"],
["㉑나만마딜없어", 68, 10, 84, "che_event_신중", [1992, 16326, 15588, 89107, 5832], 0, "default.jpg"],
["㉔모가미 시즈카", 83, 70, 10, "che_event_척사", [171308, 1093, 37132, 30646, 27127], 1, "db2e1be.jpg?=20200514"],
["③미스티", 86, 70, 10, "che_event_징병", [303011, 90413, 105582, 109313, 12040], 1, "1aadcba.png?=20180908"],
["⑭륜", 68, 11, 86, "che_event_귀병", [9362, 30131, 3853, 172499, 902], 1, "6561977.jpg?=20190904"],
["⑩붕붕", 87, 31, 33, "che_event_저격", [0, 0, 0, 0, 1040956], 1, "3958021.jpg?=20190509"],
["⑫제노에이지", 67, 10, 95, "che_event_집중", [15642, 8539, 7564, 193185, 16806], 1, "ddd1fd7.jpg?=20190320"],
["⑤그리고", 72, 11, 84, "che_event_필살", [71440, 35967, 103514, 451717, 33250], 0, "default.jpg"],
["⑱평민킬러", 73, 85, 10, "che_event_무쌍", [648896, 16172, 105798, 108806, 39869], 1, "fb23a32.jpg?=20190904"],
["⑫뽀", 71, 10, 91, "che_event_격노", [41282, 41874, 11142, 437907, 19323], 1, "34674f1.jpg?=20190719"],
["⑥윤지성", 71, 11, 83, "che_event_격노", [3813, 33716, 46917, 278476, 30217], 1, "d62ab55.png?=20181218"],
["⑥호에에에엙", 81, 75, 10, "che_event_돌격", [80841, 105196, 223478, 60069, 33392], 1, "5804326.gif?=20181207"],
["②샐러리맨", 69, 81, 10, "che_event_척사", [8758, 33798, 156554, 21898, 16072], 1, "f04de94.jpg?=20180731"],
["⑩료우기빵셔틀", 65, 9, 74, "che_event_저격", [0, 0, 0, 0, 439478], 0, "default.jpg"],
["③졸린마녀", 73, 82, 11, "che_event_견고", [90718, 621811, 79682, 81511, 29074], 1, "ef47121.jpg?=20180825"],
["⑦좀비A", 68, 78, 9, "che_event_필살", [635191, 54259, 135060, 225702, 22856], 1, "2a7a28.jpg?=20190225"],
["⑳롱리브더퀸", 70, 10, 82, "che_event_신중", [26511, 20589, 5404, 251811, 16211], 1, "99e4505.jpg?=20200110"],
["⑪오른쪽왼쪽제자리", 88, 69, 11, "che_event_격노", [742452, 42119, 41555, 44985, 23407], 1, "7996e6e.png?=20190705"],
["⑫치즈", 90, 70, 10, "che_event_견고", [489454, 21081, 5994, 63836, 24746], 1, "bd91cf9.jpg?=20190717"],
["⑯ㅁ", 70, 10, 85, "che_event_반계", [7115, 9864, 0, 172143, 35724], 1, "1f8b859.gif?=20191022"],
["㉕로리", 63, 10, 80, "che_event_집중", [62009, 26896, 55684, 558677, 13369], 1, "50794a6.jpg?=20190923"],
["⑧미스티", 71, 10, 88, "che_event_필살", [19503, 21014, 35515, 200531, 19059], 1, "1aadcba.png?=20180908"],
["③Karl", 68, 11, 81, "che_event_척사", [19987, 22453, 12689, 237959, 9237], 0, "default.jpg"],
["④자는중임", 85, 67, 10, "che_event_징병", [0, 0, 0, 4333, 459335], 1, "93af198.jpg?=20181007"],
["⑧나나야 시키", 74, 82, 10, "che_event_격노", [18213, 73787, 9776, 22679, 2091], 1, "5e5263e.jpg?=20190314"],
["⑤별", 66, 9, 75, "che_event_격노", [69645, 31109, 101201, 510270, 21789], 1, "1dd6263.jpg?=20181025"],
["⑭임사영", 70, 10, 86, "che_event_환술", [28175, 38878, 20033, 416721, 31091], 1, "d5dc381.gif?=20190719"],
["⑩Samo", 81, 9, 59, "che_event_저격", [0, 0, 1584, 0, 784171], 1, "a415c6b.jpg?=20190517"],
["⑮Samo", 71, 10, 87, "che_event_집중", [18614, 17619, 34999, 228055, 5725], 1, "25449b0.png?=20190626"],
["⑪무쇠로만든사람", 77, 11, 77, "che_event_척사", [14520, 21966, 12963, 85016, 18563], 1, "1b4b6f7.jpg?=20190608"],
["④고긔가먹ㄱ고싶ㅍ다", 71, 10, 87, "che_event_집중", [19664, 39902, 51999, 251303, 3113], 1, "a3c229e.gif?=20181005"],
["⑲불사조페이트", 83, 67, 10, "che_event_징병", [34261, 186187, 20458, 52037, 6163], 1, "fb7addd.jpg?=20190816"],
["㉙평민킬러", 76, 85, 15, "che_event_필살", [235256, 4058, 10652, 21438, 25208], 1, "fb23a32.jpg?=20190904"],
["⑧줄리엣", 87, 72, 10, "che_event_무쌍", [39145, 121268, 62105, 39731, 12485], 1, "175639b.png?=20181025"],
["⑫병리학적자세", 72, 10, 91, "che_event_집중", [69911, 35780, 11401, 688771, 27212], 1, "3679089.jpg?=20180629"],
["⑬호무새", 83, 69, 10, "che_event_척사", [6801, 25260, 287620, 58259, 35925], 1, "3649856.png?=20190815"],
["⑥료우기시키", 70, 84, 10, "che_event_위압", [8380, 59810, 197667, 66880, 2280], 1, "72a549f.png?=20181025"],
["⑩끼이이이이익", 82, 57, 9, "che_event_저격", [0, 0, 0, 0, 573957], 0, "default.jpg"],
["⑥드류", 70, 10, 83, "che_event_격노", [4265, 0, 45796, 234156, 9707], 1, "507327d.png?=20181001"],
["㉙1시30분", 89, 73, 15, "che_event_견고", [6227, 16004, 292895, 33442, 26718], 0, "default.jpg"],
["⑥카이스트", 71, 10, 83, "che_event_신중", [50236, 85696, 80990, 407936, 23191], 1, "9361ef8.jpg?=20180907"],
["④야부키 나코", 83, 72, 10, "che_event_위압", [113841, 345237, 39818, 102705, 21023], 1, "eac341b.png?=20181005"]
]
}
@@ -7,6 +7,7 @@ import { LiteHashDRBG, RandUtil, type RNG } from '@sammo-ts/common';
import {
ITEM_KEYS,
DOMESTIC_TRAIT_KEYS,
EVENT_DOMESTIC_TRAIT_KEYS,
NATION_TRAIT_KEYS,
PERSONALITY_TRAIT_KEYS,
WAR_TRAIT_KEYS,
@@ -450,7 +451,7 @@ describeWithReference('ref ↔ core2026 battle differential', () => {
expect(failures, failures.join('\n')).toEqual([]);
});
it('matches every war, personality, and nation trait in battle', { timeout: 180_000 }, () => {
it('matches every event-domestic, war, personality, and nation trait in battle', { timeout: 180_000 }, () => {
const unitSet = readJson<UnitSetDefinition>(
path.resolve(process.cwd(), '../../resources/unitset/unitset_che.json')
);
@@ -464,9 +465,25 @@ describeWithReference('ref ↔ core2026 battle differential', () => {
armTypes: { footman: 1, archer: 2, cavalry: 3, wizard: 4, siege: 5, misc: 6, castle: 0 },
};
const cases = [
...EVENT_DOMESTIC_TRAIT_KEYS.map((key) => ({
kind: 'eventDomestic' as const,
key,
})),
...WAR_TRAIT_KEYS.map((key) => ({ kind: 'war' as const, key })),
...PERSONALITY_TRAIT_KEYS.map((key) => ({ kind: 'personality' as const, key })),
...NATION_TRAIT_KEYS.map((key) => ({ kind: 'nation' as const, key })),
{
kind: 'dualSlot' as const,
key: 'che_event_무쌍+che_무쌍',
special: 'che_event_무쌍',
special2: 'che_무쌍',
},
{
kind: 'dualSlot' as const,
key: 'che_event_견고+che_견고',
special: 'che_event_견고',
special2: 'che_견고',
},
];
for (const entry of cases) {
@@ -478,7 +495,18 @@ describeWithReference('ref ↔ core2026 battle differential', () => {
base.attackerGeneral.leadership = 90;
base.attackerGeneral.strength = 85;
base.attackerGeneral.intel = 80;
base.attackerGeneral.special2 = entry.kind === 'war' ? entry.key : 'None';
base.attackerGeneral.special =
entry.kind === 'dualSlot'
? entry.special
: entry.kind === 'eventDomestic'
? entry.key
: 'None';
base.attackerGeneral.special2 =
entry.kind === 'dualSlot'
? entry.special2
: entry.kind === 'war'
? entry.key
: 'None';
base.attackerGeneral.personal = entry.kind === 'personality' ? entry.key : 'None';
if (entry.kind === 'nation') {
base.attackerNation.type = entry.key;