diff --git a/app/game-api/src/router/nation/index.ts b/app/game-api/src/router/nation/index.ts index 6557192..a710143 100644 --- a/app/game-api/src/router/nation/index.ts +++ b/app/game-api/src/router/nation/index.ts @@ -1,10 +1,874 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; +import { asNumber, asRecord } from '@sammo-ts/common'; +import { + DomesticTraitLoader, + loadDomesticTraitModules, + isDomesticTraitKey, + loadNationTraitModules, + NationTraitLoader, + isNationTraitKey, + loadPersonalityTraitModules, + PersonalityTraitLoader, + isPersonalityTraitKey, + loadWarTraitModules, + WarTraitLoader, + isWarTraitKey, + type GeneralActionContext, + type General as LogicGeneral, + type Nation as LogicNation, + type TriggerValue, +} from '@sammo-ts/logic'; + +import type { WorldStateRow } from '../../context.js'; import { authedProcedure, router } from '../../trpc.js'; import { getMyGeneral } from '../shared/general.js'; +type PermissionKind = 'normal' | 'ambassador' | 'auditor'; + +type TraitNameMap = Map; + +type TraitCache = { + domestic: TraitNameMap; + war: TraitNameMap; + personality: TraitNameMap; + nation: TraitNameMap; +}; + +type NationTraitModule = Awaited>[number] | null; + +type NationIncomeContext = { + trait: NationTraitModule; + context: GeneralActionContext; + rate: number; +}; + +type NationIncomeRow = { + id: number; + name: string; + color: string; + capitalCityId: number | null; + level: number; + typeCode: string; + meta: unknown; + gold?: number; + rice?: number; +}; + +type CityIncomeRow = { + id: number; + name: string; + level: number; + nationId: number; + region: number; + population: number; + populationMax: number; + agriculture: number; + agricultureMax: number; + commerce: number; + commerceMax: number; + security: number; + securityMax: number; + trust: number; + trade: number; + defence: number; + defenceMax: number; + wall: number; + wallMax: number; + supplyState: number; + frontState: number; + meta: unknown; +}; + +type GeneralListRow = { + id: number; + name: string; + npcState: number; + nationId: number; + cityId: number; + troopId: number; + officerLevel: number; + leadership: number; + strength: number; + intel: number; + experience: number; + dedication: number; + injury: number; + gold: number; + rice: number; + crew: number; + personalCode: string; + specialCode: string; + special2Code: string; + meta: unknown; + penalty: unknown; +}; + +type GeneralOfficerRow = { + id: number; + name: string; + npcState: number; + officerLevel: number; + cityId: number; + leadership: number; + strength: number; + intel: number; + meta: unknown; +}; + +const traitCache: TraitCache = { + domestic: new Map(), + war: new Map(), + personality: new Map(), + nation: new Map(), +}; + +const DEFAULT_CHIEF_STAT_MIN = 65; + +const normalizeTraitKey = (value: string | null | undefined): string | null => { + if (!value || value === 'None') { + return null; + } + return value; +}; + +const readMetaNumber = (meta: Record, key: string, fallback: number): number => { + const raw = meta[key]; + return typeof raw === 'number' && Number.isFinite(raw) ? raw : fallback; +}; + +const resolveOfficerCity = (meta: Record): number => { + const camel = readMetaNumber(meta, 'officerCity', 0); + if (camel > 0) { + return camel; + } + return readMetaNumber(meta, 'officer_city', 0); +}; + +const resolveBelong = (meta: Record): number => readMetaNumber(meta, 'belong', 0); + +const resolvePermission = (meta: Record): PermissionKind => { + const value = meta.permission; + if (value === 'ambassador' || value === 'auditor') { + return value; + } + return 'normal'; +}; + +const resolveChiefStatMin = (worldState: WorldStateRow | null): number => { + if (!worldState) { + return DEFAULT_CHIEF_STAT_MIN; + } + const config = asRecord(worldState.config); + const stat = asRecord(config.stat); + return asNumber(stat.chiefMin, DEFAULT_CHIEF_STAT_MIN); +}; + +const resolveNationRate = (nation: NationIncomeRow): number => { + const meta = asRecord(nation.meta); + return asNumber(meta.rate, 20); +}; + +const checkSecretMaxPermission = (penalty: Record): number => { + if (penalty.noTopSecret) { + return 1; + } + if (penalty.noChief) { + return 1; + } + if (penalty.noAmbassador) { + return 2; + } + return 4; +}; + +const loadTraitNames = async ( + keys: Array, + kind: keyof TraitCache +): Promise => { + const cache = traitCache[kind]; + const unique = Array.from(new Set(keys.filter((key): key is string => Boolean(key)))); + const missing = unique.filter((key) => !cache.has(key)); + + if (!missing.length) { + return cache; + } + + if (kind === 'domestic') { + const filtered = missing.filter((key) => isDomesticTraitKey(key)); + if (filtered.length) { + const modules = await loadDomesticTraitModules(filtered, new DomesticTraitLoader()); + for (const module of modules) { + cache.set(module.key, { name: module.name, info: module.info ?? '' }); + } + } + } else if (kind === 'war') { + const filtered = missing.filter((key) => isWarTraitKey(key)); + if (filtered.length) { + const modules = await loadWarTraitModules(filtered, new WarTraitLoader()); + for (const module of modules) { + cache.set(module.key, { name: module.name, info: module.info ?? '' }); + } + } + } else if (kind === 'personality') { + const filtered = missing.filter((key) => isPersonalityTraitKey(key)); + if (filtered.length) { + const modules = await loadPersonalityTraitModules(filtered, new PersonalityTraitLoader()); + for (const module of modules) { + cache.set(module.key, { name: module.name, info: module.info ?? '' }); + } + } + } else if (kind === 'nation') { + const filtered = missing.filter((key) => isNationTraitKey(key)); + if (filtered.length) { + const modules = await loadNationTraitModules(filtered, new NationTraitLoader()); + for (const module of modules) { + cache.set(module.key, { name: module.name, info: module.info ?? '' }); + } + } + } + + return cache; +}; + +const buildIncomeContext = (nation: NationIncomeRow): GeneralActionContext => { + const nationMeta = asRecord(nation.meta) as Record; + const general: LogicGeneral = { + id: 0, + name: 'SYSTEM', + nationId: nation.id, + cityId: 0, + troopId: 0, + stats: { leadership: 0, strength: 0, intelligence: 0 }, + experience: 0, + dedication: 0, + officerLevel: 0, + role: { + personality: null, + specialDomestic: null, + specialWar: null, + items: { + horse: null, + weapon: null, + book: null, + item: null, + }, + }, + injury: 0, + gold: 0, + rice: 0, + crew: 0, + crewTypeId: 0, + train: 0, + atmos: 0, + age: 0, + npcState: 0, + triggerState: { + flags: {}, + counters: {}, + modifiers: {}, + meta: {}, + }, + meta: {}, + }; + const logicNation: LogicNation = { + id: nation.id, + name: nation.name, + color: nation.color, + capitalCityId: nation.capitalCityId, + chiefGeneralId: null, + gold: nation.gold ?? 0, + rice: nation.rice ?? 0, + power: 0, + level: nation.level, + typeCode: nation.typeCode, + meta: nationMeta, + }; + return { general, nation: logicNation }; +}; + +const buildNationIncomeContext = async (nation: NationIncomeRow): Promise => { + let trait: NationTraitModule = null; + if (isNationTraitKey(nation.typeCode)) { + [trait] = await loadNationTraitModules([nation.typeCode], new NationTraitLoader()); + } + return { + trait, + context: buildIncomeContext(nation), + rate: resolveNationRate(nation), + }; +}; + +const applyNationIncome = ( + incomeContext: NationIncomeContext, + type: 'gold' | 'rice' | 'pop', + amount: number +): number => { + if (!incomeContext.trait?.onCalcNationalIncome) { + return amount; + } + return incomeContext.trait.onCalcNationalIncome(incomeContext.context, type, amount); +}; + +const calcCityGoldIncome = ( + incomeContext: NationIncomeContext, + city: CityIncomeRow, + officerCnt: number, + isCapital: boolean, + nationLevel: number +): number => { + if (city.supplyState === 0) { + return 0; + } + const trustRatio = city.trust / 200 + 0.5; + const commMax = Math.max(1, city.commerceMax); + const secuMax = Math.max(1, city.securityMax); + + let income = (city.population * city.commerce * trustRatio) / commMax / 30; + income *= 1 + city.security / secuMax / 10; + income *= Math.pow(1.05, officerCnt); + if (isCapital && nationLevel > 0) { + income *= 1 + 1 / (3 * nationLevel); + } + + const adjusted = applyNationIncome(incomeContext, 'gold', income); + return Math.round(adjusted * (incomeContext.rate / 20)); +}; + +const calcCityRiceIncome = ( + incomeContext: NationIncomeContext, + city: CityIncomeRow, + officerCnt: number, + isCapital: boolean, + nationLevel: number +): number => { + if (city.supplyState === 0) { + return 0; + } + const trustRatio = city.trust / 200 + 0.5; + const agriMax = Math.max(1, city.agricultureMax); + const secuMax = Math.max(1, city.securityMax); + + let income = (city.population * city.agriculture * trustRatio) / agriMax / 30; + income *= 1 + city.security / secuMax / 10; + income *= Math.pow(1.05, officerCnt); + if (isCapital && nationLevel > 0) { + income *= 1 + 1 / (3 * nationLevel); + } + + const adjusted = applyNationIncome(incomeContext, 'rice', income); + return Math.round(adjusted * (incomeContext.rate / 20)); +}; + +const calcCityWallIncome = ( + incomeContext: NationIncomeContext, + city: CityIncomeRow, + officerCnt: number, + isCapital: boolean, + nationLevel: number +): number => { + if (city.supplyState === 0) { + return 0; + } + const wallMax = Math.max(1, city.wallMax); + const secuMax = Math.max(1, city.securityMax); + + let income = (city.defence * city.wall) / wallMax / 3; + income *= 1 + city.security / secuMax / 10; + income *= Math.pow(1.05, officerCnt); + if (isCapital && nationLevel > 0) { + income *= 1 + 1 / (3 * nationLevel); + } + + const adjusted = applyNationIncome(incomeContext, 'rice', income); + return Math.round(adjusted * (incomeContext.rate / 20)); +}; + +const assertNationAccess = (general: { nationId: number; officerLevel: number }) => { + if (general.nationId <= 0 || general.officerLevel <= 0) { + throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' }); + } +}; + +const mapGeneralList = async ( + generals: GeneralListRow[], + cityNameMap: Map, + troopNameMap: Map +) => { + const personalityKeys = generals.map((general) => normalizeTraitKey(general.personalCode)); + const domesticKeys = generals.map((general) => normalizeTraitKey(general.specialCode)); + const warKeys = generals.map((general) => normalizeTraitKey(general.special2Code)); + + const [personalityMap, domesticMap, warMap] = await Promise.all([ + loadTraitNames(personalityKeys, 'personality'), + loadTraitNames(domesticKeys, 'domestic'), + loadTraitNames(warKeys, 'war'), + ]); + + return generals.map((general) => { + const meta = asRecord(general.meta); + const officerCity = resolveOfficerCity(meta); + const permission = resolvePermission(meta); + const belong = resolveBelong(meta); + const personalityKey = normalizeTraitKey(general.personalCode); + const domesticKey = normalizeTraitKey(general.specialCode); + const warKey = normalizeTraitKey(general.special2Code); + + return { + id: general.id, + name: general.name, + npcState: general.npcState, + officerLevel: general.officerLevel, + cityId: general.cityId, + cityName: cityNameMap.get(general.cityId) ?? null, + troopId: general.troopId, + troopName: troopNameMap.get(general.troopId) ?? null, + officerCity, + officerCityName: officerCity > 0 ? cityNameMap.get(officerCity) ?? null : null, + 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, + personality: personalityKey + ? { + key: personalityKey, + name: personalityMap.get(personalityKey)?.name ?? personalityKey, + } + : null, + specialDomestic: domesticKey + ? { + key: domesticKey, + name: domesticMap.get(domesticKey)?.name ?? domesticKey, + } + : null, + specialWar: warKey + ? { + key: warKey, + name: warMap.get(warKey)?.name ?? warKey, + } + : null, + belong, + permission, + }; + }); +}; + export const nationRouter = router({ + getGeneralList: authedProcedure.query(async ({ ctx }) => { + const general = await getMyGeneral(ctx); + assertNationAccess(general); + + const [nation, cityRows, troopRows, generalRows, worldState] = await Promise.all([ + ctx.db.nation.findUnique({ + where: { id: general.nationId }, + select: { + id: true, + name: true, + color: true, + level: true, + typeCode: true, + capitalCityId: true, + meta: true, + }, + }), + ctx.db.city.findMany({ select: { id: true, name: true } }), + ctx.db.troop.findMany({ select: { troopLeaderId: true, name: true } }), + ctx.db.general.findMany({ + where: { nationId: general.nationId }, + select: { + id: true, + name: true, + npcState: true, + nationId: true, + cityId: true, + troopId: true, + picture: true, + imageServer: true, + officerLevel: true, + leadership: true, + strength: true, + intel: true, + experience: true, + dedication: true, + injury: true, + gold: true, + rice: true, + crew: true, + personalCode: true, + specialCode: true, + special2Code: true, + meta: true, + penalty: true, + }, + orderBy: { id: 'asc' }, + }), + ctx.db.worldState.findFirst(), + ]); + + if (!nation) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); + } + + const cityNameMap = new Map(cityRows.map((city) => [city.id, city.name])); + const troopNameMap = new Map(troopRows.map((troop) => [troop.troopLeaderId, troop.name])); + const list = await mapGeneralList(generalRows, cityNameMap, troopNameMap); + + return { + nation: { + id: nation.id, + name: nation.name, + color: nation.color, + level: nation.level, + typeCode: nation.typeCode, + capitalCityId: nation.capitalCityId ?? 0, + }, + chiefStatMin: resolveChiefStatMin(worldState), + generals: list, + }; + }), + getCityOverview: authedProcedure.query(async ({ ctx }) => { + const me = await getMyGeneral(ctx); + assertNationAccess(me); + + const [nation, cityRows, generalRows, worldState] = await Promise.all([ + ctx.db.nation.findUnique({ + where: { id: me.nationId }, + select: { + id: true, + name: true, + color: true, + level: true, + typeCode: true, + capitalCityId: true, + meta: true, + }, + }), + ctx.db.city.findMany({ + where: { nationId: me.nationId }, + select: { + id: true, + name: true, + level: true, + nationId: true, + region: true, + population: true, + populationMax: true, + agriculture: true, + agricultureMax: true, + commerce: true, + commerceMax: true, + security: true, + securityMax: true, + trust: true, + trade: true, + defence: true, + defenceMax: true, + wall: true, + wallMax: true, + supplyState: true, + frontState: true, + meta: true, + }, + orderBy: { id: 'asc' }, + }), + ctx.db.general.findMany({ + where: { nationId: me.nationId }, + select: { + id: true, + name: true, + npcState: true, + officerLevel: true, + cityId: true, + leadership: true, + strength: true, + intel: true, + meta: true, + }, + orderBy: { id: 'asc' }, + }), + ctx.db.worldState.findFirst(), + ]); + + if (!nation) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); + } + + const cityNameMap = new Map(cityRows.map((city) => [city.id, city.name])); + + const officerByCity = new Map>(); + const officerCntByCity = new Map(); + + for (const general of generalRows) { + if (general.officerLevel < 2 || general.officerLevel > 4) { + continue; + } + const meta = asRecord(general.meta); + const officerCity = resolveOfficerCity(meta); + if (!officerCity) { + continue; + } + const entry = officerByCity.get(officerCity) ?? {}; + entry[general.officerLevel] = general; + officerByCity.set(officerCity, entry); + + if (general.cityId === officerCity) { + officerCntByCity.set(officerCity, (officerCntByCity.get(officerCity) ?? 0) + 1); + } + } + + const incomeContext = await buildNationIncomeContext(nation); + + const cities = cityRows.map((city) => { + const officers = officerByCity.get(city.id) ?? {}; + const officerCnt = officerCntByCity.get(city.id) ?? 0; + const isCapital = nation.capitalCityId === city.id; + const incomes = { + gold: calcCityGoldIncome(incomeContext, city, officerCnt, isCapital, nation.level), + rice: calcCityRiceIncome(incomeContext, city, officerCnt, isCapital, nation.level), + wall: calcCityWallIncome(incomeContext, city, officerCnt, isCapital, nation.level), + }; + + return { + id: city.id, + name: city.name, + level: city.level, + region: city.region, + population: city.population, + populationMax: city.populationMax, + agriculture: city.agriculture, + agricultureMax: city.agricultureMax, + commerce: city.commerce, + commerceMax: city.commerceMax, + security: city.security, + securityMax: city.securityMax, + trust: city.trust, + trade: city.trade, + defence: city.defence, + defenceMax: city.defenceMax, + wall: city.wall, + wallMax: city.wallMax, + supplyState: city.supplyState, + frontState: city.frontState, + incomes, + officers: { + 4: officers[4] + ? { + id: officers[4].id, + name: officers[4].name, + npcState: officers[4].npcState, + officerLevel: officers[4].officerLevel, + cityId: officers[4].cityId, + cityName: cityNameMap.get(officers[4].cityId) ?? null, + } + : null, + 3: officers[3] + ? { + id: officers[3].id, + name: officers[3].name, + npcState: officers[3].npcState, + officerLevel: officers[3].officerLevel, + cityId: officers[3].cityId, + cityName: cityNameMap.get(officers[3].cityId) ?? null, + } + : null, + 2: officers[2] + ? { + id: officers[2].id, + name: officers[2].name, + npcState: officers[2].npcState, + officerLevel: officers[2].officerLevel, + cityId: officers[2].cityId, + cityName: cityNameMap.get(officers[2].cityId) ?? null, + } + : null, + }, + }; + }); + + const generals = generalRows.map((general) => { + const meta = asRecord(general.meta); + return { + id: general.id, + name: general.name, + npcState: general.npcState, + officerLevel: general.officerLevel, + cityId: general.cityId, + officerCity: resolveOfficerCity(meta), + stats: { + leadership: general.leadership, + strength: general.strength, + intelligence: general.intel, + }, + }; + }); + + return { + me: { + id: me.id, + officerLevel: me.officerLevel, + }, + nation: { + id: nation.id, + name: nation.name, + color: nation.color, + level: nation.level, + typeCode: nation.typeCode, + capitalCityId: nation.capitalCityId ?? 0, + rate: resolveNationRate(nation), + }, + chiefStatMin: resolveChiefStatMin(worldState), + cities, + generals, + }; + }), + getPersonnelInfo: authedProcedure.query(async ({ ctx }) => { + const me = await getMyGeneral(ctx); + assertNationAccess(me); + + const [nation, cityRows, troopRows, generalRows, worldState] = await Promise.all([ + ctx.db.nation.findUnique({ + where: { id: me.nationId }, + select: { + id: true, + name: true, + color: true, + level: true, + typeCode: true, + capitalCityId: true, + meta: true, + }, + }), + ctx.db.city.findMany({ + where: { nationId: me.nationId }, + select: { id: true, name: true, level: true, region: true }, + orderBy: { id: 'asc' }, + }), + ctx.db.troop.findMany({ select: { troopLeaderId: true, name: true } }), + ctx.db.general.findMany({ + where: { nationId: me.nationId }, + select: { + id: true, + name: true, + npcState: true, + nationId: true, + cityId: true, + troopId: true, + officerLevel: true, + leadership: true, + strength: true, + intel: true, + experience: true, + dedication: true, + injury: true, + gold: true, + rice: true, + crew: true, + personalCode: true, + specialCode: true, + special2Code: true, + meta: true, + penalty: true, + }, + orderBy: { id: 'asc' }, + }), + ctx.db.worldState.findFirst(), + ]); + + if (!nation) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); + } + + const cityNameMap = new Map(cityRows.map((city) => [city.id, city.name])); + const troopNameMap = new Map(troopRows.map((troop) => [troop.troopLeaderId, troop.name])); + const mappedGenerals = await mapGeneralList(generalRows, cityNameMap, troopNameMap); + + const chiefAssignments = mappedGenerals + .filter((general) => general.officerLevel >= 5) + .reduce>((acc, general) => { + acc[general.officerLevel] = general; + return acc; + }, {}); + + const cityAssignments = cityRows.map((city) => { + const officers = mappedGenerals.filter( + (general) => general.officerLevel >= 2 && general.officerLevel <= 4 && general.officerCity === city.id + ); + + const officerMap: Record = { + 4: null, + 3: null, + 2: null, + }; + + for (const officer of officers) { + officerMap[officer.officerLevel] = officer; + } + + return { + id: city.id, + name: city.name, + level: city.level, + region: city.region, + officers: officerMap, + }; + }); + + const penaltyMap = new Map>( + generalRows.map((row) => [row.id, asRecord(row.penalty)]) + ); + + const permissionCandidates = mappedGenerals + .filter((general) => general.officerLevel !== 12) + .map((general) => { + const penalty = penaltyMap.get(general.id) ?? {}; + const maxPermission = checkSecretMaxPermission(penalty); + return { + id: general.id, + name: general.name, + npcState: general.npcState, + permission: general.permission, + maxPermission, + }; + }); + + const ambassadors = permissionCandidates.filter( + (candidate) => candidate.permission === 'ambassador' || candidate.maxPermission === 4 + ); + const auditors = permissionCandidates.filter( + (candidate) => candidate.permission === 'auditor' || candidate.maxPermission >= 3 + ); + + return { + me: { + id: me.id, + officerLevel: me.officerLevel, + }, + nation: { + id: nation.id, + name: nation.name, + color: nation.color, + level: nation.level, + typeCode: nation.typeCode, + capitalCityId: nation.capitalCityId ?? 0, + }, + chiefStatMin: resolveChiefStatMin(worldState), + generals: mappedGenerals, + chiefAssignments, + cityAssignments, + permissionCandidates: { + ambassadors, + auditors, + }, + }; + }), changePermission: authedProcedure .input( z.object({ diff --git a/app/game-frontend/src/router/index.ts b/app/game-frontend/src/router/index.ts index d898a47..893d6f8 100644 --- a/app/game-frontend/src/router/index.ts +++ b/app/game-frontend/src/router/index.ts @@ -3,6 +3,9 @@ import MainView from '../views/MainView.vue'; import PublicView from '../views/PublicView.vue'; import LoginView from '../views/LoginView.vue'; import JoinView from '../views/JoinView.vue'; +import NationCitiesView from '../views/NationCitiesView.vue'; +import NationGeneralsView from '../views/NationGeneralsView.vue'; +import NationPersonnelView from '../views/NationPersonnelView.vue'; import NotFoundView from '../views/NotFoundView.vue'; import { useSessionStore } from '../stores/session'; @@ -30,6 +33,33 @@ const routes = [ requiresNoGeneral: true, }, }, + { + path: '/nation/cities', + name: 'nation-cities', + component: NationCitiesView, + meta: { + requiresAuth: true, + requiresGeneral: true, + }, + }, + { + path: '/nation/generals', + name: 'nation-generals', + component: NationGeneralsView, + meta: { + requiresAuth: true, + requiresGeneral: true, + }, + }, + { + path: '/nation/personnel', + name: 'nation-personnel', + component: NationPersonnelView, + meta: { + requiresAuth: true, + requiresGeneral: true, + }, + }, { path: '/login', name: 'login', diff --git a/app/game-frontend/src/utils/nationFormat.ts b/app/game-frontend/src/utils/nationFormat.ts new file mode 100644 index 0000000..8fcea66 --- /dev/null +++ b/app/game-frontend/src/utils/nationFormat.ts @@ -0,0 +1,123 @@ +export const officerLevelMapDefault: Record = { + 12: '군주', + 11: '참모', + 10: '제1장군', + 9: '제1모사', + 8: '제2장군', + 7: '제2모사', + 6: '제3장군', + 5: '제3모사', + 4: '태수', + 3: '군사', + 2: '종사', + 1: '일반', + 0: '재야', +}; + +export const officerLevelMapByNationLevel: Record> = { + 7: { + 12: '황제', + 11: '승상', + 10: '표기장군', + 9: '사공', + 8: '거기장군', + 7: '태위', + 6: '위장군', + 5: '사도', + }, + 6: { + 12: '왕', + 11: '광록훈', + 10: '좌장군', + 9: '상서령', + 8: '우장군', + 7: '중서령', + 6: '전장군', + 5: '비서령', + }, + 5: { + 12: '공', + 11: '광록대부', + 10: '안국장군', + 9: '집금오', + 8: '파로장군', + 7: '소부', + }, + 4: { + 12: '주목', + 11: '태사령', + 10: '아문장군', + 9: '낭중', + 8: '호군', + 7: '종사중랑', + }, + 3: { + 12: '주자사', + 11: '주부', + 10: '편장군', + 9: '간의대부', + }, + 2: { + 12: '군벌', + 11: '참모', + 10: '비장군', + 9: '부참모', + }, + 1: { + 12: '영주', + 11: '참모', + }, + 0: { + 12: '두목', + 11: '부두목', + }, +}; + +export const formatOfficerLevelText = (officerLevel: number, nationLevel?: number): string => { + if (officerLevel < 5) { + return officerLevelMapDefault[officerLevel] ?? '???'; + } + + const nationMap = + nationLevel === undefined + ? officerLevelMapDefault + : (officerLevelMapByNationLevel[nationLevel] ?? officerLevelMapDefault); + + return nationMap[officerLevel] ?? (officerLevelMapDefault[officerLevel] ?? '???'); +}; + +export const regionMap: Record = { + 1: '하북', + 2: '중원', + 3: '서북', + 4: '서촉', + 5: '남중', + 6: '초', + 7: '오월', + 8: '동이', +}; + +export const cityLevelMap: Record = { + 1: '수', + 2: '진', + 3: '관', + 4: '이', + 5: '소', + 6: '중', + 7: '대', + 8: '특', +}; + +export const getNationChiefLevel = (nationLevel: number): number => { + const map: Record = { + 7: 5, + 6: 5, + 5: 7, + 4: 7, + 3: 9, + 2: 9, + 1: 11, + 0: 11, + }; + return map[nationLevel] ?? 11; +}; diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index fd33e8e..57a5eab 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -91,6 +91,9 @@ watch(

{{ statusLine }}

+ 세력 도시 + 세력 장수 + 인사부