merge: scenario 2601 seed UI parity
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
||||
import { isSelectionPoolWorld, resolveSelectionMaxGeneral } from '../../services/selectPool.js';
|
||||
import { procedure, router } from '../../trpc.js';
|
||||
@@ -23,6 +25,7 @@ export const lobbyRouter = router({
|
||||
const userCnt = await ctx.db.general.count({ where: { npcState: { lt: 2 } } });
|
||||
const npcCnt = await ctx.db.general.count({ where: { npcState: { gte: 2 } } });
|
||||
const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } });
|
||||
const scenarioTitle = asRecord(asRecord(rawWorldState.meta).scenarioMeta).title;
|
||||
|
||||
let myGeneral = null;
|
||||
if (ctx.auth?.user.id) {
|
||||
@@ -55,6 +58,7 @@ export const lobbyRouter = router({
|
||||
isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0,
|
||||
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
|
||||
npcPossessionEnabled: worldState.config.npcMode === 1,
|
||||
scenarioTitle: typeof scenarioTitle === 'string' ? scenarioTitle : '',
|
||||
myGeneral,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -27,6 +27,8 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: true,
|
||||
imageServer: true,
|
||||
npcState: true,
|
||||
officerLevel: true,
|
||||
cityId: true,
|
||||
@@ -43,6 +45,16 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
crew: true,
|
||||
train: true,
|
||||
atmos: true,
|
||||
age: true,
|
||||
crewTypeId: true,
|
||||
weaponCode: true,
|
||||
bookCode: true,
|
||||
horseCode: true,
|
||||
itemCode: true,
|
||||
personalCode: true,
|
||||
specialCode: true,
|
||||
special2Code: true,
|
||||
meta: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
@@ -79,29 +91,62 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const generals = generalRows.map((general) => ({
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
npcState: general.npcState,
|
||||
officerLevel: general.officerLevel,
|
||||
cityId: general.cityId,
|
||||
turnTime: formatDateTime(general.turnTime),
|
||||
recentWar: formatDateTime(general.recentWarTime),
|
||||
warnum: battleCountMap.get(general.id) ?? 0,
|
||||
stats: {
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
intelligence: general.intel,
|
||||
},
|
||||
experience: general.experience,
|
||||
dedication: general.dedication,
|
||||
injury: general.injury,
|
||||
gold: general.gold,
|
||||
rice: general.rice,
|
||||
crew: general.crew,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
}));
|
||||
const generals = generalRows.map((general) => {
|
||||
const meta =
|
||||
general.meta && typeof general.meta === 'object' && !Array.isArray(general.meta)
|
||||
? (general.meta as Record<string, unknown>)
|
||||
: {};
|
||||
const metaNumber = (key: string): number => {
|
||||
const value = meta[key];
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
||||
};
|
||||
return {
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
picture: general.picture,
|
||||
imageServer: general.imageServer,
|
||||
npcState: general.npcState,
|
||||
officerLevel: general.officerLevel,
|
||||
cityId: general.cityId,
|
||||
turnTime: formatDateTime(general.turnTime),
|
||||
recentWar: formatDateTime(general.recentWarTime),
|
||||
warnum: battleCountMap.get(general.id) ?? 0,
|
||||
stats: {
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
intelligence: general.intel,
|
||||
},
|
||||
experience: general.experience,
|
||||
dedication: general.dedication,
|
||||
injury: general.injury,
|
||||
gold: general.gold,
|
||||
rice: general.rice,
|
||||
crew: general.crew,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
age: general.age,
|
||||
crewTypeId: general.crewTypeId,
|
||||
equipment: {
|
||||
weapon: general.weaponCode,
|
||||
book: general.bookCode,
|
||||
horse: general.horseCode,
|
||||
item: general.itemCode,
|
||||
},
|
||||
traits: {
|
||||
personal: general.personalCode,
|
||||
specialDomestic: general.specialCode,
|
||||
specialWar: general.special2Code,
|
||||
},
|
||||
battleStats: {
|
||||
kills: metaNumber('rank_killnum') || metaNumber('killnum'),
|
||||
deaths: metaNumber('deathnum'),
|
||||
fire: metaNumber('firenum'),
|
||||
killCrew: metaNumber('killcrew'),
|
||||
deathCrew: metaNumber('deathcrew'),
|
||||
dex: [1, 2, 3, 4, 5].map((index) => metaNumber(`dex${index}`)),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
me: {
|
||||
|
||||
@@ -5,7 +5,13 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission, zGeneralLogType, type GeneralLogType } from '../shared.js';
|
||||
import {
|
||||
assertNationAccess,
|
||||
formatDateTime,
|
||||
resolveNationPermission,
|
||||
zGeneralLogType,
|
||||
type GeneralLogType,
|
||||
} from '../shared.js';
|
||||
|
||||
export const getGeneralLog = authedProcedure
|
||||
.input(
|
||||
@@ -75,6 +81,9 @@ export const getGeneralLog = authedProcedure
|
||||
logs: logs.map((entry) => ({
|
||||
id: entry.id,
|
||||
text: entry.text,
|
||||
year: entry.year,
|
||||
month: entry.month,
|
||||
createdAt: formatDateTime(entry.createdAt),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getNationInfo = authedProcedure.query(async ({ ctx }) => {
|
||||
nationId: me.nationId,
|
||||
},
|
||||
select: { id: true, year: true, month: true, text: true },
|
||||
orderBy: { id: 'asc' },
|
||||
orderBy: { id: 'desc' },
|
||||
}),
|
||||
]);
|
||||
if (!nation) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { loadUnitSetDefinitionByName } from '../../../battleSim/unitSetLoader.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission } from '../shared.js';
|
||||
@@ -41,7 +42,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
||||
});
|
||||
}
|
||||
|
||||
const [cities, troops, generalRows] = await Promise.all([
|
||||
const [cities, troops, generalRows, worldState] = await Promise.all([
|
||||
ctx.db.city.findMany({ select: { id: true, name: true } }),
|
||||
ctx.db.troop.findMany({
|
||||
where: { nationId: me.nationId },
|
||||
@@ -51,7 +52,14 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
||||
where: { nationId: me.nationId },
|
||||
orderBy: [{ turnTime: 'asc' }, { id: 'asc' }],
|
||||
}),
|
||||
ctx.db.worldState.findFirst({ select: { config: true } }),
|
||||
]);
|
||||
const worldConfig = asRecord(worldState?.config);
|
||||
const environment = asRecord(worldConfig.environment ?? worldConfig.map);
|
||||
const unitSetName =
|
||||
typeof environment.unitSet === 'string' && environment.unitSet.trim() ? environment.unitSet : ctx.profile.id;
|
||||
const unitSet = await loadUnitSetDefinitionByName(unitSetName);
|
||||
const crewTypeNames = new Map((unitSet.crewTypes ?? []).map((crewType) => [crewType.id, crewType.name]));
|
||||
const generalIds = generalRows.map((general) => general.id);
|
||||
const turns = generalIds.length
|
||||
? await ctx.db.generalTurn.findMany({
|
||||
@@ -92,6 +100,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
||||
defenceTrain,
|
||||
defenceTrainText: defenceTrainText(defenceTrain),
|
||||
crewTypeId: general.crewTypeId,
|
||||
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
|
||||
crew: general.crew,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { getGoldIncome, getOutcome, getRiceIncome, getWallIncome, getWarGoldIncome, type NationIncomeContext } from '@sammo-ts/logic';
|
||||
import {
|
||||
getGoldIncome,
|
||||
getOutcome,
|
||||
getRiceIncome,
|
||||
getWallIncome,
|
||||
getWarGoldIncome,
|
||||
type NationIncomeContext,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
@@ -171,13 +178,7 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>();
|
||||
for (const city of cityRows) {
|
||||
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||
const valueSum =
|
||||
city.population +
|
||||
city.agriculture +
|
||||
city.commerce +
|
||||
city.security +
|
||||
city.wall +
|
||||
city.defence;
|
||||
const valueSum = city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||
const maxSum =
|
||||
city.populationMax +
|
||||
city.agricultureMax +
|
||||
@@ -222,22 +223,24 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const nationsList = nationRows.map((nationItem) => {
|
||||
const diplomacy =
|
||||
nationItem.id === nation.id
|
||||
? { state: 7, term: null }
|
||||
: diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 };
|
||||
return {
|
||||
id: nationItem.id,
|
||||
name: nationItem.name,
|
||||
color: nationItem.color,
|
||||
level: nationItem.level,
|
||||
power: powerByNation.get(nationItem.id) ?? 0,
|
||||
generalCount: generalCountMap.get(nationItem.id) ?? 0,
|
||||
cityCount: cityCountMap.get(nationItem.id) ?? 0,
|
||||
diplomacy,
|
||||
};
|
||||
});
|
||||
const nationsList = nationRows
|
||||
.filter((nationItem) => nationItem.id > 0)
|
||||
.map((nationItem) => {
|
||||
const diplomacy =
|
||||
nationItem.id === nation.id
|
||||
? { state: 7, term: null }
|
||||
: (diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 });
|
||||
return {
|
||||
id: nationItem.id,
|
||||
name: nationItem.name,
|
||||
color: nationItem.color,
|
||||
level: nationItem.level,
|
||||
power: powerByNation.get(nationItem.id) ?? 0,
|
||||
generalCount: generalCountMap.get(nationItem.id) ?? 0,
|
||||
cityCount: cityCountMap.get(nationItem.id) ?? 0,
|
||||
diplomacy,
|
||||
};
|
||||
});
|
||||
|
||||
const nationCities = cityRows.filter((city) => city.nationId === nation.id);
|
||||
const nationGenerals = generalRows.filter((general) => general.nationId === nation.id);
|
||||
|
||||
@@ -143,9 +143,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
const nationGenerals = generalsByNation.get(nation.id) ?? [];
|
||||
const nationCities = citiesByNation.get(nation.id) ?? [];
|
||||
const officers = Array.from({ length: 8 }, (_, index) => 12 - index).map((officerLevel) => {
|
||||
const general = nationGenerals
|
||||
.filter((candidate) => candidate.officerLevel === officerLevel)
|
||||
.at(-1);
|
||||
const general = nationGenerals.filter((candidate) => candidate.officerLevel === officerLevel).at(-1);
|
||||
return {
|
||||
officerLevel,
|
||||
general: general
|
||||
@@ -178,7 +176,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
},
|
||||
power: readMetaNumber(nation.meta, 'power'),
|
||||
capitalCityId: nation.capitalCityId ?? 0,
|
||||
generalCount: nationGenerals.length,
|
||||
generalCount: readMetaNumber(nation.meta, 'gennum', nationGenerals.length),
|
||||
cityCount: nationCities.length,
|
||||
officers,
|
||||
ambassadorNames: secretPermissions
|
||||
@@ -204,8 +202,8 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional()).query(
|
||||
async ({ ctx, input }) => {
|
||||
await getMyGeneral(ctx);
|
||||
const sort = input?.sort ?? 9;
|
||||
const [generals, nations, accessLogs, worldState] = await Promise.all([
|
||||
@@ -365,4 +363,5 @@ export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: z
|
||||
});
|
||||
|
||||
return { sort, generals: rows };
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,10 +7,7 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
||||
import {
|
||||
generalAccessEndpointWeights,
|
||||
recordGeneralAccessWeight,
|
||||
} from '../../services/generalAccess.js';
|
||||
import { generalAccessEndpointWeights, recordGeneralAccessWeight } from '../../services/generalAccess.js';
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
|
||||
@@ -72,6 +69,7 @@ const parseYearbookNations = (value: unknown): YearbookNation[] => {
|
||||
|
||||
const resolveArchiveTarget = (
|
||||
worldMeta: unknown,
|
||||
profileId: string,
|
||||
profileName: string,
|
||||
requestedServerId?: string
|
||||
): { archiveKey: string; legacyAlias: string | null; isCurrentProfile: boolean } => {
|
||||
@@ -81,7 +79,12 @@ const resolveArchiveTarget = (
|
||||
const isCurrentProfile = requested === profileName || requested === canonicalServerId;
|
||||
return {
|
||||
archiveKey: isCurrentProfile ? canonicalServerId : requested,
|
||||
legacyAlias: isCurrentProfile && canonicalServerId !== profileName ? profileName : null,
|
||||
legacyAlias:
|
||||
isCurrentProfile && canonicalServerId !== profileName
|
||||
? profileName
|
||||
: isCurrentProfile && profileId !== canonicalServerId
|
||||
? profileId
|
||||
: null,
|
||||
isCurrentProfile,
|
||||
};
|
||||
};
|
||||
@@ -263,7 +266,7 @@ export const yearbookRouter = router({
|
||||
message: 'World state is not initialized.',
|
||||
});
|
||||
}
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.name, input?.serverID);
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.id, ctx.profile.name, input?.serverID);
|
||||
|
||||
const findRange = async (profileName: string) =>
|
||||
Promise.all([
|
||||
@@ -278,10 +281,19 @@ export const yearbookRouter = router({
|
||||
orderBy: [{ year: 'desc' as const }, { month: 'desc' as const }],
|
||||
}),
|
||||
]);
|
||||
let [firstRow, lastRow] = await findRange(target.archiveKey);
|
||||
if ((!firstRow || !lastRow) && target.legacyAlias) {
|
||||
[firstRow, lastRow] = await findRange(target.legacyAlias);
|
||||
}
|
||||
const ranges = await Promise.all(
|
||||
[target.archiveKey, target.legacyAlias]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map(findRange)
|
||||
);
|
||||
const firstRow = ranges
|
||||
.map(([first]) => first)
|
||||
.filter((row): row is NonNullable<typeof row> => Boolean(row))
|
||||
.sort((a, b) => joinYearMonth(a.year, a.month) - joinYearMonth(b.year, b.month))[0];
|
||||
const lastRow = ranges
|
||||
.map(([, last]) => last)
|
||||
.filter((row): row is NonNullable<typeof row> => Boolean(row))
|
||||
.sort((a, b) => joinYearMonth(b.year, b.month) - joinYearMonth(a.year, a.month))[0];
|
||||
|
||||
if (!target.isCurrentProfile && (!firstRow || !lastRow)) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' });
|
||||
@@ -314,7 +326,7 @@ export const yearbookRouter = router({
|
||||
if (!worldState) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
||||
}
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.name, input.serverID);
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.id, ctx.profile.name, input.serverID);
|
||||
const shouldRecordAfterHashCheck = target.isCurrentProfile && Boolean(input.hash);
|
||||
if (target.isCurrentProfile && !shouldRecordAfterHashCheck) {
|
||||
await recordHistoryAccess(ctx);
|
||||
|
||||
@@ -79,7 +79,7 @@ const createContext = (options: {
|
||||
nationMeta?: Record<string, unknown>;
|
||||
requestCommand?: ReturnType<typeof vi.fn>;
|
||||
accessToken?: string;
|
||||
logs?: Array<{ id: number; text: string }>;
|
||||
logs?: Array<{ id: number; text: string; year?: number; month?: number; createdAt?: Date }>;
|
||||
}) => {
|
||||
const me = options.me === undefined ? buildGeneral() : options.me;
|
||||
const targets = options.targets ?? (me ? [me] : []);
|
||||
@@ -119,12 +119,24 @@ const createContext = (options: {
|
||||
},
|
||||
logEntry: {
|
||||
groupBy: vi.fn(async () => []),
|
||||
findMany: vi.fn(async (query?: { where?: { id?: { lt?: number } }; take?: number }) => {
|
||||
const source = options.logs ?? [{ id: 1, text: '기록' }];
|
||||
const beforeId = query?.where?.id?.lt;
|
||||
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source;
|
||||
return query?.take ? filtered.slice(0, query.take) : filtered;
|
||||
}),
|
||||
findMany: vi.fn(
|
||||
async (query?: {
|
||||
where?: { id?: { lt?: number } };
|
||||
take?: number;
|
||||
select?: { id?: boolean; text?: boolean };
|
||||
}) => {
|
||||
const source = (options.logs ?? [{ id: 1, text: '기록' }]).map((entry) => ({
|
||||
year: 185,
|
||||
month: 1,
|
||||
createdAt: now,
|
||||
...entry,
|
||||
}));
|
||||
const beforeId = query?.where?.id?.lt;
|
||||
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source;
|
||||
const selected = query?.select ? filtered.map(({ id, text }) => ({ id, text })) : filtered;
|
||||
return query?.take ? selected.slice(0, query.take) : selected;
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
const redisClient = { get: async () => null, set: async () => null };
|
||||
@@ -405,6 +417,14 @@ describe('battle-center general and user permissions', () => {
|
||||
});
|
||||
await expect(appRouter.createCaller(tenured.context).nation.getBattleCenter()).resolves.toMatchObject({
|
||||
me: { id: 7, permissionLevel: 1 },
|
||||
generals: [
|
||||
{
|
||||
id: 7,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
battleStats: { kills: 0, deaths: 0, fire: 0, killCrew: 0, deathCrew: 0, dex: [0, 0, 0, 0, 0] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auditor = createContext({
|
||||
@@ -430,6 +450,7 @@ describe('battle-center general and user permissions', () => {
|
||||
|
||||
await expect(member.nation.getGeneralLog({ generalId: me.id, type: 'generalAction' })).resolves.toMatchObject({
|
||||
generalId: me.id,
|
||||
logs: [{ id: 1, year: 185, month: 1, createdAt: '2026-01-01 00:00:00' }],
|
||||
});
|
||||
await expect(
|
||||
member.nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' })
|
||||
|
||||
@@ -52,12 +52,27 @@ const archiveRows = [
|
||||
year: 219,
|
||||
month: 12,
|
||||
map: { year: 219, month: 12, startYear: 190, cityList: [], nationList: [] },
|
||||
nations: [{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] }],
|
||||
nations: [
|
||||
{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] },
|
||||
],
|
||||
globalHistory: ['저장된 현재 기수 과거 기록'],
|
||||
globalAction: ['저장된 현재 기수 과거 행동'],
|
||||
hash: 'current-archive',
|
||||
createdAt: new Date('2026-07-31T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
profileName: profile.id,
|
||||
sourceId: 201,
|
||||
year: 219,
|
||||
month: 11,
|
||||
map: { year: 219, month: 11, startYear: 190, cityList: [], nationList: [] },
|
||||
nations: [],
|
||||
globalHistory: ['레거시 프로필 별칭 기록'],
|
||||
globalAction: [],
|
||||
hash: 'legacy-profile-alias',
|
||||
createdAt: new Date('2026-07-30T00:00:00.000Z'),
|
||||
},
|
||||
];
|
||||
|
||||
const authFor = (userId: string): GameSessionTokenPayload => ({
|
||||
@@ -75,14 +90,21 @@ const authFor = (userId: string): GameSessionTokenPayload => ({
|
||||
sanctions: {},
|
||||
});
|
||||
|
||||
const buildContext = (auth: GameSessionTokenPayload | null, options: { hasGeneral?: boolean } = {}): GameApiContext => {
|
||||
const buildContext = (
|
||||
auth: GameSessionTokenPayload | null,
|
||||
options: { hasGeneral?: boolean; worldMeta?: unknown } = {}
|
||||
): GameApiContext => {
|
||||
const db = {
|
||||
general: {
|
||||
findFirst: async ({ where }: { where: { userId: string } }) =>
|
||||
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
|
||||
},
|
||||
worldState: {
|
||||
findFirst: async () => ({ currentYear: 220, currentMonth: 1, meta: { serverId: currentServerId } }),
|
||||
findFirst: async () => ({
|
||||
currentYear: 220,
|
||||
currentMonth: 1,
|
||||
meta: options.worldMeta ?? { serverId: currentServerId },
|
||||
}),
|
||||
},
|
||||
yearbookHistory: {
|
||||
findFirst: async (args: {
|
||||
@@ -198,6 +220,16 @@ describe('historical yearbook access from dynasty', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reads imported history under the short profile ID when world metadata has no server ID', async () => {
|
||||
const caller = appRouter.createCaller(buildContext(authFor('owner-a'), { worldMeta: {} }));
|
||||
|
||||
await expect(caller.yearbook.getRange()).resolves.toEqual({
|
||||
firstYearMonth: 219 * 12 + 10,
|
||||
lastYearMonth: 219 * 12 + 10,
|
||||
currentYearMonth: 220 * 12,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses stored logs for a past month of the current generation', async () => {
|
||||
const caller = appRouter.createCaller(buildContext(authFor('owner-a')));
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const nameColor = computed(() => (props.npcState !== null ? getNpcColor(props.npcState) : undefined));
|
||||
const displayName = computed(() => {
|
||||
const name = props.name ?? '-';
|
||||
return (props.npcState ?? 0) > 0 && !/^[ⓜⓝ]/u.test(name) ? `ⓝ${name}` : name;
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if (props.clickable) {
|
||||
@@ -45,7 +49,7 @@ const handleClick = () => {
|
||||
<span
|
||||
class="compact-name"
|
||||
:style="{ color: nameColor, textDecoration: props.isMe ? 'underline' : undefined }"
|
||||
>{{ props.name ?? '-' }}</span
|
||||
>{{ displayName }}</span
|
||||
>
|
||||
<span class="compact-meta"
|
||||
><span>{{ props.officerLevelText }}</span
|
||||
@@ -56,7 +60,7 @@ const handleClick = () => {
|
||||
<div class="chief-title">
|
||||
<span class="chief-level">{{ props.officerLevelText }}</span>
|
||||
<span class="chief-name" :style="{ color: nameColor }">
|
||||
{{ props.name ?? '-' }}
|
||||
{{ displayName }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="props.isMe" class="chief-me">ME</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ const onQuick = (item: QuickNavigationItem) => {
|
||||
외부 메뉴
|
||||
<span class="dropup-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === 'global'" id="mobile-global-menu" class="bottom-popup" role="menu">
|
||||
<ul v-if="openId === 'global'" id="mobile-global-menu" class="bottom-popup" role="menu">
|
||||
<template v-for="entry in globalEntries" :key="entry.id">
|
||||
<li v-if="entry.kind === 'link'" role="none">
|
||||
<MainNavigationLink
|
||||
@@ -122,7 +122,7 @@ const onQuick = (item: QuickNavigationItem) => {
|
||||
국가 메뉴
|
||||
<span class="dropup-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === 'nation'" id="mobile-nation-menu" class="bottom-popup" role="menu">
|
||||
<ul v-if="openId === 'nation'" id="mobile-nation-menu" class="bottom-popup" role="menu">
|
||||
<template v-for="entry in nationNavigation" :key="entry.id">
|
||||
<li v-if="entry.kind === 'link'" role="none">
|
||||
<MainNavigationLink
|
||||
@@ -163,7 +163,7 @@ const onQuick = (item: QuickNavigationItem) => {
|
||||
빠른 이동
|
||||
<span class="dropup-caret" aria-hidden="true"></span>
|
||||
</button>
|
||||
<ul v-show="openId === 'quick'" id="mobile-quick-menu" class="bottom-popup" role="menu">
|
||||
<ul v-if="openId === 'quick'" id="mobile-quick-menu" class="bottom-popup" role="menu">
|
||||
<template v-for="item in quickNavigation" :key="item.id">
|
||||
<template v-if="'kind' in item">
|
||||
<li class="bottom-heading" role="presentation">{{ item.label }}</li>
|
||||
|
||||
@@ -426,6 +426,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
||||
}
|
||||
|
||||
.MessageList {
|
||||
height: 650px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ const props = defineProps<{
|
||||
winnerId?: number;
|
||||
betTotals?: Record<number, number>;
|
||||
totalBet: number;
|
||||
forceDesktop?: boolean;
|
||||
showLegend?: boolean;
|
||||
}>();
|
||||
|
||||
const bracket = computed(() => buildTournamentBracket(props.participants, props.matches, props.winnerId));
|
||||
@@ -68,7 +70,18 @@ const odds = (id: number | null) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="tournament-bracket" aria-label="토너먼트 대진표" tabindex="0">
|
||||
<section
|
||||
class="tournament-bracket"
|
||||
:class="{ 'force-desktop': forceDesktop }"
|
||||
aria-label="토너먼트 대진표"
|
||||
tabindex="0"
|
||||
>
|
||||
<span class="legacy-connector-text">
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┏━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━┻━━━━━━━━━┓
|
||||
┏━━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━━┻━━━━━━━━━┓ ┏━━━━━━━━━┻━━━━━━━━━┓ ┏━━━━┻━━━━┓ ┏━━━━┻━━━━┓ ┏━━━━┻━━━━┓
|
||||
┏━━━━┻━━━━┓ ┏━━━━┻━━━━┓ ┏━━━━┻━━━━┓ ┏━━━━┻━━━━┓ ┏━━━━┻━━━━┓
|
||||
</span>
|
||||
<div class="bracket-canvas">
|
||||
<div class="bracket-round bracket-champion" style="--slot-count: 1">
|
||||
<span
|
||||
@@ -176,7 +189,9 @@ const odds = (id: number | null) => {
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<p>배당률이 낮을수록 베팅된 금액이 많고 유저들이 우승후보로 많이 선택한 장수입니다.</p>
|
||||
<p v-if="showLegend !== false">
|
||||
배당률이 낮을수록 베팅된 금액이 많고 유저들이 우승후보로 많이 선택한 장수입니다.
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -186,6 +201,14 @@ const odds = (id: number | null) => {
|
||||
padding: 10px 0;
|
||||
scrollbar-color: #777 #24140e;
|
||||
}
|
||||
.legacy-connector-text {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bracket-canvas {
|
||||
width: 2000px;
|
||||
min-width: 2000px;
|
||||
@@ -308,5 +331,16 @@ const odds = (id: number | null) => {
|
||||
.mobile-bracket {
|
||||
display: block;
|
||||
}
|
||||
.tournament-bracket.force-desktop {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tournament-bracket.force-desktop .bracket-canvas {
|
||||
display: block;
|
||||
}
|
||||
.tournament-bracket.force-desktop .mobile-bracket {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -202,7 +202,11 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main id="container" class="legacy-auction-page bg0">
|
||||
<main
|
||||
id="container"
|
||||
class="legacy-auction-page bg0"
|
||||
:class="activeTab === 'resource' ? 'resource-page' : 'unique-page'"
|
||||
>
|
||||
<header class="top-back-bar bg0">
|
||||
<button class="legacy-button close-button" type="button" @click="closeWindow">창 닫기</button>
|
||||
<button class="legacy-button reload-button" type="button" :disabled="loading" @click="loadOverview">
|
||||
@@ -240,12 +244,16 @@ onMounted(() => {
|
||||
<span class="bid-ratio">단가</span><span class="finish-bid">마감가</span>
|
||||
<span class="close-date">거래 종료</span>
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
v-for="auction in buyRice"
|
||||
:key="auction.id"
|
||||
class="resource-row clickable-row"
|
||||
:class="{ selected: selectedResource?.id === auction.id }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectResource(auction)"
|
||||
@keydown.enter="selectResource(auction)"
|
||||
@keydown.space.prevent="selectResource(auction)"
|
||||
>
|
||||
<span class="idx tnum">{{ auction.id }}</span>
|
||||
<span class="host">{{ auction.hostName }}</span>
|
||||
@@ -263,7 +271,7 @@ onMounted(() => {
|
||||
</span>
|
||||
<span class="finish-bid tnum">금 {{ formatNumber(auction.detail.finishBidAmount) }}</span>
|
||||
<span class="close-date tnum">{{ cutDateTime(auction.closeAt) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="buyRice.length === 0" class="empty-row">진행 중인 쌀 구매 경매가 없습니다.</p>
|
||||
</section>
|
||||
|
||||
@@ -275,12 +283,16 @@ onMounted(() => {
|
||||
<span class="bid-ratio">단가</span><span class="finish-bid">마감가</span>
|
||||
<span class="close-date">거래 종료</span>
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
v-for="auction in sellRice"
|
||||
:key="auction.id"
|
||||
class="resource-row clickable-row"
|
||||
:class="{ selected: selectedResource?.id === auction.id }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectResource(auction)"
|
||||
@keydown.enter="selectResource(auction)"
|
||||
@keydown.space.prevent="selectResource(auction)"
|
||||
>
|
||||
<span class="idx tnum">{{ auction.id }}</span>
|
||||
<span class="host">{{ auction.hostName }}</span>
|
||||
@@ -298,7 +310,7 @@ onMounted(() => {
|
||||
</span>
|
||||
<span class="finish-bid tnum">쌀 {{ formatNumber(auction.detail.finishBidAmount) }}</span>
|
||||
<span class="close-date tnum">{{ cutDateTime(auction.closeAt) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="sellRice.length === 0" class="empty-row">진행 중인 쌀 판매 경매가 없습니다.</p>
|
||||
</section>
|
||||
|
||||
@@ -322,6 +334,10 @@ onMounted(() => {
|
||||
|
||||
<h3 class="subsection-title">경매 등록</h3>
|
||||
<form class="open-form" @submit.prevent="openResourceAuction">
|
||||
<input type="hidden" name="action" value="openAuction" />
|
||||
<input type="hidden" name="server" value="hwe" />
|
||||
<input type="hidden" name="turn" value="24" />
|
||||
<input type="hidden" name="submitType" value="resource" />
|
||||
<fieldset>
|
||||
<legend>매물</legend>
|
||||
<div class="item-toggle">
|
||||
@@ -414,12 +430,16 @@ onMounted(() => {
|
||||
<span>번호</span><span>경매명</span><span>주최자</span><span>종료일시</span> <span>연장</span
|
||||
><span>1순위</span><span>포인트</span>
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
v-for="auction in ongoingUnique"
|
||||
:key="auction.id"
|
||||
class="unique-row clickable-row"
|
||||
:class="{ selected: selectedUnique?.id === auction.id }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectUnique(auction)"
|
||||
@keydown.enter="selectUnique(auction)"
|
||||
@keydown.space.prevent="selectUnique(auction)"
|
||||
>
|
||||
<span>{{ auction.id }}</span
|
||||
><span>{{ auction.detail.title ?? auction.targetCode }}</span>
|
||||
@@ -432,7 +452,7 @@ onMounted(() => {
|
||||
<span class="tnum">{{
|
||||
formatNumber(auction.highestBid?.amount ?? auction.detail.startBidAmount)
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="ongoingUnique.length === 0" class="empty-row">진행중인 유니크 경매가 없습니다.</p>
|
||||
</section>
|
||||
|
||||
@@ -442,12 +462,16 @@ onMounted(() => {
|
||||
<span>번호</span><span>경매명</span><span>주최자</span><span>종료일시</span> <span>연장</span
|
||||
><span>1순위</span><span>포인트</span>
|
||||
</div>
|
||||
<button
|
||||
<div
|
||||
v-for="auction in finishedUnique"
|
||||
:key="auction.id"
|
||||
class="unique-row clickable-row"
|
||||
:class="{ selected: selectedUnique?.id === auction.id }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="selectUnique(auction)"
|
||||
@keydown.enter="selectUnique(auction)"
|
||||
@keydown.space.prevent="selectUnique(auction)"
|
||||
>
|
||||
<span>{{ auction.id }}</span
|
||||
><span>{{ auction.detail.title ?? auction.targetCode }}</span>
|
||||
@@ -460,9 +484,10 @@ onMounted(() => {
|
||||
<span class="tnum">{{
|
||||
formatNumber(auction.highestBid?.amount ?? auction.detail.startBidAmount)
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="finishedUnique.length === 0" class="empty-row">종료된 유니크 경매가 없습니다.</p>
|
||||
</section>
|
||||
<input type="hidden" name="auctionType" value="unique" />
|
||||
</section>
|
||||
|
||||
<footer class="bottom-bar bg0">
|
||||
@@ -482,6 +507,14 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
.legacy-auction-page.resource-page {
|
||||
height: 689px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.legacy-auction-page.unique-page {
|
||||
height: 378px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.legacy-auction-page.bg0 {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -784,6 +817,12 @@ input:focus-visible {
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.legacy-auction-page.resource-page {
|
||||
height: 1109px;
|
||||
}
|
||||
.legacy-auction-page.unique-page {
|
||||
height: 420px;
|
||||
}
|
||||
.resource-row {
|
||||
min-height: 43px;
|
||||
grid-template-columns: 1fr 3fr 3fr 1fr 2fr 2fr;
|
||||
|
||||
@@ -6,6 +6,7 @@ import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { getNpcColor } from '../utils/npcColor';
|
||||
import { formatLog } from '../utils/formatLog';
|
||||
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
||||
|
||||
type BattleCenterResponse = Awaited<ReturnType<typeof trpc.nation.getBattleCenter.query>>;
|
||||
type GeneralEntry = BattleCenterResponse['generals'][number];
|
||||
@@ -134,6 +135,8 @@ const formatGeneralLabel = (general: GeneralEntry): string => {
|
||||
return `${name} (${time})`;
|
||||
};
|
||||
|
||||
const generalImageUrl = (general: GeneralEntry): string => resolveGeneralIconUrl(general);
|
||||
|
||||
let logRequestId = 0;
|
||||
|
||||
const loadLogs = async (generalId: number) => {
|
||||
@@ -151,10 +154,13 @@ const loadLogs = async (generalId: number) => {
|
||||
return;
|
||||
}
|
||||
for (const response of responses) {
|
||||
const formatted = response.logs.map((entry) => ({
|
||||
id: entry.id,
|
||||
html: formatLog(entry.text),
|
||||
}));
|
||||
const formatted = response.logs.map((entry) => {
|
||||
const eventTime = response.type === 'generalAction' ? ` ${entry.createdAt.slice(-8, -3)}` : '';
|
||||
return {
|
||||
id: entry.id,
|
||||
html: formatLog(`${entry.text}${eventTime}`),
|
||||
};
|
||||
});
|
||||
logs[response.type] = formatted;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -223,9 +229,13 @@ onMounted(() => {
|
||||
<template>
|
||||
<main class="ref-shell battle-page">
|
||||
<header class="battle-top legacy-bg0">
|
||||
<RouterLink class="battle-nav" to="/">창 닫기</RouterLink>
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="battle-nav" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
<button class="battle-nav" @click="loadBattleCenter">갱신</button>
|
||||
<h1>감찰부</h1><div></div><div></div>
|
||||
<h1>감찰부</h1>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</header>
|
||||
|
||||
<div v-if="error" class="ref-feedback ref-feedback--error" role="alert">{{ error }}</div>
|
||||
@@ -260,6 +270,12 @@ onMounted(() => {
|
||||
<div class="battle-general-name">
|
||||
{{ selectedGeneral.name }} (관직 {{ selectedGeneral.officerLevel }})
|
||||
</div>
|
||||
<span
|
||||
class="battle-general-portrait"
|
||||
role="img"
|
||||
:aria-label="`${selectedGeneral.name} 초상`"
|
||||
:style="{ backgroundImage: `url(${generalImageUrl(selectedGeneral)})` }"
|
||||
/>
|
||||
<div class="battle-general-grid">
|
||||
<span>통솔</span><strong>{{ selectedGeneral.stats.leadership }}</strong> <span>무력</span
|
||||
><strong>{{ selectedGeneral.stats.strength }}</strong> <span>지력</span
|
||||
@@ -274,6 +290,21 @@ onMounted(() => {
|
||||
><strong>{{ selectedGeneral.dedication }}</strong> <span>전투</span
|
||||
><strong>{{ selectedGeneral.warnum }}회</strong>
|
||||
</div>
|
||||
<div class="battle-general-extra">
|
||||
<span>명성</span><strong>{{ selectedGeneral.experience.toLocaleString('ko-KR') }}</strong>
|
||||
<span>계급</span><strong>{{ selectedGeneral.dedication.toLocaleString('ko-KR') }}</strong>
|
||||
<span>나이</span><strong>{{ selectedGeneral.age }}세</strong> <span>병종</span
|
||||
><strong>{{ selectedGeneral.crewTypeId }}</strong> <span>승리</span
|
||||
><strong>{{ selectedGeneral.battleStats.kills }}</strong> <span>패배</span
|
||||
><strong>{{ selectedGeneral.battleStats.deaths }}</strong> <span>사살</span
|
||||
><strong>{{ selectedGeneral.battleStats.killCrew.toLocaleString('ko-KR') }}</strong>
|
||||
<span>피살</span
|
||||
><strong>{{ selectedGeneral.battleStats.deathCrew.toLocaleString('ko-KR') }}</strong>
|
||||
<span>전투 특기</span><strong>{{ selectedGeneral.traits.specialWar }}</strong>
|
||||
<span>내정 특기</span><strong>{{ selectedGeneral.traits.specialDomestic }}</strong>
|
||||
<span>성격</span><strong>{{ selectedGeneral.traits.personal }}</strong> <span>숙련도</span
|
||||
><strong>{{ selectedGeneral.battleStats.dex.join(' / ') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedGeneral" class="general-meta">
|
||||
<div>최근 턴: {{ selectedGeneral.turnTime ? selectedGeneral.turnTime.slice(-5) : '-' }}</div>
|
||||
@@ -298,6 +329,11 @@ onMounted(() => {
|
||||
</PanelCard>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="battle-footer legacy-bg0">
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="battle-nav" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -340,10 +376,20 @@ onMounted(() => {
|
||||
|
||||
.battle-general-card {
|
||||
min-height: 292px;
|
||||
position: relative;
|
||||
background-color: #172a52;
|
||||
background-image: var(--sammo-texture-blue);
|
||||
}
|
||||
|
||||
.battle-general-portrait {
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 80px;
|
||||
float: left;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.battle-general-name {
|
||||
min-height: 24px;
|
||||
padding: 2px 6px;
|
||||
@@ -359,6 +405,33 @@ onMounted(() => {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.battle-general-extra {
|
||||
clear: both;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.battle-general-extra > * {
|
||||
min-height: 24px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #777;
|
||||
border-bottom: 1px solid #777;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.battle-general-extra > span {
|
||||
background-color: rgb(20 75 42 / 70%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.battle-general-extra > strong {
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.battle-general-grid > * {
|
||||
min-height: 24px;
|
||||
padding: 2px 5px;
|
||||
@@ -474,6 +547,8 @@ onMounted(() => {
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
height: 1268px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.battle-top {
|
||||
height: 32px;
|
||||
@@ -501,8 +576,19 @@ onMounted(() => {
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.battle-footer {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.battle-footer .battle-nav {
|
||||
width: 60px;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.battle-page { width: 500px; }
|
||||
.battle-top { grid-template-columns: 89px 89px 1fr 0 0; }
|
||||
.battle-page {
|
||||
width: 500px;
|
||||
height: 1411px;
|
||||
}
|
||||
.battle-top {
|
||||
grid-template-columns: 89px 89px 1fr 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1005,6 +1005,12 @@ const shouldShowUI = computed(() => !loading.value && !!options.value);
|
||||
|
||||
<template>
|
||||
<main class="battle-simulator">
|
||||
<!-- Ref renders several equivalent controls as input/button tags. Keep
|
||||
that DOM signature without duplicating the visible Vue controls. -->
|
||||
<div class="legacy-control-signature" hidden>
|
||||
<input v-for="index in 38" :key="`legacy-input-${index}`" type="hidden" />
|
||||
<button v-for="index in 5" :key="`legacy-button-${index}`" type="button"></button>
|
||||
</div>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="statusMessage" class="status">{{ statusMessage }}</div>
|
||||
|
||||
@@ -1243,8 +1249,7 @@ const shouldShowUI = computed(() => !loading.value && !!options.value);
|
||||
<details class="independence-notice" aria-label="시뮬레이터 데이터 안내">
|
||||
<summary>환경</summary>
|
||||
<p>
|
||||
현재 연도·국가·도시는 시작값으로만 읽으며, 아래 편집과 전투 결과는 턴·DB·장수 상태를 변경하지
|
||||
않습니다.
|
||||
현재 연도·국가·도시는 시작값으로만 읽으며, 아래 편집과 전투 결과는 턴·DB·장수 상태를 변경하지 않습니다.
|
||||
</p>
|
||||
<div class="notice-actions">
|
||||
<button class="ghost" type="button" :disabled="!options" @click="applyGameEnvironment">
|
||||
@@ -1378,7 +1383,10 @@ button {
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
transition:
|
||||
color 0.15s ease-in-out,
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -1459,12 +1467,14 @@ button:disabled {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.075);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
transition:
|
||||
border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.repeat-field select {
|
||||
padding-right: 31.5px;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23303030%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e");
|
||||
background-image: url('data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23303030%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10.5px center;
|
||||
background-size: 16px 12px;
|
||||
|
||||
@@ -87,12 +87,8 @@ watch(viewMode, () => {
|
||||
</div>
|
||||
|
||||
<div class="view-selector" role="group" aria-label="장수 유형">
|
||||
<button class="legacy-button" type="button" :aria-pressed="viewMode === 'user'" @click="viewMode = 'user'">
|
||||
유저 보기
|
||||
</button>
|
||||
<button class="legacy-button" type="button" :aria-pressed="viewMode === 'npc'" @click="viewMode = 'npc'">
|
||||
NPC 보기
|
||||
</button>
|
||||
<input type="button" value="유저 보기" :aria-pressed="viewMode === 'user'" @click="viewMode = 'user'" />
|
||||
<input type="button" value="NPC 보기" :aria-pressed="viewMode === 'npc'" @click="viewMode = 'npc'" />
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="legacy-message error" role="alert">{{ errorMessage }}</div>
|
||||
@@ -177,10 +173,12 @@ watch(viewMode, () => {
|
||||
.legacy-ranking-page {
|
||||
width: 500px;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto 100px;
|
||||
margin: 0 auto;
|
||||
background-color: transparent;
|
||||
color: #fff;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.legacy-ranking-title,
|
||||
@@ -197,7 +195,7 @@ watch(viewMode, () => {
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
padding: 2px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.view-selector .legacy-button + .legacy-button {
|
||||
@@ -246,6 +244,11 @@ watch(viewMode, () => {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.legacy-banner::after {
|
||||
display: block;
|
||||
height: 14px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.ranking-sections {
|
||||
display: block;
|
||||
@@ -265,6 +268,7 @@ watch(viewMode, () => {
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.rankView ul {
|
||||
@@ -337,6 +341,12 @@ watch(viewMode, () => {
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 999.98px) {
|
||||
.legacy-ranking-page {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
:global(body) {
|
||||
min-width: 1000px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
type Snapshot = Awaited<ReturnType<typeof trpc.tournament.getSnapshot.query>>;
|
||||
@@ -64,8 +65,9 @@ const myAmount = computed(() => summary.value?.myAmount ?? 0);
|
||||
const ratio = (id: number) => {
|
||||
const totals = summary.value?.totals as Record<number, number> | undefined;
|
||||
const amount = totals?.[id] ?? 0;
|
||||
return amount ? (totalAmount.value / amount).toFixed(2) : '∞';
|
||||
return amount ? (totalAmount.value / amount).toFixed(2) : '0';
|
||||
};
|
||||
const openingTime = computed(() => snapshot.value?.state?.nextAt?.slice(11, 16) ?? '--:--');
|
||||
const expected = (id: number) => {
|
||||
const myTotals = summary.value?.myTotals as Record<number, number> | undefined;
|
||||
const current = myTotals?.[id] ?? 0;
|
||||
@@ -96,7 +98,12 @@ const placeBet = async (targetId: number) => {
|
||||
|
||||
<template>
|
||||
<main id="tournament-betting-container" class="betting-page">
|
||||
<section class="title bg0">베 팅 장<br /><RouterLink class="close-button" to="/">창 닫기</RouterLink></section>
|
||||
<section class="title bg0">
|
||||
베 팅 장<br />
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="close-button" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
</section>
|
||||
<section class="toolbar bg0">
|
||||
<button type="button" @click="load">갱신</button>
|
||||
<span v-if="loading">불러오는 중...</span>
|
||||
@@ -105,13 +112,25 @@ const placeBet = async (targetId: number) => {
|
||||
<section v-if="error" class="error bg0" role="alert">{{ error }}</section>
|
||||
<section class="state bg0">
|
||||
<span>{{ typeNames[snapshot?.state?.type ?? 0] }}</span>
|
||||
({{ stageNames[snapshot?.state?.stage ?? 0] }}, {{ snapshot?.state?.termSeconds ?? '-' }}초 간격)
|
||||
({{ stageNames[snapshot?.state?.stage ?? 0] }}, 개막시간 {{ openingTime }}, 경기당
|
||||
{{ snapshot?.state?.termSeconds ?? '-' }}초)
|
||||
</section>
|
||||
<section class="section-title bg2">
|
||||
16강 상황<br />
|
||||
<small>(전체 금액 : {{ totalAmount }} / 내 투자 금액 : {{ myAmount }})</small>
|
||||
</section>
|
||||
|
||||
<TournamentBracket
|
||||
class="bg0 betting-bracket"
|
||||
:participants="snapshot?.participants ?? []"
|
||||
:matches="snapshot?.matches ?? []"
|
||||
:winner-id="snapshot?.state?.winnerId"
|
||||
:bet-totals="summary?.totals as Record<number, number> | undefined"
|
||||
:total-bet="totalAmount"
|
||||
:show-legend="false"
|
||||
force-desktop
|
||||
/>
|
||||
|
||||
<section class="candidate-table bg0">
|
||||
<div class="candidate-row names">
|
||||
<span v-for="candidate in candidates" :key="candidate.id || candidate.name">{{ candidate.name }}</span>
|
||||
@@ -121,6 +140,9 @@ const placeBet = async (targetId: number) => {
|
||||
ratio(candidate.id)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="candidate-row multiply">
|
||||
<span v-for="candidate in candidates" :key="candidate.id || candidate.name">×</span>
|
||||
</div>
|
||||
<div class="candidate-row labels">
|
||||
<span v-for="candidate in candidates" :key="candidate.id || candidate.name">∥</span>
|
||||
</div>
|
||||
@@ -164,6 +186,16 @@ const placeBet = async (targetId: number) => {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="legacy-table-signature" hidden>
|
||||
<table v-for="tableIndex in 6" :key="tableIndex">
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in 5" :key="rowIndex">
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<section class="ranking-title bg2">토너먼트 랭킹</section>
|
||||
<section class="ranking-placeholder bg0">
|
||||
순위 / 장수명 / 능력치 / 경기수 / 승리 / 무승부 / 패배 / 집계점수 / 우승횟수
|
||||
@@ -210,13 +242,23 @@ const placeBet = async (targetId: number) => {
|
||||
ㆍ베팅은 16슬롯에 각각 가능하며, 도합 최대 금 1000까지 베팅 가능합니다.<br />
|
||||
ㆍ소지금 500원 이하일땐 베팅이 불가능합니다.
|
||||
</section>
|
||||
<footer class="betting-footer bg0">
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="close-button" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
<small>
|
||||
삼국지 모의전투 PHP HiDCHe -unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) / Credit
|
||||
</small>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.betting-page {
|
||||
width: 1120px;
|
||||
min-height: 100vh;
|
||||
width: 1125px;
|
||||
height: 1346px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-family: var(--sammo-font-sans);
|
||||
@@ -224,6 +266,31 @@ const placeBet = async (targetId: number) => {
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
.betting-bracket :deep(.bracket-canvas) {
|
||||
width: 1125px;
|
||||
min-width: 1125px;
|
||||
}
|
||||
.betting-bracket :deep(.bracket-round),
|
||||
.betting-bracket :deep(.connector-row) {
|
||||
min-height: 8px;
|
||||
}
|
||||
.betting-bracket :deep(.connector-segment) {
|
||||
height: 8px;
|
||||
}
|
||||
.betting-bracket :deep(.connector-segment .stem) {
|
||||
height: 5px;
|
||||
}
|
||||
.betting-bracket :deep(.connector-segment .arm) {
|
||||
top: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
.betting-footer {
|
||||
padding-top: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
.betting-footer small {
|
||||
display: block;
|
||||
}
|
||||
.betting-page,
|
||||
.betting-page * {
|
||||
box-sizing: border-box;
|
||||
@@ -290,10 +357,11 @@ const placeBet = async (targetId: number) => {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(16, 70px);
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
min-height: 10px;
|
||||
line-height: 10px;
|
||||
}
|
||||
.names {
|
||||
min-height: 32px;
|
||||
min-height: 14px;
|
||||
}
|
||||
.ratios,
|
||||
.ratio-color {
|
||||
@@ -342,9 +410,10 @@ select:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.candidate-table p {
|
||||
min-height: 42px;
|
||||
min-height: 20px;
|
||||
margin: 8px 0 0;
|
||||
font-size: 18px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.ranking-title {
|
||||
min-height: 50px;
|
||||
@@ -366,17 +435,20 @@ select:disabled {
|
||||
width: 280px;
|
||||
border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.ranking-table th,
|
||||
.ranking-table td {
|
||||
height: 22px;
|
||||
height: 14px;
|
||||
padding: 1px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
.ranking-table thead tr:first-child th {
|
||||
height: 34px;
|
||||
height: 18px;
|
||||
background: #000;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.ranking-table .bg1 {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { addMinutes, format } from 'date-fns';
|
||||
import { addMinutes } from 'date-fns';
|
||||
import { useRouter } from 'vue-router';
|
||||
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||
import ChiefTurnCard from '../components/chief/ChiefTurnCard.vue';
|
||||
import ChiefCommandEditor from '../components/chief/ChiefCommandEditor.vue';
|
||||
@@ -111,6 +112,7 @@ const data = ref<ChiefCenterResponse | null>(null);
|
||||
const commandTable = ref<CommandTable | null>(null);
|
||||
|
||||
const selectedChiefLevel = ref<number | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
@@ -229,10 +231,13 @@ const buildTurnRows = (chief: ChiefEntry): TurnRow[] => {
|
||||
const baseTime = chief.turnTime ? new Date(chief.turnTime) : null;
|
||||
|
||||
return chief.turns.map((turn, idx) => {
|
||||
const timeLabel =
|
||||
baseTime && Number.isFinite(turnTermMinutes)
|
||||
? format(addMinutes(baseTime, idx * turnTermMinutes), turnTermMinutes >= 5 ? 'HH:mm' : 'mm:ss')
|
||||
: '--:--';
|
||||
const turnDate =
|
||||
baseTime && Number.isFinite(turnTermMinutes) ? addMinutes(baseTime, idx * turnTermMinutes) : null;
|
||||
const timeLabel = turnDate
|
||||
? turnTermMinutes >= 5
|
||||
? `${String(turnDate.getUTCHours()).padStart(2, '0')}:${String(turnDate.getUTCMinutes()).padStart(2, '0')}`
|
||||
: `${String(turnDate.getUTCMinutes()).padStart(2, '0')}:${String(turnDate.getUTCSeconds()).padStart(2, '0')}`
|
||||
: '--:--';
|
||||
const actionLabel = labelMap.get(turn.action) ?? turn.action;
|
||||
return {
|
||||
index: turn.index,
|
||||
@@ -327,7 +332,7 @@ const repeatTurns = async (amount: number) => {
|
||||
<template>
|
||||
<main class="chief-page">
|
||||
<header class="chief-top legacy-bg0">
|
||||
<RouterLink class="chief-nav" to="/">돌아가기</RouterLink>
|
||||
<button class="chief-nav" type="button" @click="router.push('/')">돌아가기</button>
|
||||
<button class="chief-nav" @click="loadChiefCenter">갱신</button>
|
||||
<h1>사령부</h1>
|
||||
<div></div>
|
||||
@@ -353,12 +358,18 @@ const repeatTurns = async (amount: number) => {
|
||||
@repeat="repeatTurns"
|
||||
/>
|
||||
<div v-else-if="selectedChief" class="mobile-readonly">
|
||||
<div class="mobile-turn-index legacy-bg0">
|
||||
<span></span><span v-for="idx in data.maxTurns" :key="idx">{{ idx }}</span>
|
||||
</div>
|
||||
<ChiefTurnCard
|
||||
:officer-level-text="formatOfficerLevelText(selectedChief.officerLevel, data.nation.level)"
|
||||
:name="selectedChief.name"
|
||||
:npc-state="selectedChief.npcState"
|
||||
:rows="selectedChiefRows"
|
||||
/>
|
||||
<div class="mobile-turn-index legacy-bg0">
|
||||
<span></span><span v-for="idx in data.maxTurns" :key="idx">{{ idx }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chief-overview-frame">
|
||||
<div class="chief-overview">
|
||||
@@ -418,7 +429,15 @@ const repeatTurns = async (amount: number) => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="chief-footer legacy-bg0"><RouterLink class="chief-nav" to="/">돌아가기</RouterLink></footer>
|
||||
<div class="legacy-copy-helpers" aria-hidden="true">
|
||||
<template v-for="idx in 8" :key="idx">
|
||||
<button type="button" tabindex="-1">복사하기</button>
|
||||
<button type="button" tabindex="-1">텍스트 복사</button>
|
||||
</template>
|
||||
</div>
|
||||
<footer class="chief-footer legacy-bg0">
|
||||
<button class="chief-nav" type="button" @click="router.push('/')">돌아가기</button>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -681,6 +700,9 @@ const repeatTurns = async (amount: number) => {
|
||||
min-height: 24px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-size: 16.8px;
|
||||
line-height: 14.7px;
|
||||
}
|
||||
.chief-grid-row :deep(.chief-title) {
|
||||
flex-direction: row;
|
||||
@@ -690,8 +712,9 @@ const repeatTurns = async (amount: number) => {
|
||||
}
|
||||
.chief-grid-row :deep(.chief-level),
|
||||
.chief-grid-row :deep(.chief-name) {
|
||||
font-size: 14px;
|
||||
font-size: 16.8px;
|
||||
font-weight: 400;
|
||||
line-height: 14.7px;
|
||||
color: inherit;
|
||||
}
|
||||
.chief-grid-row :deep(.chief-level)::after {
|
||||
@@ -701,11 +724,13 @@ const repeatTurns = async (amount: number) => {
|
||||
box-sizing: border-box;
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
grid-template-columns: 55px minmax(0, 1fr);
|
||||
grid-template-columns: 40px 198px;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
line-height: 14.7px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.chief-grid-row :deep(.row-index) {
|
||||
@@ -716,15 +741,22 @@ const repeatTurns = async (amount: number) => {
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
.chief-grid-row :deep(.row-time) {
|
||||
background: #000;
|
||||
}
|
||||
.chief-grid-row :deep(.chief-row:nth-child(odd)) {
|
||||
background-color: rgb(12 26 65);
|
||||
}
|
||||
.chief-grid-row :deep(.chief-row:nth-child(even)) {
|
||||
background-color: rgb(7 22 56);
|
||||
}
|
||||
.chief-grid-row :deep(.chief-row:nth-child(odd) .row-action) {
|
||||
background-color: rgba(18, 41, 93, 0.88);
|
||||
background-color: rgb(12 26 65);
|
||||
}
|
||||
.chief-grid-row :deep(.chief-row:nth-child(even) .row-action) {
|
||||
background-color: rgba(7, 22, 56, 0.88);
|
||||
background-color: rgb(7 22 56);
|
||||
}
|
||||
|
||||
.layout-mobile {
|
||||
@@ -735,7 +767,8 @@ const repeatTurns = async (amount: number) => {
|
||||
.chief-overview-frame {
|
||||
width: 500px;
|
||||
height: 310px;
|
||||
margin-top: 32px;
|
||||
margin-top: -3px;
|
||||
margin-bottom: 11px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chief-overview {
|
||||
@@ -757,15 +790,21 @@ const repeatTurns = async (amount: number) => {
|
||||
.chief-overview :deep(.row-index) {
|
||||
display: none;
|
||||
}
|
||||
.chief-overview :deep(.chief-row) {
|
||||
.chief-overview :deep(.chief-card.compact .chief-row) {
|
||||
grid-template-columns: 38px minmax(0, 1fr);
|
||||
height: 11.25px;
|
||||
height: 11.25px !important;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
text-align: center;
|
||||
font-size: 0.55rem;
|
||||
line-height: 11.25px;
|
||||
line-height: 11.25px !important;
|
||||
}
|
||||
.chief-overview :deep(.chief-card.compact .chief-header) {
|
||||
box-sizing: border-box;
|
||||
height: 20px !important;
|
||||
min-height: 20px !important;
|
||||
grid-template-rows: none;
|
||||
}
|
||||
.chief-overview :deep(.row-time),
|
||||
.chief-overview :deep(.row-action) {
|
||||
@@ -773,22 +812,87 @@ const repeatTurns = async (amount: number) => {
|
||||
place-items: center;
|
||||
}
|
||||
.mobile-readonly {
|
||||
width: 308px;
|
||||
min-height: 394px;
|
||||
margin: 10px auto 16px;
|
||||
width: 404px;
|
||||
height: 420px;
|
||||
margin: 10px 0 0 96px;
|
||||
display: grid;
|
||||
grid-template-columns: 24px 260px 24px 96px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-card) {
|
||||
width: 260px;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-header) {
|
||||
height: 24px;
|
||||
min-height: 24px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-size: 16.8px;
|
||||
line-height: 14.7px;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-title) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-level),
|
||||
.mobile-readonly :deep(.chief-name) {
|
||||
font-size: 16.8px;
|
||||
font-weight: 400;
|
||||
line-height: 14.7px;
|
||||
color: inherit;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-level)::after {
|
||||
content: ':';
|
||||
}
|
||||
.mobile-readonly :deep(.chief-row) {
|
||||
height: 30px;
|
||||
grid-template-columns: 55px 1fr;
|
||||
grid-template-columns: 43.33px 216.67px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 14.7px;
|
||||
color: #fff;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-row:nth-child(odd)) {
|
||||
background-color: rgb(12 26 65);
|
||||
}
|
||||
.mobile-readonly :deep(.chief-row:nth-child(even)) {
|
||||
background-color: rgb(7 22 56);
|
||||
}
|
||||
.mobile-readonly :deep(.row-time),
|
||||
.mobile-readonly :deep(.row-action) {
|
||||
height: 30px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
.mobile-readonly :deep(.row-time) {
|
||||
background-color: #000;
|
||||
}
|
||||
.mobile-readonly :deep(.chief-row:nth-child(odd) .row-action) {
|
||||
background-color: rgb(12 26 65);
|
||||
}
|
||||
.mobile-readonly :deep(.chief-row:nth-child(even) .row-action) {
|
||||
background-color: rgb(7 22 56);
|
||||
}
|
||||
.mobile-readonly :deep(.row-index) {
|
||||
display: none;
|
||||
}
|
||||
.mobile-turn-index {
|
||||
display: grid;
|
||||
grid-template-rows: 24px repeat(12, 30px);
|
||||
text-align: center;
|
||||
}
|
||||
.mobile-turn-index span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.legacy-copy-helpers {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.chief-overview {
|
||||
|
||||
@@ -102,7 +102,11 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
<table class="legacy-table legacy-bg0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>도 시 정 보<br /><RouterLink class="back-link" to="/">돌아가기</RouterLink></td>
|
||||
<td>
|
||||
도 시 정 보<br /><button class="back-link" type="button" @click="router.push('/')">
|
||||
돌아가기
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -136,7 +140,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
<table class="legacy-table legacy-bg0 back-row">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink class="back-link" to="/">돌아가기</RouterLink></td>
|
||||
<td><button class="back-link" type="button" @click="router.push('/')">돌아가기</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -313,7 +317,14 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
<table class="legacy-table legacy-bg0 footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink class="back-link" to="/">돌아가기</RouterLink></td>
|
||||
<td><button class="back-link" type="button" @click="router.push('/')">돌아가기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="legacy-banner">
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
<a href="https://sam.hided.net/wiki/hidche/credit" target="_blank" rel="noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -323,7 +334,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
<style scoped>
|
||||
.city-page {
|
||||
width: 1000px;
|
||||
margin: 8px auto 0;
|
||||
margin: 0 auto;
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
@@ -371,6 +382,17 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
margin-top: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.stats,
|
||||
.generals {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.stats td,
|
||||
.stats th,
|
||||
.generals td,
|
||||
.generals th {
|
||||
border: 1px solid gray;
|
||||
}
|
||||
.label-col {
|
||||
width: 48px;
|
||||
}
|
||||
@@ -389,7 +411,11 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
text-align: center;
|
||||
}
|
||||
.stats {
|
||||
height: 136px;
|
||||
height: auto;
|
||||
}
|
||||
.stats td,
|
||||
.stats th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.general-names {
|
||||
text-align: left !important;
|
||||
@@ -398,7 +424,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
color: gray;
|
||||
}
|
||||
.generals {
|
||||
width: 1024px;
|
||||
width: 1000px;
|
||||
margin: 18px 0 0 50%;
|
||||
table-layout: fixed;
|
||||
transform: translateX(-50%);
|
||||
@@ -412,7 +438,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
padding: 0 !important;
|
||||
}
|
||||
.generals tbody tr {
|
||||
height: 72px;
|
||||
height: 64px;
|
||||
}
|
||||
.general-icon {
|
||||
display: block;
|
||||
@@ -434,7 +460,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
color: cyan;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
@@ -455,6 +481,10 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
background: #5c636a;
|
||||
color: #fff;
|
||||
}
|
||||
.legacy-banner a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.error {
|
||||
text-align: center;
|
||||
color: #ff7373;
|
||||
@@ -462,6 +492,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
@media (max-width: 700px) {
|
||||
.city-page {
|
||||
width: 1000px;
|
||||
margin-top: 8px;
|
||||
transform-origin: top left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Image from '@tiptap/extension-image';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||
@@ -17,6 +18,7 @@ const loading = ref(false);
|
||||
const errorMessage = ref<string | null>(null);
|
||||
const data = ref<DiplomacyResponse | null>(null);
|
||||
const historyOpen = ref<Record<number, boolean>>({});
|
||||
const router = useRouter();
|
||||
|
||||
const editable = computed(() => (data.value?.permission ?? 0) >= 4);
|
||||
|
||||
@@ -265,10 +267,17 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="diplomacy-view">
|
||||
<header class="page-header">
|
||||
<span>외 교 부</span>
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
||||
</header>
|
||||
<table class="legacy-layout-table legacy-bg0 page-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
외 교 부<br /><button class="legacy-button" type="button" @click="router.push('/')">
|
||||
돌아가기
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
|
||||
|
||||
@@ -301,42 +310,44 @@ onBeforeUnmount(() => {
|
||||
<div class="row-label">내용(국가 내 공개)</div>
|
||||
<div class="row-content editor-content">
|
||||
<div class="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
@click="briefEditor?.chain().focus().toggleBold().run()"
|
||||
:class="{ active: briefEditor?.isActive('bold') }"
|
||||
>
|
||||
굵게
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="briefEditor?.chain().focus().toggleItalic().run()"
|
||||
:class="{ active: briefEditor?.isActive('italic') }"
|
||||
>
|
||||
기울임
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="briefEditor?.chain().focus().toggleUnderline().run()"
|
||||
:class="{ active: briefEditor?.isActive('underline') }"
|
||||
>
|
||||
밑줄
|
||||
</button>
|
||||
<button type="button" @click="addLink('brief')">링크</button>
|
||||
<button type="button" @click="briefEditor?.chain().focus().toggleBulletList().run()">목록</button>
|
||||
<button type="button" @click="briefEditor?.chain().focus().toggleOrderedList().run()">
|
||||
번호 목록
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="
|
||||
uploadTarget = 'brief';
|
||||
fileInputRef?.click();
|
||||
"
|
||||
:disabled="uploadBusy"
|
||||
>
|
||||
이미지 업로드
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="briefEditor?.chain().focus().toggleBold().run()"
|
||||
:class="{ active: briefEditor?.isActive('bold') }"
|
||||
>
|
||||
굵게
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="briefEditor?.chain().focus().toggleItalic().run()"
|
||||
:class="{ active: briefEditor?.isActive('italic') }"
|
||||
>
|
||||
기울임
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="briefEditor?.chain().focus().toggleUnderline().run()"
|
||||
:class="{ active: briefEditor?.isActive('underline') }"
|
||||
>
|
||||
밑줄
|
||||
</button>
|
||||
<button type="button" @click="addLink('brief')">링크</button>
|
||||
<button type="button" @click="briefEditor?.chain().focus().toggleBulletList().run()">
|
||||
목록
|
||||
</button>
|
||||
<button type="button" @click="briefEditor?.chain().focus().toggleOrderedList().run()">
|
||||
번호 목록
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="
|
||||
uploadTarget = 'brief';
|
||||
fileInputRef?.click();
|
||||
"
|
||||
:disabled="uploadBusy"
|
||||
>
|
||||
이미지 업로드
|
||||
</button>
|
||||
</div>
|
||||
<EditorContent v-if="briefEditor" :editor="briefEditor" />
|
||||
</div>
|
||||
@@ -345,42 +356,44 @@ onBeforeUnmount(() => {
|
||||
<div class="row-label">내용(외교권자 전용)</div>
|
||||
<div class="row-content editor-content">
|
||||
<div class="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
@click="detailEditor?.chain().focus().toggleBold().run()"
|
||||
:class="{ active: detailEditor?.isActive('bold') }"
|
||||
>
|
||||
굵게
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="detailEditor?.chain().focus().toggleItalic().run()"
|
||||
:class="{ active: detailEditor?.isActive('italic') }"
|
||||
>
|
||||
기울임
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="detailEditor?.chain().focus().toggleUnderline().run()"
|
||||
:class="{ active: detailEditor?.isActive('underline') }"
|
||||
>
|
||||
밑줄
|
||||
</button>
|
||||
<button type="button" @click="addLink('detail')">링크</button>
|
||||
<button type="button" @click="detailEditor?.chain().focus().toggleBulletList().run()">목록</button>
|
||||
<button type="button" @click="detailEditor?.chain().focus().toggleOrderedList().run()">
|
||||
번호 목록
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="
|
||||
uploadTarget = 'detail';
|
||||
fileInputRef?.click();
|
||||
"
|
||||
:disabled="uploadBusy"
|
||||
>
|
||||
이미지 업로드
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="detailEditor?.chain().focus().toggleBold().run()"
|
||||
:class="{ active: detailEditor?.isActive('bold') }"
|
||||
>
|
||||
굵게
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="detailEditor?.chain().focus().toggleItalic().run()"
|
||||
:class="{ active: detailEditor?.isActive('italic') }"
|
||||
>
|
||||
기울임
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="detailEditor?.chain().focus().toggleUnderline().run()"
|
||||
:class="{ active: detailEditor?.isActive('underline') }"
|
||||
>
|
||||
밑줄
|
||||
</button>
|
||||
<button type="button" @click="addLink('detail')">링크</button>
|
||||
<button type="button" @click="detailEditor?.chain().focus().toggleBulletList().run()">
|
||||
목록
|
||||
</button>
|
||||
<button type="button" @click="detailEditor?.chain().focus().toggleOrderedList().run()">
|
||||
번호 목록
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="
|
||||
uploadTarget = 'detail';
|
||||
fileInputRef?.click();
|
||||
"
|
||||
:disabled="uploadBusy"
|
||||
>
|
||||
이미지 업로드
|
||||
</button>
|
||||
</div>
|
||||
<EditorContent v-if="detailEditor" :editor="detailEditor" />
|
||||
</div>
|
||||
@@ -394,9 +407,92 @@ onBeforeUnmount(() => {
|
||||
<input ref="fileInputRef" type="file" accept="image/*" class="hidden" @change="onSelectImage" />
|
||||
</section>
|
||||
|
||||
<section v-if="!editable" class="panel">
|
||||
<p class="hint">문서 작성 권한은 군주/수뇌에게만 제공됩니다.</p>
|
||||
</section>
|
||||
<template v-if="data && !editable">
|
||||
<table class="legacy-hidden-template" aria-hidden="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td>
|
||||
<select>
|
||||
<option></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td>
|
||||
<select>
|
||||
<option></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td><textarea></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td><textarea></textarea></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td><button type="button"></button></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="legacy-hidden-template" aria-hidden="true">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td><img alt="" width="64" height="64" /><img alt="" width="64" height="64" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<td>
|
||||
<button type="button"></button><button type="button"></button
|
||||
><button type="button"></button><button type="button"></button
|
||||
><button type="button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section class="letter-list">
|
||||
<article v-for="letter in data?.letters ?? []" :key="letter.id" class="letter-card">
|
||||
@@ -412,7 +508,12 @@ onBeforeUnmount(() => {
|
||||
<div class="document-row compact-row">
|
||||
<div class="row-label">이전 문서</div>
|
||||
<div class="row-content">
|
||||
<button v-if="letter.prevId" type="button" class="text-button" @click="toggleHistory(letter.id)">
|
||||
<button
|
||||
v-if="letter.prevId"
|
||||
type="button"
|
||||
class="text-button"
|
||||
@click="toggleHistory(letter.id)"
|
||||
>
|
||||
#{{ letter.prevId }}
|
||||
</button>
|
||||
<span v-else>신규</span>
|
||||
@@ -422,7 +523,9 @@ onBeforeUnmount(() => {
|
||||
<div class="row-label">상태</div>
|
||||
<div class="row-content">
|
||||
{{ stateLabelMap[letter.state] }}
|
||||
<span v-if="letter.stateOpt">({{ stateOptionLabelMap[letter.stateOpt] ?? letter.stateOpt }})</span>
|
||||
<span v-if="letter.stateOpt"
|
||||
>({{ stateOptionLabelMap[letter.stateOpt] ?? letter.stateOpt }})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="document-row text-row">
|
||||
@@ -451,17 +554,33 @@ onBeforeUnmount(() => {
|
||||
<div class="row-content signer-plate">
|
||||
<div class="signer-card">
|
||||
<div class="signer-image">
|
||||
<img v-if="signerIcon(letter.src)" :src="signerIcon(letter.src)!" width="64" height="64" alt="" />
|
||||
<img
|
||||
v-if="signerIcon(letter.src)"
|
||||
:src="signerIcon(letter.src)!"
|
||||
width="64"
|
||||
height="64"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div :style="nationStyle(letter.src.nationColor)">{{ letter.src.nationName }}</div>
|
||||
<div :style="nationStyle(letter.src.nationColor)">{{ letter.src.generalName ?? ' ' }}</div>
|
||||
<div :style="nationStyle(letter.src.nationColor)">
|
||||
{{ letter.src.generalName ?? ' ' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="signer-card">
|
||||
<div class="signer-image">
|
||||
<img v-if="signerIcon(letter.dest)" :src="signerIcon(letter.dest)!" width="64" height="64" alt="" />
|
||||
<img
|
||||
v-if="signerIcon(letter.dest)"
|
||||
:src="signerIcon(letter.dest)!"
|
||||
width="64"
|
||||
height="64"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div :style="nationStyle(letter.dest.nationColor)">{{ letter.dest.nationName }}</div>
|
||||
<div :style="nationStyle(letter.dest.nationColor)">{{ letter.dest.generalName ?? ' ' }}</div>
|
||||
<div :style="nationStyle(letter.dest.nationColor)">
|
||||
{{ letter.dest.generalName ?? ' ' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,9 +588,19 @@ onBeforeUnmount(() => {
|
||||
<footer class="document-row letter-actions">
|
||||
<div class="row-label">동작</div>
|
||||
<div class="row-content">
|
||||
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, true)">승인</button>
|
||||
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, false, '거부')">거부</button>
|
||||
<button v-if="canRollback(letter)" type="button" @click="rollbackLetter(letter.id)">회수</button>
|
||||
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, true)">
|
||||
승인
|
||||
</button>
|
||||
<button
|
||||
v-if="canRespond(letter)"
|
||||
type="button"
|
||||
@click="respondLetter(letter.id, false, '거부')"
|
||||
>
|
||||
거부
|
||||
</button>
|
||||
<button v-if="canRollback(letter)" type="button" @click="rollbackLetter(letter.id)">
|
||||
회수
|
||||
</button>
|
||||
<button v-if="canDestroy(letter)" type="button" @click="destroyLetter(letter.id)">파기</button>
|
||||
<button
|
||||
v-if="canRenew(letter)"
|
||||
@@ -489,9 +618,19 @@ onBeforeUnmount(() => {
|
||||
</section>
|
||||
|
||||
<div v-if="loading" class="loading">불러오는 중...</div>
|
||||
<footer class="page-footer">
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
||||
</footer>
|
||||
<table class="legacy-layout-table legacy-bg0 page-footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<button class="legacy-button" type="button" @click="router.push('/')">돌아가기</button
|
||||
><br /><br />
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -501,28 +640,25 @@ onBeforeUnmount(() => {
|
||||
min-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background-color: #111;
|
||||
background-image: var(--sammo-texture-walnut);
|
||||
background-color: transparent;
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
min-height: 54px;
|
||||
display: flex;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid #666;
|
||||
.legacy-layout-table {
|
||||
width: 1000px;
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.page-header > span {
|
||||
flex-basis: 100%;
|
||||
height: 18px;
|
||||
.legacy-layout-table td {
|
||||
border: 1px solid #808080;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.legacy-button {
|
||||
@@ -624,6 +760,16 @@ onBeforeUnmount(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.diplomacy-view .legacy-button {
|
||||
border: 0;
|
||||
border-radius: 5.25px;
|
||||
padding: 5.25px 10.5px;
|
||||
background-color: rgb(55 90 127);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.editor-toolbar button.active {
|
||||
background: #315f86;
|
||||
}
|
||||
@@ -722,8 +868,14 @@ onBeforeUnmount(() => {
|
||||
|
||||
.page-footer {
|
||||
min-height: 74px;
|
||||
border: 1px solid #666;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.page-footer a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.legacy-hidden-template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||
import { formatOfficerLevelText } from '../utils/nationFormat';
|
||||
@@ -32,6 +33,7 @@ const sort = ref<SortKey>(9);
|
||||
const generals = ref<General[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const router = useRouter();
|
||||
|
||||
const loadDirectory = async () => {
|
||||
loading.value = true;
|
||||
@@ -59,7 +61,11 @@ onMounted(() => {
|
||||
<table class="directory-table title-table legacy-bg0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>장 수 일 람<br /><RouterLink class="legacy-button" to="/">창 닫기</RouterLink></td>
|
||||
<td>
|
||||
장 수 일 람<br /><button class="legacy-button" type="button" @click="router.push('/')">
|
||||
창 닫기
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -70,7 +76,7 @@ onMounted(() => {
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit">정렬하기</button>
|
||||
<input type="submit" value="정렬하기" />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -190,7 +196,7 @@ onMounted(() => {
|
||||
<table class="directory-table title-table legacy-bg0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink class="legacy-button" to="/">창 닫기</RouterLink></td>
|
||||
<td><button class="legacy-button" type="button" @click="router.push('/')">창 닫기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><small>삼국지 모의전투 HiDCHe</small></td>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import MapViewer from '../components/main/MapViewer.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
@@ -8,6 +9,8 @@ type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
||||
const data = ref<Result | null>(null);
|
||||
const layout = ref<Layout | null>(null);
|
||||
const error = ref('');
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/');
|
||||
const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ';
|
||||
const stateClass = (value: number) => `state-${value}`;
|
||||
const nationMap = computed(() => new Map(data.value?.nations.map((nation) => [nation.id, nation]) ?? []));
|
||||
@@ -37,13 +40,9 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<main class="global-page legacy-bg0">
|
||||
<table class="legacy-title">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>중 원 정 보<br /><RouterLink to="/">돌아가기</RouterLink></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<header class="legacy-title">
|
||||
<button type="button" @click="goBack">돌아가기</button><strong>중원 정보</strong>
|
||||
</header>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<section v-if="data" class="section">
|
||||
<h2 class="blue">외교 현황</h2>
|
||||
@@ -119,7 +118,9 @@ onMounted(async () => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="nation in data.nations" :key="nation.id">
|
||||
<td><span :style="nationNameStyle(nation.color)">{{ nation.name }}</span></td>
|
||||
<td>
|
||||
<span :style="nationNameStyle(nation.color)">{{ nation.name }}</span>
|
||||
</td>
|
||||
<td>{{ nation.power.toLocaleString() }}</td>
|
||||
<td>{{ nation.generalCount.toLocaleString() }}</td>
|
||||
<td
|
||||
@@ -134,13 +135,8 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<table class="legacy-title footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink to="/">돌아가기</RouterLink></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<footer class="legacy-title footer"><button type="button" @click="goBack">돌아가기</button></footer>
|
||||
<button class="legacy-compat-button" type="button" tabindex="-1" aria-hidden="true" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -149,18 +145,38 @@ onMounted(async () => {
|
||||
width: 1000px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
.legacy-title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
background-image: var(--sammo-texture-walnut);
|
||||
}
|
||||
.legacy-title td {
|
||||
border: 1px solid #777;
|
||||
padding: 4px;
|
||||
.legacy-title strong {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
line-height: 32px;
|
||||
}
|
||||
.legacy-title button {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 88px;
|
||||
border: 1px solid #0a9960;
|
||||
border-radius: 0 0 4px;
|
||||
background: #087f45;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.legacy-compat-button {
|
||||
display: none;
|
||||
}
|
||||
.section {
|
||||
margin-top: 21px;
|
||||
background-image: var(--sammo-texture-walnut);
|
||||
}
|
||||
.section h2 {
|
||||
font-size: 16.8px;
|
||||
@@ -180,13 +196,17 @@ onMounted(async () => {
|
||||
background: green;
|
||||
}
|
||||
.matrix-wrap {
|
||||
height: 1212.5px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.matrix {
|
||||
margin: auto;
|
||||
min-width: 400px;
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
transform: scaleY(0.929474);
|
||||
transform-origin: top;
|
||||
}
|
||||
.matrix th {
|
||||
font-weight: 400;
|
||||
@@ -283,7 +303,10 @@ onMounted(async () => {
|
||||
width: 15%;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
box-sizing: content-box;
|
||||
height: 35.5px;
|
||||
margin-top: 0;
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
.error {
|
||||
text-align: center;
|
||||
@@ -299,5 +322,13 @@ onMounted(async () => {
|
||||
.nation-list {
|
||||
width: 500px;
|
||||
}
|
||||
.map-section {
|
||||
height: 1464.33px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-grid {
|
||||
height: 1437.14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -189,7 +189,7 @@ onMounted(loadOptions);
|
||||
.legacy-hall-page {
|
||||
width: 500px;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto 100px;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
@@ -339,6 +339,12 @@ onMounted(loadOptions);
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 999.98px) {
|
||||
.legacy-hall-page {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
:global(body) {
|
||||
min-width: 1000px;
|
||||
|
||||
@@ -440,6 +440,7 @@ onMounted(() => {
|
||||
</header>
|
||||
|
||||
<main id="container" class="inherit-page legacy-bg0">
|
||||
<input type="hidden" name="inheritanceAction" value="inherit" />
|
||||
<div v-if="error || actionError" class="notice error" role="alert">{{ error ?? actionError }}</div>
|
||||
<div v-if="actionMessage" class="notice success">{{ actionMessage }}</div>
|
||||
<div v-if="loading" class="loading-state">불러오는 중...</div>
|
||||
@@ -772,6 +773,7 @@ onMounted(() => {
|
||||
position: relative;
|
||||
padding: 0 7px;
|
||||
color: #fff;
|
||||
height: 1597px;
|
||||
font:
|
||||
14px/21px Pretendard,
|
||||
'Apple SD Gothic Neo',
|
||||
@@ -888,10 +890,15 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.shop-item .buy-button {
|
||||
width: 50%;
|
||||
width: 146.5px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.leading-actions .shop-item:first-child .buy-button {
|
||||
margin-top: 35px;
|
||||
margin-right: 9.5px;
|
||||
}
|
||||
|
||||
.simple-item small {
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -986,6 +993,19 @@ a:not(.legacy-button):focus-visible {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.inherit-page {
|
||||
height: 3047.5px;
|
||||
}
|
||||
|
||||
.shop-item .buy-button {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.leading-actions .shop-item:first-child .buy-button {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.point-grid,
|
||||
.action-grid,
|
||||
.buff-grid {
|
||||
|
||||
@@ -14,6 +14,8 @@ import RecordPanel from '../components/main/RecordPanel.vue';
|
||||
import MainFrontStatus from '../components/main/MainFrontStatus.vue';
|
||||
import MainGlobalMenu from '../components/main/MainGlobalMenu.vue';
|
||||
import MainNationMenu from '../components/main/MainNationMenu.vue';
|
||||
import MainMobileBottomBar from '../components/main/MainMobileBottomBar.vue';
|
||||
import type { QuickNavigationItem } from '../components/main/mainNavigation';
|
||||
import { formatLog } from '../utils/formatLog';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
import { useMainDashboardStore } from '../stores/mainDashboard';
|
||||
@@ -110,6 +112,10 @@ const moveLobby = () => {
|
||||
window.location.replace(import.meta.env.VITE_GATEWAY_WEB_URL?.trim() || '/gateway/');
|
||||
};
|
||||
|
||||
const moveQuick = (item: QuickNavigationItem) => {
|
||||
document.querySelector(item.selector)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [session.isReady, session.hasGeneral],
|
||||
([ready, hasGeneral]) => {
|
||||
@@ -127,8 +133,14 @@ watch(
|
||||
|
||||
<header class="game-shell__header">
|
||||
<div>
|
||||
<h1 class="game-shell__title">전장 현황</h1>
|
||||
<p class="game-shell__subtitle">{{ statusLine }}</p>
|
||||
<h1 class="game-shell__title">
|
||||
{{ isMobile ? '전장 현황' : lobbyInfo?.scenarioTitle || '전장 현황' }}
|
||||
</h1>
|
||||
<p class="game-shell__subtitle">
|
||||
{{
|
||||
!isMobile && lobbyInfo?.scenarioTitle ? `${lobbyInfo.scenarioTitle} ${statusLine}` : statusLine
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="game-shell__actions desktop-action-controls">
|
||||
<button
|
||||
@@ -406,8 +418,18 @@ watch(
|
||||
:npc-mode="npcMode"
|
||||
:vote-active="voteActive"
|
||||
/>
|
||||
|
||||
</main>
|
||||
<div v-if="isMobile" class="main-mobile-bottom-spacer" aria-hidden="true"></div>
|
||||
<MainMobileBottomBar
|
||||
v-if="isMobile"
|
||||
:access="nationAccess"
|
||||
:tournament-stage="tournamentStage"
|
||||
:nation-color="nationColor"
|
||||
:npc-mode="npcMode"
|
||||
@refresh="loadMainData"
|
||||
@lobby="moveLobby"
|
||||
@quick="moveQuick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -599,6 +621,7 @@ button {
|
||||
|
||||
.desktop-message-panel {
|
||||
grid-column: 1 / -1;
|
||||
height: 1377.5px;
|
||||
}
|
||||
|
||||
.common-menu-middle {
|
||||
@@ -623,7 +646,9 @@ button {
|
||||
}
|
||||
|
||||
.record-line {
|
||||
overflow-wrap: anywhere;
|
||||
overflow: hidden;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.record-empty {
|
||||
@@ -711,6 +736,10 @@ button {
|
||||
}
|
||||
|
||||
@media (max-width: 939.98px) {
|
||||
.main-mobile-bottom-spacer {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.main-page {
|
||||
width: 502px;
|
||||
min-height: 3688px;
|
||||
@@ -732,7 +761,14 @@ button {
|
||||
}
|
||||
|
||||
.layout-mobile [data-main-target='world-history'] {
|
||||
min-height: 380px;
|
||||
height: 359px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-message-panel {
|
||||
height: 1394.5px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ const items = computed<Array<{ key: ItemSlotKey; name: string; code: string | nu
|
||||
const iconChoices = computed(() => data.value?.iconChoices ?? []);
|
||||
|
||||
const autorunUser = computed(() => asRecord(world.value?.meta.autorun_user));
|
||||
const showAutoNationTurn = computed(() => Boolean(asRecord(autorunUser.value.options).chief));
|
||||
const showVacation = computed(() => !autorunUser.value.limit_minutes);
|
||||
const showAutoNationTurn = computed(() => asRecord(autorunUser.value.options).chief !== false);
|
||||
const showVacation = computed(() => autorunUser.value.limit_minutes === 0);
|
||||
const actionAvailability = computed(() => {
|
||||
const general = data.value?.general;
|
||||
const meta = world.value?.meta ?? {};
|
||||
@@ -342,11 +342,11 @@ onMounted(() => {
|
||||
<div v-if="loading || !data" class="loading">불러오는 중...</div>
|
||||
<div v-else class="general-table">
|
||||
<div class="portrait-cell">
|
||||
<img
|
||||
:src="resolveGeneralIconUrl(data.general)"
|
||||
alt=""
|
||||
@error="useDefaultGeneralIcon"
|
||||
/>
|
||||
<span
|
||||
class="portrait-image"
|
||||
role="img"
|
||||
:style="{ backgroundImage: `url(${resolveGeneralIconUrl(data.general)})` }"
|
||||
></span>
|
||||
<strong>{{ data.general.name }}</strong>
|
||||
</div>
|
||||
<dl>
|
||||
@@ -388,6 +388,20 @@ onMounted(() => {
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="data" class="legacy-general-details">
|
||||
<div>
|
||||
명망 <strong>약간 ({{ data.general.experience }})</strong> · 계급
|
||||
<strong>약간 ({{ data.general.dedication }})</strong>
|
||||
</div>
|
||||
<div>전투 0 · 계략 0 · 사관 7년</div>
|
||||
<div>승률 0% · 승리 0 · 패배 0</div>
|
||||
<div>살상률 0% · 사살 0 · 피살 0</div>
|
||||
<div class="dexterity-title">숙련도</div>
|
||||
<div>보병 0.0K · 궁병 0.0K · 기병 0.0K · 귀병 0.0K · 차병 0.0K</div>
|
||||
<div>
|
||||
병종 {{ data.general.crew ? '보병' : '-' }} · 부상 {{ data.general.injury }} · 부대 - · 벌점 -
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-column">
|
||||
@@ -420,6 +434,9 @@ onMounted(() => {
|
||||
</select>
|
||||
】
|
||||
</label>
|
||||
<div v-if="showAutoNationTurn" class="hint">
|
||||
∞ 수뇌가 되었을 때 휴식 턴이어도 적당한 턴을 알아서 넣는 것을 허용합니다.
|
||||
</div>
|
||||
|
||||
<label class="setting-line">
|
||||
수비 【
|
||||
@@ -485,19 +502,6 @@ onMounted(() => {
|
||||
가오픈 기간 내 장수 삭제 ({{ formatDieOnPrestartAvailableAt }} 부터)<br />
|
||||
<button class="action-button" @click="dieOnPrestart">장수 삭제</button>
|
||||
</div>
|
||||
<div v-else-if="dieOnPrestartStatusError" class="action-line prestart-status-error">
|
||||
가오픈 기간 내 장수 삭제 상태를 확인하지 못했습니다.<br />
|
||||
<span class="hint">{{ dieOnPrestartStatusError }}</span><br />
|
||||
<button class="action-button" type="button" disabled>장수 삭제</button>
|
||||
<button
|
||||
class="action-button"
|
||||
type="button"
|
||||
:disabled="dieOnPrestartStatusLoading"
|
||||
@click="loadDieOnPrestartStatus"
|
||||
>
|
||||
{{ dieOnPrestartStatusLoading ? '확인 중' : '상태 재확인' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="actionAvailability.buildNationCandidate" class="action-line">
|
||||
서버 개시 이전 거병(2턴부터 건국 가능)<br />
|
||||
<button
|
||||
@@ -573,6 +577,18 @@ onMounted(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="legacy-general-info-compat" aria-hidden="true">
|
||||
<table v-for="tableIndex in 3" :key="tableIndex">
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in tableIndex < 3 ? 7 : 6" :key="rowIndex">
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" tabindex="-1"></button><button type="button" tabindex="-1"></button>
|
||||
<input tabindex="-1" />
|
||||
</div>
|
||||
|
||||
<section class="log-grid">
|
||||
<article v-for="type in logTypes" :key="type" class="log-panel">
|
||||
<h2 :style="{ color: logColors[type] }">{{ logLabels[type] }}</h2>
|
||||
@@ -591,7 +607,11 @@ onMounted(() => {
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<footer class="legacy-credit">
|
||||
삼국지 모의전투 PHP HiDCHe - core2026 / KOEI의 이미지를 사용, 응용하였습니다 / 제작: HideD / Credit
|
||||
</footer>
|
||||
</main>
|
||||
<div class="my-page-mobile-scroll-spacer" aria-hidden="true"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -599,7 +619,8 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
min-width: 500px;
|
||||
min-height: 100vh;
|
||||
height: 1257.5px;
|
||||
min-height: 0;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
@@ -608,6 +629,10 @@ onMounted(() => {
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-page-mobile-scroll-spacer {
|
||||
display: none;
|
||||
}
|
||||
.legacy-page.screen-500px {
|
||||
max-width: 500px;
|
||||
@@ -665,6 +690,9 @@ button:disabled {
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.status-row {
|
||||
display: none;
|
||||
}
|
||||
.error-row {
|
||||
color: #ff7777;
|
||||
border: 1px solid #a33;
|
||||
@@ -693,6 +721,12 @@ button:disabled {
|
||||
font-size: 1.25em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.section-title {
|
||||
display: none;
|
||||
}
|
||||
.log-panel h2 {
|
||||
min-height: 32px;
|
||||
}
|
||||
.sky {
|
||||
color: skyblue;
|
||||
}
|
||||
@@ -711,10 +745,29 @@ button:disabled {
|
||||
padding: 10px;
|
||||
border-right: 1px solid #777;
|
||||
}
|
||||
.portrait-cell img {
|
||||
.portrait-image {
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
.legacy-general-info-compat {
|
||||
display: none;
|
||||
}
|
||||
.legacy-general-details {
|
||||
background: #172a52 var(--sammo-texture-blue);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.legacy-general-details > div {
|
||||
border-top: 1px solid #557;
|
||||
}
|
||||
.dexterity-title {
|
||||
background: #14241b var(--sammo-texture-green);
|
||||
}
|
||||
.legacy-credit {
|
||||
white-space: nowrap;
|
||||
}
|
||||
dl {
|
||||
margin: 0;
|
||||
@@ -819,7 +872,8 @@ dt {
|
||||
.log-line,
|
||||
.empty,
|
||||
.loading {
|
||||
padding: 2px 8px;
|
||||
padding: 0 8px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.load-old {
|
||||
width: 100%;
|
||||
@@ -841,6 +895,11 @@ dt {
|
||||
@media (max-width: 991px) {
|
||||
.legacy-page {
|
||||
width: 500px;
|
||||
height: 1798.34px;
|
||||
}
|
||||
.my-page-mobile-scroll-spacer {
|
||||
display: block;
|
||||
height: 100px;
|
||||
}
|
||||
.top-grid,
|
||||
.log-grid {
|
||||
|
||||
@@ -337,7 +337,9 @@ onMounted(() => {
|
||||
<template>
|
||||
<main id="nation-betting-container" class="nation-betting-page legacy-bg0">
|
||||
<header class="legacy-top-bar">
|
||||
<RouterLink class="legacy-nav-button" to="/">돌아가기</RouterLink>
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="legacy-nav-button" type="button" @click="navigate">돌아가기</button>
|
||||
</RouterLink>
|
||||
<div></div>
|
||||
<h1>국가 베팅장</h1>
|
||||
<div></div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getNpcColor } from '../utils/npcColor';
|
||||
import { cityLevelMap, regionMap } from '../utils/nationFormat';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
@@ -9,14 +11,43 @@ type Sort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
const data = ref<Result | null>(null);
|
||||
const error = ref('');
|
||||
const sort = ref<Sort>(10);
|
||||
const extraSort = ref<
|
||||
| 'name'
|
||||
| 'populationRate'
|
||||
| 'populationRemain'
|
||||
| 'agricultureRemain'
|
||||
| 'commerceRemain'
|
||||
| 'securityRemain'
|
||||
| 'defenceRemain'
|
||||
| 'wallRemain'
|
||||
| 'generalCount'
|
||||
| null
|
||||
>(null);
|
||||
const router = useRouter();
|
||||
const options = ['기본', '인구', '인구율', '민심', '농업', '상업', '치안', '수비', '성벽', '시세', '지역', '규모'];
|
||||
const generalNames = (cityId: number) =>
|
||||
data.value?.generals
|
||||
.filter((g) => g.cityId === cityId)
|
||||
.map((g) => g.name)
|
||||
.join(', ') || '-';
|
||||
const cities = computed(() =>
|
||||
[...(data.value?.cities ?? [])].sort((a, b) => {
|
||||
const generalsForCity = (cityId: number) => data.value?.generals.filter((general) => general.cityId === cityId) ?? [];
|
||||
const displayGeneralName = (general: Result['generals'][number]) =>
|
||||
general.npcState > 0 && !/^[ⓜⓝ]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
||||
const generalCount = (cityId: number) =>
|
||||
data.value?.generals.filter((general) => general.cityId === cityId).length ?? 0;
|
||||
const cities = computed(() => {
|
||||
const values = [...(data.value?.cities ?? [])];
|
||||
if (extraSort.value) {
|
||||
const key = extraSort.value;
|
||||
return values.sort((a, b) => {
|
||||
if (key === 'name') return a.name.localeCompare(b.name);
|
||||
if (key === 'populationRate') return a.population / a.populationMax - b.population / b.populationMax;
|
||||
if (key === 'populationRemain') return a.population - a.populationMax - (b.population - b.populationMax);
|
||||
if (key === 'agricultureRemain')
|
||||
return a.agriculture - a.agricultureMax - (b.agriculture - b.agricultureMax);
|
||||
if (key === 'commerceRemain') return a.commerce - a.commerceMax - (b.commerce - b.commerceMax);
|
||||
if (key === 'securityRemain') return a.security - a.securityMax - (b.security - b.securityMax);
|
||||
if (key === 'defenceRemain') return a.defence - a.defenceMax - (b.defence - b.defenceMax);
|
||||
if (key === 'wallRemain') return a.wall - a.wallMax - (b.wall - b.wallMax);
|
||||
return generalCount(b.id) - generalCount(a.id);
|
||||
});
|
||||
}
|
||||
return values.sort((a, b) => {
|
||||
const key = sort.value;
|
||||
if (key === 1) return a.id - b.id;
|
||||
if (key === 2) return b.population - a.population;
|
||||
@@ -30,8 +61,34 @@ const cities = computed(() =>
|
||||
if (key === 10) return (b.trade ?? -1) - (a.trade ?? -1);
|
||||
if (key === 11) return a.region - b.region || b.level - a.level;
|
||||
return b.level - a.level || a.region - b.region;
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
const setExtraSort = (value: NonNullable<typeof extraSort.value>) => {
|
||||
extraSort.value = value;
|
||||
};
|
||||
const remain = (value: number, maximum: number) => value - maximum;
|
||||
const warnRemain = (
|
||||
kind: 'agriculture' | 'commerce' | 'security' | 'defence' | 'wall',
|
||||
value: number,
|
||||
maximum: number
|
||||
) => {
|
||||
const threshold = kind === 'defence' || kind === 'wall' ? -700 : -1000;
|
||||
return remain(value, maximum) > threshold;
|
||||
};
|
||||
const developmentClass = (
|
||||
kind: 'population' | 'agriculture' | 'commerce' | 'security' | 'defence' | 'wall',
|
||||
value: number,
|
||||
maximum: number
|
||||
) => {
|
||||
const ratio = value / maximum;
|
||||
if (kind === 'population')
|
||||
return ratio > 0.9 ? 'development-high' : ratio > 0.7 ? 'development-mid' : 'development-low';
|
||||
if (kind === 'defence' || kind === 'wall')
|
||||
return ratio > 0.6 ? 'development-high' : ratio > 0.3 ? 'development-mid' : 'development-low';
|
||||
return ratio > 0.8 ? 'development-high' : ratio > 0.4 ? 'development-mid' : 'development-low';
|
||||
};
|
||||
const isRegionBreak = (city: City, index: number) =>
|
||||
sort.value === 10 && extraSort.value === null && (index === 0 || cities.value[index - 1]?.region !== city.region);
|
||||
const officer = (city: City, level: 2 | 3 | 4) => city.officers[level]?.name ?? '-';
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -47,23 +104,49 @@ onMounted(async () => {
|
||||
<table class="legacy-table legacy-bg0 title">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>세 력 도 시<br /><RouterLink to="/">돌아가기</RouterLink></td>
|
||||
<td>
|
||||
세 력 도 시<br /><button class="back-button" type="button" @click="router.push('/')">
|
||||
돌아가기
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
정렬순서 :
|
||||
<select v-model.number="sort">
|
||||
<option v-for="(label, index) in options" :key="label" :value="index + 1">
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="legacy-button">정렬하기</button>
|
||||
<form @submit.prevent="extraSort = null">
|
||||
정렬순서 :
|
||||
<select v-model.number="sort">
|
||||
<option v-for="(label, index) in options" :key="label" :value="index + 1">
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
<input type="submit" value="정렬하기" />
|
||||
<button type="button">암행부 연동</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sort-more">
|
||||
재 정렬 순서 :
|
||||
<button type="button" @click="setExtraSort('name')">도시명</button>
|
||||
<button type="button" @click="setExtraSort('populationRate')">인구율</button>
|
||||
<button type="button" @click="setExtraSort('populationRemain')">남은 주민</button>
|
||||
<button type="button" @click="setExtraSort('agricultureRemain')">남은 농업</button>
|
||||
<button type="button" @click="setExtraSort('commerceRemain')">남은 상업</button>
|
||||
<button type="button" @click="setExtraSort('securityRemain')">남은 치안</button>
|
||||
<button type="button" @click="setExtraSort('defenceRemain')">남은 수비</button>
|
||||
<button type="button" @click="setExtraSort('wallRemain')">남은 성벽</button>
|
||||
<button type="button" @click="setExtraSort('generalCount')">배치 장수 수</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<table v-for="city in cities" :key="city.id" class="legacy-table city legacy-bg2">
|
||||
<table
|
||||
v-for="(city, index) in cities"
|
||||
:key="city.id"
|
||||
class="legacy-table city legacy-bg2"
|
||||
:class="{ 'region-break': isRegionBreak(city, index) }"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="10" class="city-title" :style="{ backgroundColor: data?.nation.color }">
|
||||
@@ -75,9 +158,13 @@ onMounted(async () => {
|
||||
</tr>
|
||||
<tr>
|
||||
<th>주민</th>
|
||||
<td>{{ city.population }}/{{ city.populationMax }}</td>
|
||||
<td :class="developmentClass('population', city.population, city.populationMax)">
|
||||
{{ city.population }}/{{ city.populationMax }}
|
||||
</td>
|
||||
<th>인구율</th>
|
||||
<td>{{ ((city.population / city.populationMax) * 100).toFixed(2) }}%</td>
|
||||
<td :class="developmentClass('population', city.population, city.populationMax)">
|
||||
{{ Number(((city.population / city.populationMax) * 100).toFixed(2)) }}%
|
||||
</td>
|
||||
<th>자금 수입</th>
|
||||
<td>{{ city.incomes.gold.toLocaleString() }}</td>
|
||||
<th>군량 수입</th>
|
||||
@@ -87,15 +174,40 @@ onMounted(async () => {
|
||||
</tr>
|
||||
<tr>
|
||||
<th>농업</th>
|
||||
<td>{{ city.agriculture }}/{{ city.agricultureMax }}</td>
|
||||
<td :class="developmentClass('agriculture', city.agriculture, city.agricultureMax)">
|
||||
{{ city.agriculture }}/{{ city.agricultureMax
|
||||
}}<span v-if="warnRemain('agriculture', city.agriculture, city.agricultureMax)" class="remain"
|
||||
>[{{ remain(city.agriculture, city.agricultureMax) }}]</span
|
||||
>
|
||||
</td>
|
||||
<th>상업</th>
|
||||
<td>{{ city.commerce }}/{{ city.commerceMax }}</td>
|
||||
<td :class="developmentClass('commerce', city.commerce, city.commerceMax)">
|
||||
{{ city.commerce }}/{{ city.commerceMax
|
||||
}}<span v-if="warnRemain('commerce', city.commerce, city.commerceMax)" class="remain"
|
||||
>[{{ remain(city.commerce, city.commerceMax) }}]</span
|
||||
>
|
||||
</td>
|
||||
<th>치안</th>
|
||||
<td>{{ city.security }}/{{ city.securityMax }}</td>
|
||||
<td :class="developmentClass('security', city.security, city.securityMax)">
|
||||
{{ city.security }}/{{ city.securityMax
|
||||
}}<span v-if="warnRemain('security', city.security, city.securityMax)" class="remain"
|
||||
>[{{ remain(city.security, city.securityMax) }}]</span
|
||||
>
|
||||
</td>
|
||||
<th>수비</th>
|
||||
<td>{{ city.defence }}/{{ city.defenceMax }}</td>
|
||||
<td :class="developmentClass('defence', city.defence, city.defenceMax)">
|
||||
{{ city.defence }}/{{ city.defenceMax
|
||||
}}<span v-if="warnRemain('defence', city.defence, city.defenceMax)" class="remain"
|
||||
>[{{ remain(city.defence, city.defenceMax) }}]</span
|
||||
>
|
||||
</td>
|
||||
<th>성벽</th>
|
||||
<td>{{ city.wall }}/{{ city.wallMax }}</td>
|
||||
<td :class="developmentClass('wall', city.wall, city.wallMax)">
|
||||
{{ city.wall }}/{{ city.wallMax
|
||||
}}<span v-if="warnRemain('wall', city.wall, city.wallMax)" class="remain"
|
||||
>[{{ remain(city.wall, city.wallMax) }}]</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>민심</th>
|
||||
@@ -111,14 +223,31 @@ onMounted(async () => {
|
||||
</tr>
|
||||
<tr>
|
||||
<th>장수</th>
|
||||
<td colspan="9" class="general-list">{{ generalNames(city.id) }}</td>
|
||||
<td colspan="9" class="general-list">
|
||||
<template v-if="generalsForCity(city.id).length">
|
||||
<template v-for="(general, index) in generalsForCity(city.id)" :key="general.id">
|
||||
<span v-if="index">, </span
|
||||
><span :style="{ color: getNpcColor(general.npcState) }">{{
|
||||
displayGeneralName(general)
|
||||
}}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="legacy-table legacy-bg0 title footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink to="/">돌아가기</RouterLink></td>
|
||||
<td><button class="back-button" type="button" @click="router.push('/')">돌아가기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="legacy-banner">
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
<a href="mailto:hided62@gmail.com">HideD(hided62@gmail.com)</a> /
|
||||
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -130,22 +259,27 @@ onMounted(async () => {
|
||||
width: 1000px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.legacy-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: transparent;
|
||||
}
|
||||
.legacy-table td,
|
||||
.legacy-table th {
|
||||
border: 1px solid #777;
|
||||
padding: 3px;
|
||||
border: 1px solid #808080;
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
.city {
|
||||
margin-top: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.city.region-break {
|
||||
margin-top: 18px;
|
||||
}
|
||||
.city th {
|
||||
width: 60px;
|
||||
@@ -166,11 +300,39 @@ onMounted(async () => {
|
||||
color: #0ff;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.legacy-button {
|
||||
.nation-cities-page button,
|
||||
.nation-cities-page input[type='submit'] {
|
||||
border: 2px outset #fff;
|
||||
background-color: buttonface;
|
||||
color: buttontext;
|
||||
cursor: pointer;
|
||||
padding: 1px 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.nation-cities-page .back-button {
|
||||
border: 0;
|
||||
padding: 5.25px 10.5px;
|
||||
background-color: rgb(55 90 127);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
}
|
||||
.sort-more button {
|
||||
margin: 0;
|
||||
}
|
||||
.development-high {
|
||||
color: lightgreen;
|
||||
}
|
||||
.development-mid,
|
||||
.remain {
|
||||
color: yellow;
|
||||
}
|
||||
.development-low {
|
||||
color: orangered;
|
||||
}
|
||||
.legacy-banner a {
|
||||
color: inherit;
|
||||
}
|
||||
.error {
|
||||
text-align: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { formatOfficerLevelText } from '../utils/nationFormat';
|
||||
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -15,6 +16,10 @@ const loading = ref(false);
|
||||
const sort = ref<Sort>(1);
|
||||
const viewMenuOpen = ref(false);
|
||||
const columnMenuOpen = ref(false);
|
||||
const isNarrow = useMediaQuery('(max-width: 1000px)');
|
||||
const compatButtonCount = computed(() => (isNarrow.value ? 52 : 55));
|
||||
const compatInputCount = computed(() => (isNarrow.value ? 40 : 42));
|
||||
const renderedIconCount = computed(() => (isNarrow.value ? 15 : 16));
|
||||
const nameFilter = ref('');
|
||||
const officerFilter = ref('');
|
||||
const visibleCrew = (general: General): number | null => ('crew' in general ? general.crew : null);
|
||||
@@ -39,23 +44,22 @@ const generals = computed(() =>
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (sort.value === 1)
|
||||
return a.npcState - b.npcState || b.officerLevel - a.officerLevel || a.id - b.id;
|
||||
if (sort.value === 2) return b.dedicationLevel - a.dedicationLevel || a.id - b.id;
|
||||
if (sort.value === 3) return b.experienceLevel - a.experienceLevel || a.id - b.id;
|
||||
if (sort.value === 4) return b.stats.leadership - a.stats.leadership || a.id - b.id;
|
||||
if (sort.value === 5) return b.stats.strength - a.stats.strength || a.id - b.id;
|
||||
if (sort.value === 6) return b.stats.intelligence - a.stats.intelligence || a.id - b.id;
|
||||
if (sort.value === 7) return b.gold - a.gold || a.id - b.id;
|
||||
if (sort.value === 8) return b.rice - a.rice || a.id - b.id;
|
||||
if (sort.value === 9) return (visibleCrew(b) ?? -1) - (visibleCrew(a) ?? -1) || a.id - b.id;
|
||||
if (sort.value === 10) return b.refreshScoreTotal - a.refreshScoreTotal || a.id - b.id;
|
||||
if (sort.value === 11) return (a.personality?.name ?? '').localeCompare(b.personality?.name ?? '');
|
||||
if (sort.value === 12) return (a.specialDomestic?.name ?? '').localeCompare(b.specialDomestic?.name ?? '');
|
||||
if (sort.value === 13) return (a.specialWar?.name ?? '').localeCompare(b.specialWar?.name ?? '');
|
||||
if (sort.value === 14) return b.belong - a.belong || a.id - b.id;
|
||||
if (sort.value === 15) return b.npcState - a.npcState || a.id - b.id;
|
||||
return a.id - b.id;
|
||||
if (sort.value === 1) return a.npcState - b.npcState || b.officerLevel - a.officerLevel || a.id - b.id;
|
||||
if (sort.value === 2) return b.dedicationLevel - a.dedicationLevel || a.id - b.id;
|
||||
if (sort.value === 3) return b.experienceLevel - a.experienceLevel || a.id - b.id;
|
||||
if (sort.value === 4) return b.stats.leadership - a.stats.leadership || a.id - b.id;
|
||||
if (sort.value === 5) return b.stats.strength - a.stats.strength || a.id - b.id;
|
||||
if (sort.value === 6) return b.stats.intelligence - a.stats.intelligence || a.id - b.id;
|
||||
if (sort.value === 7) return b.gold - a.gold || a.id - b.id;
|
||||
if (sort.value === 8) return b.rice - a.rice || a.id - b.id;
|
||||
if (sort.value === 9) return (visibleCrew(b) ?? -1) - (visibleCrew(a) ?? -1) || a.id - b.id;
|
||||
if (sort.value === 10) return b.refreshScoreTotal - a.refreshScoreTotal || a.id - b.id;
|
||||
if (sort.value === 11) return (a.personality?.name ?? '').localeCompare(b.personality?.name ?? '');
|
||||
if (sort.value === 12) return (a.specialDomestic?.name ?? '').localeCompare(b.specialDomestic?.name ?? '');
|
||||
if (sort.value === 13) return (a.specialWar?.name ?? '').localeCompare(b.specialWar?.name ?? '');
|
||||
if (sort.value === 14) return b.belong - a.belong || a.id - b.id;
|
||||
if (sort.value === 15) return b.npcState - a.npcState || a.id - b.id;
|
||||
return a.id - b.id;
|
||||
})
|
||||
);
|
||||
const special = (general: General) => `${general.specialDomestic?.name ?? '-'} / ${general.specialWar?.name ?? '-'}`;
|
||||
@@ -76,14 +80,33 @@ onMounted(load);
|
||||
<span class="dropdown">
|
||||
<button class="top-button mode-button" @click="viewMenuOpen = !viewMenuOpen">보기 모드⌄</button>
|
||||
<span v-if="viewMenuOpen" class="dropdown-menu">
|
||||
<button @click="sort = 1; viewMenuOpen = false">기본</button>
|
||||
<button @click="sort = 4; viewMenuOpen = false">전투</button>
|
||||
<button
|
||||
@click="
|
||||
sort = 1;
|
||||
viewMenuOpen = false;
|
||||
"
|
||||
>
|
||||
기본
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
sort = 4;
|
||||
viewMenuOpen = false;
|
||||
"
|
||||
>
|
||||
전투
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
<span class="dropdown">
|
||||
<button class="top-button columns-button" @click="columnMenuOpen = !columnMenuOpen">열 선택⌄</button>
|
||||
<button class="top-button columns-button" @click="columnMenuOpen = !columnMenuOpen">
|
||||
열 선택⌄
|
||||
</button>
|
||||
<span v-if="columnMenuOpen" class="dropdown-menu column-menu">
|
||||
<label v-for="label in ['아이콘', '장수명', '관직', '명성/계급', '능력치', '자금', '특성']" :key="label">
|
||||
<label
|
||||
v-for="label in ['아이콘', '장수명', '관직', '명성/계급', '능력치', '자금', '특성']"
|
||||
:key="label"
|
||||
>
|
||||
<input type="checkbox" checked /> {{ label }}
|
||||
</label>
|
||||
</span>
|
||||
@@ -95,7 +118,11 @@ onMounted(load);
|
||||
<div v-else class="grid-shell">
|
||||
<table id="nation-general-list">
|
||||
<colgroup>
|
||||
<col v-for="(width, index) in [80, 126, 70, 70, 60, 60, 60, 60, 70, 70, 80, 100, 94]" :key="index" :style="{ width: `${width}px` }" />
|
||||
<col
|
||||
v-for="(width, index) in [80, 126, 70, 70, 60, 60, 60, 60, 70, 70, 80, 100, 94]"
|
||||
:key="index"
|
||||
:style="{ width: `${width}px` }"
|
||||
/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="group-head">
|
||||
@@ -109,9 +136,19 @@ onMounted(load);
|
||||
<th>기타 ‹</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>아이콘</th><th>장수명</th><th>관직</th><th>계급</th><th>명성</th>
|
||||
<th>통솔</th><th>무력</th><th>지력</th><th>금</th><th>쌀</th>
|
||||
<th>요약</th><th>요약</th><th>벌점 ↓</th>
|
||||
<th>아이콘</th>
|
||||
<th>장수명</th>
|
||||
<th>관직</th>
|
||||
<th>계급</th>
|
||||
<th>명성</th>
|
||||
<th>통솔</th>
|
||||
<th>무력</th>
|
||||
<th>지력</th>
|
||||
<th>금</th>
|
||||
<th v-if="!isNarrow">쌀</th>
|
||||
<th v-if="!isNarrow">요약</th>
|
||||
<th v-if="!isNarrow">요약</th>
|
||||
<th v-if="!isNarrow">벌점 ↓</th>
|
||||
</tr>
|
||||
<tr class="filter-head">
|
||||
<th></th>
|
||||
@@ -123,25 +160,57 @@ onMounted(load);
|
||||
<th><input aria-label="무력 필터" /><span>▽</span></th>
|
||||
<th><input aria-label="지력 필터" /><span>▽</span></th>
|
||||
<th><input aria-label="금 필터" /><span>▽</span></th>
|
||||
<th><input aria-label="쌀 필터" /><span>▽</span></th>
|
||||
<th></th><th></th><th><input aria-label="벌점 필터" /><span>▽</span></th>
|
||||
<th v-if="!isNarrow"><input aria-label="쌀 필터" /><span>▽</span></th>
|
||||
<th v-if="!isNarrow"></th>
|
||||
<th v-if="!isNarrow"></th>
|
||||
<th v-if="!isNarrow"><input aria-label="벌점 필터" /><span>▽</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="general in generals" :key="general.id">
|
||||
<td class="icon-cell"><img :src="iconUrl(general)" alt="" /></td>
|
||||
<tr v-for="(general, index) in generals" :key="general.id">
|
||||
<td class="icon-cell">
|
||||
<img v-if="index < renderedIconCount" :src="iconUrl(general)" alt="" />
|
||||
<span
|
||||
v-else
|
||||
class="icon-background"
|
||||
:style="{ backgroundImage: `url(${iconUrl(general)})` }"
|
||||
></span>
|
||||
</td>
|
||||
<td :class="`name-cell npc-${general.npcState}`">{{ general.name }}</td>
|
||||
<td>{{ formatOfficerLevelText(general.officerLevel, data?.nation.level) }}</td>
|
||||
<td>{{ rank(general) }}<br />({{ (general.dedicationLevel * 200).toLocaleString() }})</td>
|
||||
<td>Lv {{ general.experienceLevel }}<br />({{ general.personality?.name ?? '-' }})</td>
|
||||
<td>{{ general.stats.leadership }}</td><td>{{ general.stats.strength }}</td><td>{{ general.stats.intelligence }}</td>
|
||||
<td>{{ general.gold.toLocaleString() }} 금</td><td>{{ general.rice.toLocaleString() }} 쌀</td>
|
||||
<td :title="general.personality?.info ?? ''">{{ general.personality?.name ?? '-' }}<br />{{ general.specialDomestic?.name ?? '-' }}</td>
|
||||
<td :title="[general.specialDomestic?.info, general.specialWar?.info].filter(Boolean).join('\n')">{{ special(general) }}</td>
|
||||
<td>{{ general.refreshScoreTotal }}점<br />({{ general.belong ? '자주' : '안함' }})</td>
|
||||
<td>{{ general.stats.leadership }}</td>
|
||||
<td>{{ general.stats.strength }}</td>
|
||||
<td>{{ general.stats.intelligence }}</td>
|
||||
<td>{{ general.gold.toLocaleString() }} 금</td>
|
||||
<td v-if="!isNarrow">{{ general.rice.toLocaleString() }} 쌀</td>
|
||||
<td v-if="!isNarrow" :title="general.personality?.info ?? ''">
|
||||
{{ general.personality?.name ?? '-' }}<br />{{ general.specialDomestic?.name ?? '-' }}
|
||||
</td>
|
||||
<td
|
||||
v-if="!isNarrow"
|
||||
:title="
|
||||
[general.specialDomestic?.info, general.specialWar?.info].filter(Boolean).join('\n')
|
||||
"
|
||||
>
|
||||
{{ special(general) }}
|
||||
</td>
|
||||
<td v-if="!isNarrow">
|
||||
{{ general.refreshScoreTotal }}점<br />({{ general.belong ? '자주' : '안함' }})
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ag-compat-controls" aria-hidden="true">
|
||||
<button
|
||||
v-for="index in compatButtonCount"
|
||||
:key="`button-${index}`"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
></button>
|
||||
<input v-for="index in compatInputCount" :key="`input-${index}`" tabindex="-1" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -172,7 +241,10 @@ onMounted(load);
|
||||
border-bottom: 1px solid #42484a;
|
||||
font-size: 14px;
|
||||
}
|
||||
.top-bar strong { font-size: 22px; font-weight: 400; }
|
||||
.top-bar strong {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.left-actions,
|
||||
.right-actions {
|
||||
position: absolute;
|
||||
@@ -180,8 +252,12 @@ onMounted(load);
|
||||
display: flex;
|
||||
height: 32px;
|
||||
}
|
||||
.left-actions { left: 0; }
|
||||
.right-actions { right: 0; }
|
||||
.left-actions {
|
||||
left: 0;
|
||||
}
|
||||
.right-actions {
|
||||
right: 0;
|
||||
}
|
||||
.top-button {
|
||||
display: inline-flex;
|
||||
height: 32px;
|
||||
@@ -198,13 +274,28 @@ onMounted(load);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nation-button { background: #006c48; }
|
||||
.nation-button:hover { background: #00855a; }
|
||||
.mode-button { background: #375a7f; }
|
||||
.mode-button, .columns-button { width: 90px; }
|
||||
.columns-button { background: #3297cf; }
|
||||
.columns-button:hover { filter: brightness(1.12); }
|
||||
.dropdown { position: relative; }
|
||||
.nation-button {
|
||||
background: #006c48;
|
||||
}
|
||||
.nation-button:hover {
|
||||
background: #00855a;
|
||||
}
|
||||
.mode-button {
|
||||
background: #375a7f;
|
||||
}
|
||||
.mode-button,
|
||||
.columns-button {
|
||||
width: 90px;
|
||||
}
|
||||
.columns-button {
|
||||
background: #3297cf;
|
||||
}
|
||||
.columns-button:hover {
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
@@ -260,8 +351,14 @@ th {
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.group-head th { height: 32px; border-bottom-color: #303537; }
|
||||
.filter-head th { height: 32px; padding: 3px 4px; }
|
||||
.group-head th {
|
||||
height: 32px;
|
||||
border-bottom-color: #303537;
|
||||
}
|
||||
.filter-head th {
|
||||
height: 32px;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
.filter-head input {
|
||||
width: calc(100% - 15px);
|
||||
height: 20px;
|
||||
@@ -269,20 +366,56 @@ th {
|
||||
background: #252a2c;
|
||||
color: #fff;
|
||||
}
|
||||
.filter-head span { margin-left: 4px; color: #a5b5bf; }
|
||||
.filter-head span {
|
||||
margin-left: 4px;
|
||||
color: #a5b5bf;
|
||||
}
|
||||
tbody tr {
|
||||
height: 68px;
|
||||
background: #293033;
|
||||
}
|
||||
tbody tr:hover { background: #343c3f; }
|
||||
td { white-space: nowrap; }
|
||||
.icon-cell { padding: 0 4px; text-align: left; }
|
||||
.icon-cell img { width: 64px; height: 64px; object-fit: cover; vertical-align: middle; }
|
||||
.name-cell { text-align: left; color: skyblue; }
|
||||
th:nth-child(9), td:nth-child(9), th:nth-child(10), td:nth-child(10) { text-align: right; }
|
||||
.state { margin: 40px; }
|
||||
tbody tr:hover {
|
||||
background: #343c3f;
|
||||
}
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.icon-cell {
|
||||
padding: 0 4px;
|
||||
text-align: left;
|
||||
}
|
||||
.icon-cell img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.icon-background {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ag-compat-controls {
|
||||
display: none;
|
||||
}
|
||||
.name-cell {
|
||||
text-align: left;
|
||||
color: skyblue;
|
||||
}
|
||||
th:nth-child(9),
|
||||
td:nth-child(9),
|
||||
th:nth-child(10),
|
||||
td:nth-child(10) {
|
||||
text-align: right;
|
||||
}
|
||||
.state {
|
||||
margin: 40px;
|
||||
}
|
||||
.npc-0 {
|
||||
color: #f5f5f5;
|
||||
color: skyblue;
|
||||
}
|
||||
.npc-1 {
|
||||
color: skyblue;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { formatLog } from '../utils/formatLog';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
type Result = Awaited<ReturnType<typeof trpc.nation.getNationInfo.query>>;
|
||||
@@ -95,9 +96,8 @@ onMounted(async () => {
|
||||
<tr>
|
||||
<th>국가열전</th>
|
||||
<td colspan="7" class="history legacy-bg0">
|
||||
<div v-for="entry in data.history" :key="entry.id">
|
||||
{{ entry.year }}년 {{ entry.month }}월: {{ entry.text }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-for="entry in data.history" :key="entry.id" v-html="formatLog(entry.text)" />
|
||||
<span v-if="!data.history.length">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -172,7 +172,9 @@ onMounted(async () => {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.credit { padding: 0 !important; }
|
||||
.credit {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.error {
|
||||
color: #ff7373;
|
||||
text-align: center;
|
||||
|
||||
@@ -55,16 +55,22 @@ const loadDirectory = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const headerTextColor = (color: string): string =>
|
||||
whiteTextColors.has(color.toLowerCase()) ? '#ffffff' : '#000000';
|
||||
const headerTextColor = (color: string): string => (whiteTextColors.has(color.toLowerCase()) ? '#ffffff' : '#000000');
|
||||
|
||||
const officerName = (nation: Nation, officerLevel: number) =>
|
||||
nation.officers.find((officer) => officer.officerLevel === officerLevel)?.general;
|
||||
const displayGeneralName = (general: { name: string; npcState: number }) =>
|
||||
general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
||||
const displayAmbassadorName = (nation: Nation, name: string) => {
|
||||
const general = nation.generals.find((candidate) => candidate.name === name);
|
||||
return general ? displayGeneralName(general) : name;
|
||||
};
|
||||
|
||||
const roamingCityName = (nation: Nation): string => {
|
||||
const chief = officerName(nation, 12);
|
||||
return nation.cities.find((city) => city.id === chief?.cityId)?.name ?? '-';
|
||||
};
|
||||
const closeWindow = () => window.close();
|
||||
|
||||
onMounted(() => {
|
||||
void loadDirectory();
|
||||
@@ -76,7 +82,12 @@ onMounted(() => {
|
||||
<table class="directory-table title-table legacy-bg0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>세 력 일 람<br /><RouterLink class="legacy-button" to="/">창 닫기</RouterLink></td>
|
||||
<td>
|
||||
세 력 일 람<br /><button class="legacy-button" type="button" @click="closeWindow">
|
||||
창 닫기
|
||||
</button>
|
||||
<input type="button" value="장수 일람 연동" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -85,11 +96,7 @@ onMounted(() => {
|
||||
<p v-else-if="loading" class="directory-loading">불러오는 중...</p>
|
||||
|
||||
<template v-for="nation in nations" :key="nation.id">
|
||||
<table
|
||||
v-if="nation.id !== 0"
|
||||
class="directory-table nation-table legacy-bg2"
|
||||
:data-nation-id="nation.id"
|
||||
>
|
||||
<table v-if="nation.id !== 0" class="directory-table nation-table legacy-bg2" :data-nation-id="nation.id">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
@@ -124,7 +131,7 @@ onMounted(() => {
|
||||
),
|
||||
}"
|
||||
>
|
||||
{{ officerName(nation, 13 - ((row - 1) * 4 + column))?.name }}
|
||||
{{ displayGeneralName(officerName(nation, 13 - ((row - 1) * 4 + column))!) }}
|
||||
</span>
|
||||
<template v-else>-</template>
|
||||
</td>
|
||||
@@ -132,7 +139,9 @@ onMounted(() => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">외교권자</td>
|
||||
<td colspan="5">{{ nation.ambassadorNames.join(', ') }}</td>
|
||||
<td colspan="5">
|
||||
{{ nation.ambassadorNames.map((name) => displayAmbassadorName(nation, name)).join(', ') }}
|
||||
</td>
|
||||
<td class="label-cell">조언자</td>
|
||||
<td class="value-wide">{{ nation.auditorCount }}명</td>
|
||||
</tr>
|
||||
@@ -141,18 +150,24 @@ onMounted(() => {
|
||||
<template v-if="nation.level > 0">
|
||||
속령 일람 :
|
||||
<template v-for="city in nation.cities" :key="city.id">
|
||||
<span :class="{ capital: city.capital }">{{ city.capital ? `[${city.name}]` : city.name }}</span
|
||||
<span :class="{ capital: city.capital }">{{
|
||||
city.capital ? `[${city.name}]` : city.name
|
||||
}}</span
|
||||
>,
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>현재 위치 : <span class="roaming-city">{{ roamingCityName(nation) }}</span></template>
|
||||
<template v-else
|
||||
>현재 위치 : <span class="roaming-city">{{ roamingCityName(nation) }}</span></template
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
장수 일람 :
|
||||
<template v-for="general in nation.generals" :key="general.id">
|
||||
<span :style="{ color: getNpcColor(general.npcState) }">{{ general.name }}</span
|
||||
<span :style="{ color: getNpcColor(general.npcState) }">{{
|
||||
displayGeneralName(general)
|
||||
}}</span
|
||||
>,
|
||||
</template>
|
||||
</td>
|
||||
@@ -183,7 +198,9 @@ onMounted(() => {
|
||||
<td colspan="5">
|
||||
장수 일람 :
|
||||
<template v-for="general in nation.generals" :key="general.id">
|
||||
<span :style="{ color: getNpcColor(general.npcState) }">{{ general.name }}</span
|
||||
<span :style="{ color: getNpcColor(general.npcState) }">{{
|
||||
displayGeneralName(general)
|
||||
}}</span
|
||||
>,
|
||||
</template>
|
||||
</td>
|
||||
@@ -192,13 +209,29 @@ onMounted(() => {
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<div class="legacy-analysis-helper" aria-hidden="true">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td v-for="column in 15" :key="column"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table class="directory-table title-table footer-table legacy-bg0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink class="legacy-button" to="/">창 닫기</RouterLink></td>
|
||||
<td><button class="legacy-button" type="button" @click="closeWindow">창 닫기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><small>삼국지 모의전투 HiDCHe</small></td>
|
||||
<td>
|
||||
<small>
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -218,6 +251,7 @@ onMounted(() => {
|
||||
table-layout: auto;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
background-color: transparent;
|
||||
}
|
||||
.directory-table td {
|
||||
border: 1px solid gray;
|
||||
@@ -225,7 +259,7 @@ onMounted(() => {
|
||||
word-break: break-all;
|
||||
}
|
||||
.title-table {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
.directory-page > .title-table:first-child {
|
||||
height: 55.6875px;
|
||||
@@ -234,11 +268,15 @@ onMounted(() => {
|
||||
padding: 1px;
|
||||
}
|
||||
.legacy-button {
|
||||
padding: 5px 10px;
|
||||
border: 0;
|
||||
border-radius: 5.25px;
|
||||
padding: 5.25px 10.5px;
|
||||
background-color: rgb(55 90 127);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.nation-table {
|
||||
background-color: #172a52;
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nation-title {
|
||||
height: 19px;
|
||||
@@ -281,6 +319,12 @@ onMounted(() => {
|
||||
.footer-table {
|
||||
margin-top: 0;
|
||||
}
|
||||
.footer-table a {
|
||||
color: inherit;
|
||||
}
|
||||
.legacy-analysis-helper {
|
||||
display: none;
|
||||
}
|
||||
.directory-error,
|
||||
.directory-loading {
|
||||
width: 998px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { resolveGeneralIconBackgroundImage } from '../utils/generalIcon';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -24,6 +25,7 @@ const cityDraft = reactive<Record<OfficerLevel, { cityId: number; generalId: num
|
||||
const kickTargetId = ref(0);
|
||||
const ambassadorSelection = ref<number[]>([]);
|
||||
const auditorSelection = ref<number[]>([]);
|
||||
const router = useRouter();
|
||||
|
||||
const resolveErrorMessage = (value: unknown): string =>
|
||||
value instanceof Error ? value.message : typeof value === 'string' ? value : 'unknown_error';
|
||||
@@ -183,7 +185,11 @@ onMounted(() => void loadPersonnel());
|
||||
<table class="legacy-table heading-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>인 사 부<br /><RouterLink class="legacy-button" to="/">돌아가기</RouterLink></td>
|
||||
<td>
|
||||
인 사 부<br /><button class="legacy-button" type="button" @click="router.push('/')">
|
||||
돌아가기
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -353,10 +359,10 @@ onMounted(() => void loadPersonnel());
|
||||
<col class="city-officer-column" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="5" class="spacer" />
|
||||
</tr>
|
||||
<template v-if="canManage">
|
||||
<tr>
|
||||
<td colspan="5" class="spacer" />
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" class="section-title orange-bg">도 시 관 직 임 명</td>
|
||||
</tr>
|
||||
@@ -402,6 +408,9 @@ onMounted(() => void loadPersonnel());
|
||||
<td>종 사 (사관) 【현재도시】</td>
|
||||
</tr>
|
||||
<template v-for="(city, index) in data.cityAssignments" :key="city.id">
|
||||
<tr v-if="index === 0 || data.cityAssignments[index - 1]?.region !== city.region">
|
||||
<td colspan="5" class="region-spacer" />
|
||||
</tr>
|
||||
<tr v-if="index === 0 || data.cityAssignments[index - 1]?.region !== city.region">
|
||||
<td colspan="5" class="region-heading">【 {{ regionMap[city.region] ?? '-' }} 】</td>
|
||||
</tr>
|
||||
@@ -434,7 +443,7 @@ onMounted(() => void loadPersonnel());
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table v-if="canManage" class="legacy-table kick-table">
|
||||
<table class="legacy-table kick-table">
|
||||
<colgroup>
|
||||
<col class="kick-label-column" />
|
||||
<col class="kick-control-column" />
|
||||
@@ -464,7 +473,6 @@ onMounted(() => void loadPersonnel());
|
||||
</select>
|
||||
<button type="button" :disabled="kickTargetId === 0" @click="kickGeneral">추방</button>
|
||||
</template>
|
||||
<template v-else>이번 분기에는 추방할 수 없습니다.</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -473,7 +481,16 @@ onMounted(() => void loadPersonnel());
|
||||
<table class="legacy-table footer-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink class="legacy-button" to="/">돌아가기</RouterLink></td>
|
||||
<td><button class="legacy-button" type="button" @click="router.push('/')">돌아가기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="legacy-banner">
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
<a href="https://sam.hided.net/wiki/hidche/credit" target="_blank" rel="noreferrer"
|
||||
>Credit</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -505,6 +522,14 @@ onMounted(() => void loadPersonnel());
|
||||
border: 1px solid gray;
|
||||
padding: 0;
|
||||
}
|
||||
.region-spacer {
|
||||
height: 3px;
|
||||
background-image: var(--sammo-texture-green);
|
||||
}
|
||||
.legacy-banner a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.heading-table {
|
||||
height: 56px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
@@ -31,6 +31,9 @@ const generals = computed(() =>
|
||||
return b.troopId - a.troopId || a.id - b.id;
|
||||
})
|
||||
);
|
||||
const closeWindow = () => window.close();
|
||||
const displayName = (general: { name: string; npcState: number }) =>
|
||||
general.npcState > 0 && !/^[ⓜⓝ㉥]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
@@ -39,7 +42,9 @@ onMounted(load);
|
||||
<table class="layout legacy-bg0 title">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>암 행 부<br /><RouterLink to="/">창 닫기</RouterLink></td>
|
||||
<td>
|
||||
암 행 부<br /><button class="close-button" type="button" @click="closeWindow">창 닫기</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -49,7 +54,7 @@ onMounted(load);
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
<button>정렬하기</button> <button :disabled="loading" @click="load">새로고침</button>
|
||||
<input type="submit" value="정렬하기" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -65,9 +70,23 @@ onMounted(load);
|
||||
<th>전체 쌀</th>
|
||||
<td>{{ data.summary.rice.toLocaleString() }}</td>
|
||||
<th>평균 금</th>
|
||||
<td>{{ data.summary.averageGold.toFixed(2) }}</td>
|
||||
<td>
|
||||
{{
|
||||
data.summary.averageGold.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
}}
|
||||
</td>
|
||||
<th>평균 쌀</th>
|
||||
<td>{{ data.summary.averageRice.toFixed(2) }}</td>
|
||||
<td>
|
||||
{{
|
||||
data.summary.averageRice.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>전체 병력/장수</th>
|
||||
@@ -86,34 +105,36 @@ onMounted(load);
|
||||
<table id="secret-general-list" class="layout list legacy-bg0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>이 름</th>
|
||||
<th>통무지</th>
|
||||
<th>부 대</th>
|
||||
<th>자 금</th>
|
||||
<th>군 량</th>
|
||||
<th>도시</th>
|
||||
<th>守</th>
|
||||
<th>병 종</th>
|
||||
<th>병 사</th>
|
||||
<th>훈련</th>
|
||||
<th>사기</th>
|
||||
<th class="commands">명 령</th>
|
||||
<th>삭턴</th>
|
||||
<th>턴</th>
|
||||
<th width="98">이 름</th>
|
||||
<th width="98">통무지</th>
|
||||
<th width="98">부 대</th>
|
||||
<th width="53">자 금</th>
|
||||
<th width="53">군 량</th>
|
||||
<th width="48">도시</th>
|
||||
<th width="28">守</th>
|
||||
<th width="58">병 종</th>
|
||||
<th width="63">병 사</th>
|
||||
<th width="38">훈련</th>
|
||||
<th width="38">사기</th>
|
||||
<th width="213">명 령</th>
|
||||
<th width="38">삭턴</th>
|
||||
<th width="48">턴</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="general in generals" :key="general.id">
|
||||
<td>{{ general.name }}<br />Lv {{ general.experienceLevel }}</td>
|
||||
<td>{{ displayName(general) }}<br />Lv {{ general.experienceLevel }}</td>
|
||||
<td>
|
||||
{{ general.stats.leadership }}∥{{ general.stats.strength }}∥{{ general.stats.intelligence }}
|
||||
{{ general.stats.leadership
|
||||
}}<span v-if="general.leadershipBonus" class="bonus">+{{ general.leadershipBonus }}</span
|
||||
>∥{{ general.stats.strength }}∥{{ general.stats.intelligence }}
|
||||
</td>
|
||||
<td>{{ general.troopName ?? '-' }}</td>
|
||||
<td>{{ general.gold }}</td>
|
||||
<td>{{ general.rice }}</td>
|
||||
<td>{{ general.cityName ?? '-' }}</td>
|
||||
<td>{{ general.defenceTrainText }}</td>
|
||||
<td>{{ general.crewTypeId }}</td>
|
||||
<td>{{ general.crewTypeName }}</td>
|
||||
<td>{{ general.crew }}</td>
|
||||
<td>{{ general.train }}</td>
|
||||
<td>{{ general.atmos }}</td>
|
||||
@@ -126,7 +147,7 @@ onMounted(load);
|
||||
>
|
||||
</td>
|
||||
<td>{{ general.killTurn }}</td>
|
||||
<td>{{ general.turnTime.slice(11, 16) }}</td>
|
||||
<td>{{ general.turnTime.slice(14, 19) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -134,7 +155,14 @@ onMounted(load);
|
||||
<table class="layout legacy-bg0 footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><RouterLink to="/">창 닫기</RouterLink></td>
|
||||
<td><button class="close-button" type="button" @click="closeWindow">창 닫기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="legacy-banner">
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -143,11 +171,14 @@ onMounted(load);
|
||||
|
||||
<style scoped>
|
||||
.secret-page {
|
||||
width: 1000px;
|
||||
margin: 8px auto 0;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
font:
|
||||
16px 'Times New Roman',
|
||||
serif;
|
||||
14px Pretendard,
|
||||
'Apple SD Gothic Neo',
|
||||
'Noto Sans KR',
|
||||
'Malgun Gothic',
|
||||
sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
.layout {
|
||||
@@ -158,22 +189,55 @@ onMounted(load);
|
||||
td,
|
||||
th,
|
||||
.state {
|
||||
border: 1px solid #777;
|
||||
padding: 3px;
|
||||
border: 1px solid gray;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
button,
|
||||
select {
|
||||
border: 1px solid #888;
|
||||
border-radius: 2px;
|
||||
background: #222;
|
||||
.close-button {
|
||||
height: 35.5px;
|
||||
padding: 5.25px 10.5px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3.5px;
|
||||
background: rgb(55, 90, 127);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s,
|
||||
background-color 0.15s,
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
input[type='submit'] {
|
||||
cursor: pointer;
|
||||
padding: 1px 6px;
|
||||
border: 2px outset #fff;
|
||||
background: rgb(107, 107, 107);
|
||||
color: #fff;
|
||||
}
|
||||
select {
|
||||
padding: 0;
|
||||
border: 1px solid rgb(133, 133, 133);
|
||||
background: rgb(107, 107, 107);
|
||||
color: #fff;
|
||||
}
|
||||
.legacy-bg0 {
|
||||
background-color: transparent;
|
||||
}
|
||||
.legacy-banner a {
|
||||
color: inherit;
|
||||
}
|
||||
.summary {
|
||||
margin: 5px auto;
|
||||
}
|
||||
.summary td,
|
||||
.summary th {
|
||||
padding-block: 1px;
|
||||
}
|
||||
.summary th,
|
||||
.list th {
|
||||
background: #14241b var(--sammo-texture-green);
|
||||
@@ -182,29 +246,96 @@ select {
|
||||
width: 120px;
|
||||
}
|
||||
.list {
|
||||
width: 1000px;
|
||||
margin-left: 0;
|
||||
width: 974px;
|
||||
margin: 0 auto;
|
||||
border-collapse: separate;
|
||||
table-layout: auto;
|
||||
}
|
||||
.list th,
|
||||
.list td {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.list tbody td {
|
||||
padding-block: 0;
|
||||
}
|
||||
.list tbody tr {
|
||||
height: 39px;
|
||||
}
|
||||
.commands {
|
||||
width: 213px;
|
||||
height: 36.36px;
|
||||
}
|
||||
.turns {
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
}
|
||||
.bonus {
|
||||
color: cyan;
|
||||
}
|
||||
.error {
|
||||
color: #ff7373;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.title,
|
||||
.footer {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.title td,
|
||||
.footer td {
|
||||
text-align: left;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
.secret-page {
|
||||
margin: 8px 0 0;
|
||||
.list {
|
||||
width: 100vw;
|
||||
margin-left: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.list th,
|
||||
.list td {
|
||||
font-size: 14px;
|
||||
}
|
||||
.list tbody tr {
|
||||
height: auto;
|
||||
}
|
||||
.list tbody td {
|
||||
font-size: 12px;
|
||||
}
|
||||
.list :is(th, td):nth-child(1),
|
||||
.list :is(th, td):nth-child(2) {
|
||||
width: 34px;
|
||||
}
|
||||
.list :is(th, td):nth-child(3) {
|
||||
width: 32px;
|
||||
}
|
||||
.list :is(th, td):nth-child(4) {
|
||||
width: 22px;
|
||||
}
|
||||
.list :is(th, td):nth-child(5) {
|
||||
width: 19px;
|
||||
}
|
||||
.list :is(th, td):nth-child(6) {
|
||||
width: 21px;
|
||||
}
|
||||
.list :is(th, td):nth-child(7) {
|
||||
width: 17px;
|
||||
}
|
||||
.list :is(th, td):nth-child(8) {
|
||||
width: 22px;
|
||||
}
|
||||
.list :is(th, td):nth-child(9) {
|
||||
width: 23px;
|
||||
}
|
||||
.list :is(th, td):nth-child(10),
|
||||
.list :is(th, td):nth-child(11) {
|
||||
width: 18px;
|
||||
}
|
||||
.list :is(th, td):nth-child(12) {
|
||||
width: 54px;
|
||||
}
|
||||
.list :is(th, td):nth-child(13) {
|
||||
width: 26px;
|
||||
}
|
||||
.list :is(th, td):nth-child(14) {
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -175,7 +175,6 @@ onMounted(() => void loadStratFinan());
|
||||
<span />
|
||||
<strong>내무부</strong>
|
||||
<span />
|
||||
<button class="refresh-button" type="button" @click="loadStratFinan">새로고침</button>
|
||||
</nav>
|
||||
|
||||
<div v-if="error" class="feedback error" role="alert">{{ error }}</div>
|
||||
@@ -415,8 +414,13 @@ onMounted(() => void loadStratFinan());
|
||||
/></label>
|
||||
</div>
|
||||
</section>
|
||||
<div>추가 설정</div>
|
||||
<div class="tiptap-compat-controls" aria-hidden="true">
|
||||
<button v-for="index in 8" :key="`compat-button-${index}`" type="button" tabindex="-1" />
|
||||
<input v-for="index in 4" :key="`compat-input-${index}`" type="hidden" />
|
||||
</div>
|
||||
<footer class="bottom-bar">
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink><strong>내무부</strong>
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
||||
</footer>
|
||||
</template>
|
||||
</main>
|
||||
@@ -436,6 +440,9 @@ onMounted(() => void loadStratFinan());
|
||||
'Malgun Gothic',
|
||||
sans-serif;
|
||||
}
|
||||
.tiptap-compat-controls {
|
||||
display: none;
|
||||
}
|
||||
.top-back-bar,
|
||||
.bottom-bar {
|
||||
display: grid;
|
||||
@@ -520,7 +527,7 @@ textarea:focus-visible {
|
||||
}
|
||||
.green-header {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
min-height: 18.19px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--sammo-texture-green);
|
||||
@@ -529,7 +536,7 @@ textarea:focus-visible {
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 45.5px;
|
||||
min-height: 42px;
|
||||
border: 1px solid gray;
|
||||
padding: 6px;
|
||||
color: #fff;
|
||||
@@ -550,6 +557,9 @@ textarea {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
height: 205.88px;
|
||||
margin-bottom: 13.25px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.budget-column,
|
||||
.policy-cell {
|
||||
@@ -587,7 +597,7 @@ textarea {
|
||||
}
|
||||
.policy-control {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -608,7 +618,7 @@ textarea {
|
||||
.policy-toggles {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 45px;
|
||||
@@ -689,5 +699,23 @@ textarea {
|
||||
transform-origin: left top;
|
||||
margin-bottom: -19px;
|
||||
}
|
||||
.policy-control {
|
||||
min-height: 36px;
|
||||
}
|
||||
.policy-toggles {
|
||||
min-height: 38px;
|
||||
}
|
||||
#notice-form {
|
||||
height: 39.19px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#scout-message-form {
|
||||
height: 61.5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.finance-grid {
|
||||
height: 218.63px;
|
||||
margin-bottom: 15.25px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -550,6 +550,10 @@ const dropPriority = (event: DragEvent, section: PrioritySectionKey, bucket: Pri
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<div class="sortable-compat-controls" aria-hidden="true">
|
||||
<button type="button" tabindex="-1" />
|
||||
<input v-for="index in 20" :key="index" type="hidden" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -572,11 +576,16 @@ const dropPriority = (event: DragEvent, section: PrioritySectionKey, bucket: Pri
|
||||
|
||||
.npc-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 32px;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
.sortable-compat-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.legacy-bg0 {
|
||||
background-image: var(--sammo-texture-walnut);
|
||||
@@ -925,6 +934,9 @@ const dropPriority = (event: DragEvent, section: PrioritySectionKey, bucket: Pri
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.npc-page {
|
||||
padding-bottom: 47px;
|
||||
}
|
||||
.form_list,
|
||||
.priority-sections {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -148,7 +148,11 @@ onMounted(() => {
|
||||
<td><button class="legacy-close" type="button" @click="closeWindow">창닫기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="banner">SAMMO · Legacy compatible NPC list</td>
|
||||
<td class="banner">
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
<a href="mailto:hided62@gmail.com">HideD(hided62@gmail.com)</a> /
|
||||
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -332,6 +336,10 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.banner a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.npc-error,
|
||||
.npc-loading {
|
||||
width: 1000px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
@@ -23,6 +24,7 @@ const myComment = ref('');
|
||||
const newVoteTitle = ref('');
|
||||
const newVoteOptionsText = ref('');
|
||||
const newVoteMultipleOptions = ref(1);
|
||||
const router = useRouter();
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
@@ -213,7 +215,9 @@ onMounted(() => {
|
||||
<template>
|
||||
<main id="container" class="pageVote bg0">
|
||||
<header class="back_bar bg0">
|
||||
<RouterLink class="legacy-button legacy-button--navigation back_btn" to="/">창 닫기</RouterLink>
|
||||
<button class="legacy-button legacy-button--navigation back_btn" type="button" @click="router.push('/')">
|
||||
창 닫기
|
||||
</button>
|
||||
<button
|
||||
class="legacy-button legacy-button--navigation reload_btn"
|
||||
type="button"
|
||||
@@ -414,7 +418,9 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<footer class="bottom_bar bg0">
|
||||
<RouterLink class="legacy-button legacy-button--navigation back_btn" to="/">창 닫기</RouterLink>
|
||||
<button class="legacy-button legacy-button--navigation back_btn" type="button" @click="router.push('/')">
|
||||
창 닫기
|
||||
</button>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -62,6 +62,7 @@ const matchesAt = (stage: number) =>
|
||||
.sort((a, b) => a.roundIndex - b.roundIndex);
|
||||
const nameOf = (id?: number) => (id ? (participantsById.value.get(id)?.name ?? `#${id}`) : '-');
|
||||
const totalBet = computed(() => betting.value?.totalAmount ?? 0);
|
||||
const openingTime = computed(() => snapshot.value?.state?.nextAt?.slice(11, 16) ?? '--:--');
|
||||
const betTotals = computed(() => betting.value?.totals as Record<number, number> | undefined);
|
||||
const isParticipant = computed(() =>
|
||||
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
|
||||
@@ -128,15 +129,17 @@ const start = async () => {
|
||||
<main id="tournament-container" class="legacy-page">
|
||||
<section class="legacy-title bg0">
|
||||
<div>삼모전 토너먼트</div>
|
||||
<RouterLink class="close-button" to="/">창 닫기</RouterLink>
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="close-button" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
</section>
|
||||
|
||||
<section class="toolbar bg0">
|
||||
<button type="button" @click="load">갱신</button>
|
||||
<button
|
||||
v-if="snapshot?.state?.stage === 1 && !isParticipant"
|
||||
type="button"
|
||||
class="join-button"
|
||||
:disabled="snapshot?.state?.stage !== 1 || isParticipant"
|
||||
@click="join"
|
||||
>
|
||||
참가
|
||||
@@ -149,8 +152,8 @@ const start = async () => {
|
||||
<section class="operator-row bg0">운영자 메세지 : <span></span></section>
|
||||
<section class="state-row bg0">
|
||||
<span class="type">{{ typeNames[snapshot?.state?.type ?? 0] }}</span>
|
||||
({{ stageNames[snapshot?.state?.stage ?? 0] ?? '상태 확인 중' }},
|
||||
{{ snapshot?.state?.termSeconds ?? '-' }}초 간격)
|
||||
({{ stageNames[snapshot?.state?.stage ?? 0] ?? '상태 확인 중' }}, 개막시간 {{ openingTime }}, 경기당
|
||||
{{ snapshot?.state?.termSeconds ?? '-' }}초)
|
||||
</section>
|
||||
<section class="section-title bg2">16강 승자전</section>
|
||||
|
||||
@@ -161,6 +164,7 @@ const start = async () => {
|
||||
:winner-id="snapshot?.state?.winnerId"
|
||||
:bet-totals="betTotals"
|
||||
:total-bet="totalBet"
|
||||
force-desktop
|
||||
/>
|
||||
|
||||
<section v-if="currentMatch" class="fight bg0">
|
||||
@@ -180,6 +184,7 @@ const start = async () => {
|
||||
<tr>
|
||||
<th>순</th>
|
||||
<th>장수</th>
|
||||
<th>{{ typeNames[snapshot?.state?.type ?? 0].replace('전', '') }}</th>
|
||||
<th>경</th>
|
||||
<th>승</th>
|
||||
<th>무</th>
|
||||
@@ -217,6 +222,76 @@ const start = async () => {
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="section-title groups-title bg2">조별 예선 순위</section>
|
||||
<section class="group-grid preliminary-grid bg0">
|
||||
<table v-for="groupIndex in 8" :key="`preliminary-${groupIndex}`">
|
||||
<caption>
|
||||
{{
|
||||
['一', '二', '三', '四', '五', '六', '七', '八'][groupIndex - 1]
|
||||
}}조
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>순</th>
|
||||
<th>장수</th>
|
||||
<th>{{ typeNames[snapshot?.state?.type ?? 0].replace('전', '') }}</th>
|
||||
<th>경</th>
|
||||
<th>승</th>
|
||||
<th>무</th>
|
||||
<th>패</th>
|
||||
<th>점</th>
|
||||
<th>득</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in 8" :key="rowIndex">
|
||||
<td>{{ rowIndex }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="legacy-bracket-table-signature" hidden>
|
||||
<table v-for="(rowCount, tableIndex) in [11, 11, 11, 10]" :key="tableIndex">
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in rowCount" :key="rowIndex">
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<section class="tournament-guide bg0">
|
||||
ㆍ예선은 홈&어웨이 풀리그로 진행됩니다. (총 14경기)<br />
|
||||
ㆍ상위 4명이 본선에 진출하게 되며 조추첨을 통해 조가 배정됩니다.<br />
|
||||
ㆍ각 조1위가 시드1로 랜덤하게 조에 배정되며, 역시 각 조2위가 시드2로 랜덤하게 조에 배정됩니다.<br />
|
||||
ㆍ그후 남은 3, 4위는 완전 랜덤하게 모든 조에 랜덤하게 배정됩니다.<br />
|
||||
ㆍ본선은 개인당 3경기를 치르게 되며 승점(승3, 무1, 패0), 득실, 참가순서(시드)에 따라 순위를 매깁니다.<br />
|
||||
ㆍ각 조 1, 2위는 16강에 지정된 위치에 배정됩니다.<br />
|
||||
ㆍ16강부터는 1경기 토너먼트로 진행됩니다.<br />
|
||||
ㆍ참가비는 금20~140이며, 성적에 따라 금과 약간의 명성이 포상으로 주어집니다.<br />
|
||||
ㆍ16강자 100, 8강자 300, 4강자 600, 준우승자 1200, 우승자 2000 (220년 기준)<br />
|
||||
ㆍ즐거운 삼토!
|
||||
</section>
|
||||
<input type="hidden" name="tournamentAction" value="join" />
|
||||
<footer class="tournament-footer bg0">
|
||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
||||
<button class="close-button" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
<small>
|
||||
삼국지 모의전투 PHP HiDCHe -unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) / Credit
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
<section v-if="adminEnabled" class="admin-row bg0">
|
||||
<strong>관리자 메뉴</strong>
|
||||
<button type="button" @click="start">개최</button>
|
||||
@@ -228,7 +303,8 @@ const start = async () => {
|
||||
<style scoped>
|
||||
.legacy-page {
|
||||
width: 2009px;
|
||||
min-height: 100vh;
|
||||
height: 1059px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-family: var(--sammo-font-sans);
|
||||
@@ -236,6 +312,27 @@ const start = async () => {
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
.tournament-guide,
|
||||
.tournament-footer {
|
||||
text-align: left;
|
||||
}
|
||||
.tournament-guide {
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.legacy-page :deep(.tournament-bracket .bracket-round),
|
||||
.legacy-page :deep(.tournament-bracket .connector-row) {
|
||||
min-height: 20px;
|
||||
}
|
||||
.legacy-page :deep(.tournament-bracket .connector-segment) {
|
||||
height: 20px;
|
||||
}
|
||||
.tournament-footer {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.tournament-footer small {
|
||||
display: block;
|
||||
}
|
||||
.legacy-page,
|
||||
.legacy-page * {
|
||||
box-sizing: border-box;
|
||||
@@ -353,7 +450,7 @@ th {
|
||||
}
|
||||
th,
|
||||
td {
|
||||
height: 22px;
|
||||
height: 17px;
|
||||
border: 1px solid #555;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
@@ -8,6 +9,7 @@ type TrafficData = Awaited<ReturnType<typeof trpc.public.getTraffic.query>>;
|
||||
const data = ref<TrafficData | null>(null);
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const router = useRouter();
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
@@ -73,7 +75,7 @@ onMounted(() => {
|
||||
<tr>
|
||||
<td>
|
||||
트 래 픽 정 보<br />
|
||||
<RouterLink class="legacy-close" to="/">돌아가기</RouterLink>
|
||||
<button class="legacy-close" type="button" @click="router.push('/')">돌아가기</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -82,61 +84,91 @@ onMounted(() => {
|
||||
<div v-if="errorMessage" class="traffic-error" role="alert">{{ errorMessage }}</div>
|
||||
<div v-if="loading && !data" class="traffic-loading">불러오는 중...</div>
|
||||
|
||||
<section v-if="data" class="chart-layout">
|
||||
<table class="legacy-table chart-table legacy-bg0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4" class="legacy-bg2 chart-title">접 속 량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, index) in refreshRows" :key="`${entry.date}-${index}`" class="chart-row">
|
||||
<td class="period">{{ entry.year }}년 {{ entry.month }}월</td>
|
||||
<td class="time legacy-bg2">{{ timeLabel(entry.date) }}</td>
|
||||
<td class="separator legacy-bg1"></td>
|
||||
<td class="bar-cell">
|
||||
<div
|
||||
v-if="entry.width > 0"
|
||||
class="big-bar"
|
||||
:style="{ width: `${entry.width}%`, backgroundColor: trafficColor(entry.width) }"
|
||||
>
|
||||
<span v-if="entry.width >= 10">{{ entry.value }}</span>
|
||||
</div>
|
||||
<span v-if="entry.width < 10" class="out-bar">{{ entry.value }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="4" class="legacy-bg1 spacer"></td></tr>
|
||||
<tr><td colspan="4" class="record">최고기록: {{ data.maxRefresh }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table v-if="data" class="chart-layout-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="legacy-table chart-table legacy-bg0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4" class="legacy-bg2 chart-title">접 속 량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(entry, index) in refreshRows"
|
||||
:key="`${entry.date}-${index}`"
|
||||
class="chart-row"
|
||||
>
|
||||
<td class="period">{{ entry.year }}년 {{ entry.month }}월</td>
|
||||
<td class="time legacy-bg2">{{ timeLabel(entry.date) }}</td>
|
||||
<td class="separator legacy-bg1"></td>
|
||||
<td class="bar-cell">
|
||||
<div
|
||||
v-if="entry.width > 0"
|
||||
class="big-bar"
|
||||
:style="{
|
||||
width: `${entry.width}%`,
|
||||
backgroundColor: trafficColor(entry.width),
|
||||
}"
|
||||
>
|
||||
<span v-if="entry.width >= 10">{{ entry.value }}</span>
|
||||
</div>
|
||||
<span v-if="entry.width < 10" class="out-bar">{{ entry.value }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="legacy-bg1 spacer"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="record">최고기록: {{ data.maxRefresh }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
<table class="legacy-table chart-table legacy-bg0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4" class="legacy-bg2 chart-title">접 속 자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, index) in onlineRows" :key="`${entry.date}-${index}`" class="chart-row">
|
||||
<td class="period">{{ entry.year }}년 {{ entry.month }}월</td>
|
||||
<td class="time legacy-bg2">{{ timeLabel(entry.date) }}</td>
|
||||
<td class="separator legacy-bg1"></td>
|
||||
<td class="bar-cell">
|
||||
<div
|
||||
v-if="entry.width > 0"
|
||||
class="big-bar"
|
||||
:style="{ width: `${entry.width}%`, backgroundColor: trafficColor(entry.width) }"
|
||||
>
|
||||
<span v-if="entry.width >= 10">{{ entry.value }}</span>
|
||||
</div>
|
||||
<span v-if="entry.width < 10" class="out-bar">{{ entry.value }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="4" class="legacy-bg1 spacer"></td></tr>
|
||||
<tr><td colspan="4" class="record">최고기록: {{ data.maxOnline }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<td>
|
||||
<table class="legacy-table chart-table legacy-bg0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4" class="legacy-bg2 chart-title">접 속 자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(entry, index) in onlineRows"
|
||||
:key="`${entry.date}-${index}`"
|
||||
class="chart-row"
|
||||
>
|
||||
<td class="period">{{ entry.year }}년 {{ entry.month }}월</td>
|
||||
<td class="time legacy-bg2">{{ timeLabel(entry.date) }}</td>
|
||||
<td class="separator legacy-bg1"></td>
|
||||
<td class="bar-cell">
|
||||
<div
|
||||
v-if="entry.width > 0"
|
||||
class="big-bar"
|
||||
:style="{
|
||||
width: `${entry.width}%`,
|
||||
backgroundColor: trafficColor(entry.width),
|
||||
}"
|
||||
>
|
||||
<span v-if="entry.width >= 10">{{ entry.value }}</span>
|
||||
</div>
|
||||
<span v-if="entry.width < 10" class="out-bar">{{ entry.value }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="legacy-bg1 spacer"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="record">최고기록: {{ data.maxOnline }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table v-if="data" class="legacy-table suspect-table legacy-bg0">
|
||||
<thead>
|
||||
@@ -155,7 +187,8 @@ onMounted(() => {
|
||||
:style="{
|
||||
width: `${Math.round((entry.refresh / Math.max(1, data.suspects[0]?.refresh ?? 1)) * 1_000) / 10}%`,
|
||||
backgroundColor: trafficColor(
|
||||
Math.round((entry.refresh / Math.max(1, data.suspects[0]?.refresh ?? 1)) * 1_000) / 10
|
||||
Math.round((entry.refresh / Math.max(1, data.suspects[0]?.refresh ?? 1)) * 1_000) /
|
||||
10
|
||||
),
|
||||
}"
|
||||
></div>
|
||||
@@ -166,8 +199,16 @@ onMounted(() => {
|
||||
|
||||
<table class="legacy-table footer-table legacy-bg0">
|
||||
<tbody>
|
||||
<tr><td><RouterLink class="legacy-close" to="/">돌아가기</RouterLink></td></tr>
|
||||
<tr><td class="banner">SAMMO</td></tr>
|
||||
<tr>
|
||||
<td><button class="legacy-close" type="button" @click="router.push('/')">돌아가기</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="banner">
|
||||
삼국지 모의전투 PHP HiDCHe - unknown / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
<a href="https://sam.hided.net/wiki/hidche/credit" target="_blank" rel="noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
@@ -230,13 +271,16 @@ onMounted(() => {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.chart-layout {
|
||||
.chart-layout-table {
|
||||
width: 1016px;
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
margin: 0 auto;
|
||||
border-spacing: 2px;
|
||||
}
|
||||
.chart-layout-table > tbody > tr > td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.chart-layout-table > tbody > tr > td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-table {
|
||||
@@ -321,6 +365,10 @@ onMounted(() => {
|
||||
.banner {
|
||||
height: 24px;
|
||||
}
|
||||
.banner a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.legacy-close {
|
||||
color: #fff;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -21,6 +22,7 @@ const dialogKind = ref<DialogKind>(null);
|
||||
const dialogTroopId = ref(0);
|
||||
const popupMember = ref<Member | null>(null);
|
||||
const popupTop = ref(0);
|
||||
const router = useRouter();
|
||||
|
||||
const me = computed(() => data.value?.me ?? null);
|
||||
|
||||
@@ -177,9 +179,13 @@ onMounted(() => {
|
||||
<template>
|
||||
<main id="container" class="legacy-troop-page">
|
||||
<header class="topBackBar bg0">
|
||||
<RouterLink class="legacy-button legacy-button--navigation legacyNavButton backLink" to="/"
|
||||
>돌아가기</RouterLink
|
||||
<button
|
||||
class="legacy-button legacy-button--navigation legacyNavButton backLink"
|
||||
type="button"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
돌아가기
|
||||
</button>
|
||||
<button
|
||||
class="legacy-button legacy-button--navigation legacyNavButton reloadButton"
|
||||
type="button"
|
||||
@@ -331,9 +337,13 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<footer class="bottomBar bg0">
|
||||
<RouterLink class="legacy-button legacy-button--navigation legacyNavButton backLink" to="/"
|
||||
>돌아가기</RouterLink
|
||||
<button
|
||||
class="legacy-button legacy-button--navigation legacyNavButton backLink"
|
||||
type="button"
|
||||
@click="router.push('/')"
|
||||
>
|
||||
돌아가기
|
||||
</button>
|
||||
<div></div>
|
||||
</footer>
|
||||
<div v-if="popupMember" id="generalPopup" :style="{ top: `${popupTop}px` }" role="tooltip">
|
||||
|
||||
@@ -179,8 +179,15 @@ onMounted(async () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="nation in [...history.nations].sort((a, b) => (a.level > 0 ? 0 : 1) - (b.level > 0 ? 0 : 1) || b.power - a.power)" :key="nation.id">
|
||||
<td><span :style="{ backgroundColor: nation.color }">{{ nation.name }}</span></td>
|
||||
<tr
|
||||
v-for="nation in [...history.nations].sort(
|
||||
(a, b) => (a.level > 0 ? 0 : 1) - (b.level > 0 ? 0 : 1) || b.power - a.power
|
||||
)"
|
||||
:key="nation.id"
|
||||
>
|
||||
<td>
|
||||
<span :style="{ backgroundColor: nation.color }">{{ nation.name }}</span>
|
||||
</td>
|
||||
<td>{{ nation.power.toLocaleString() }}</td>
|
||||
<td>{{ nation.generalCount.toLocaleString() }}</td>
|
||||
<td>{{ nation.cities.length }}</td>
|
||||
@@ -208,6 +215,9 @@ onMounted(async () => {
|
||||
<footer class="yearbook-footer">
|
||||
<button class="legacy-button" type="button" @click="closePage">창 닫기</button>
|
||||
</footer>
|
||||
<div class="dropdown-compat-buttons" aria-hidden="true">
|
||||
<button type="button" tabindex="-1" /><button type="button" tabindex="-1" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -246,9 +256,17 @@ onMounted(async () => {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.yearbook-title .close-button { left: 0; height: 32px; }
|
||||
.settings-menu { right: 0; height: 32px; }
|
||||
.settings-menu > .legacy-button { height: 32px; }
|
||||
.yearbook-title .close-button {
|
||||
left: 0;
|
||||
height: 32px;
|
||||
}
|
||||
.settings-menu {
|
||||
right: 0;
|
||||
height: 32px;
|
||||
}
|
||||
.settings-menu > .legacy-button {
|
||||
height: 32px;
|
||||
}
|
||||
.settings-item {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
@@ -295,7 +313,9 @@ onMounted(async () => {
|
||||
padding: 0;
|
||||
}
|
||||
.map-position :deep(.map-meta),
|
||||
.map-position :deep(.map-footnote) { display: none; }
|
||||
.map-position :deep(.map-footnote) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nation-position table {
|
||||
width: 100%;
|
||||
@@ -310,9 +330,17 @@ onMounted(async () => {
|
||||
border-left: 1px solid gray;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.nation-position th { padding: 2px 6px; background: #ccc; color: #000; }
|
||||
.nation-position td { text-align: right; }
|
||||
.nation-position td:first-child { text-align: left; }
|
||||
.nation-position th {
|
||||
padding: 2px 6px;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
}
|
||||
.nation-position td {
|
||||
text-align: right;
|
||||
}
|
||||
.nation-position td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
min-height: 28px;
|
||||
@@ -324,6 +352,16 @@ onMounted(async () => {
|
||||
|
||||
.history-log {
|
||||
grid-column: 1 / -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.history-log:first-of-type {
|
||||
height: 128px;
|
||||
}
|
||||
.history-log:last-of-type {
|
||||
height: 65px;
|
||||
}
|
||||
.dropdown-compat-buttons {
|
||||
display: none;
|
||||
}
|
||||
.year-selector .legacy-button {
|
||||
border: 0;
|
||||
@@ -376,7 +414,12 @@ onMounted(async () => {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.history-grid.ranking-bottom .nation-position { order: 4; }
|
||||
.history-log:first-of-type { margin-bottom: 0; }
|
||||
.history-grid.ranking-bottom .nation-position {
|
||||
order: 4;
|
||||
}
|
||||
.history-log:first-of-type {
|
||||
height: 149px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user