feat: add user icon library and in-game selection
This commit is contained in:
@@ -261,6 +261,7 @@ const zAdjustGeneralIcon = z
|
||||
picture: z.string().min(1),
|
||||
imageServer: z.number().int().nonnegative(),
|
||||
iconRevision: z.string().refine(isCanonicalIsoTimestamp),
|
||||
enforceCooldown: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -312,6 +313,9 @@ const zSelectPoolCreate = z
|
||||
uniqueName: z.string().min(1).max(20),
|
||||
personality: z.string().min(1),
|
||||
seedOwnerIdentity: z.union([z.string().min(1), zFiniteNumber]),
|
||||
ownerPicture: z.string().optional(),
|
||||
ownerImageServer: z.number().int().nonnegative().optional(),
|
||||
ownerIconRevision: z.string().refine(isCanonicalIsoTimestamp).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -515,6 +515,9 @@ export const createGeneralFromSelectionPool = async (options: {
|
||||
personality: string;
|
||||
now?: Date;
|
||||
seedOwnerIdentity?: string | number;
|
||||
ownerPicture?: string;
|
||||
ownerImageServer?: number;
|
||||
ownerIconRevision?: string;
|
||||
}): Promise<{ ok: true; generalId: number }> => {
|
||||
const { db, world, worldState, userId, ownerDisplayName, uniqueName } = options;
|
||||
requirePoolWorld(worldState);
|
||||
@@ -555,7 +558,10 @@ export const createGeneralFromSelectionPool = async (options: {
|
||||
);
|
||||
const prestartDeleteAfter = buildPrestartDeleteAfter(now, worldState.tickSeconds, config);
|
||||
const showImgLevel = asNumber(config.showImgLevel, 0);
|
||||
const picture = showImgLevel >= 3 ? info.picture : 'default.jpg';
|
||||
const useOwnerPicture =
|
||||
showImgLevel >= 1 && typeof options.ownerPicture === 'string' && options.ownerPicture !== 'default.jpg';
|
||||
const picture = useOwnerPicture ? options.ownerPicture! : showImgLevel >= 3 ? info.picture : 'default.jpg';
|
||||
const imageServer = useOwnerPicture ? (options.ownerImageServer ?? 1) : info.imgsvr;
|
||||
const defaultSpecialWar =
|
||||
typeof configConst.defaultSpecialWar === 'string' ? configConst.defaultSpecialWar : 'None';
|
||||
const personality = resolveSelectedPersonality(worldState, seedOwnerIdentity, uniqueName, options.personality);
|
||||
@@ -576,7 +582,7 @@ export const createGeneralFromSelectionPool = async (options: {
|
||||
bornYear: worldState.currentYear - age,
|
||||
deadYear: worldState.currentYear + 60,
|
||||
picture,
|
||||
imageServer: info.imgsvr,
|
||||
imageServer,
|
||||
stats: {
|
||||
leadership: info.leadership,
|
||||
strength: info.strength,
|
||||
@@ -630,6 +636,9 @@ export const createGeneralFromSelectionPool = async (options: {
|
||||
next_change: nextChangeAt.toISOString(),
|
||||
nextChangeAt: nextChangeAt.toISOString(),
|
||||
prestart_delete_after: prestartDeleteAfter.toISOString(),
|
||||
...(useOwnerPicture && options.ownerIconRevision
|
||||
? { accountIconUpdatedAt: options.ownerIconRevision }
|
||||
: {}),
|
||||
npc_org: 0,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -326,6 +326,9 @@ async function handleSelectPoolCreate(
|
||||
uniqueName: command.uniqueName,
|
||||
personality: command.personality,
|
||||
seedOwnerIdentity: command.seedOwnerIdentity,
|
||||
...(command.ownerPicture ? { ownerPicture: command.ownerPicture } : {}),
|
||||
...(command.ownerImageServer !== undefined ? { ownerImageServer: command.ownerImageServer } : {}),
|
||||
...(command.ownerIconRevision ? { ownerIconRevision: command.ownerIconRevision } : {}),
|
||||
now: acceptedAt,
|
||||
})),
|
||||
};
|
||||
@@ -711,7 +714,7 @@ async function handleAdjustGeneralIcon(
|
||||
command: Extract<TurnDaemonCommand, { type: 'adjustGeneralIcon' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const db = requireCommandDatabase(ctx);
|
||||
await resolveCommandAcceptedAt(db, command);
|
||||
const acceptedAt = await resolveCommandAcceptedAt(db, command);
|
||||
const general = ctx.world
|
||||
.listGenerals()
|
||||
.find((candidate) => candidate.userId === command.userId && candidate.npcState === 0);
|
||||
@@ -724,6 +727,44 @@ async function handleAdjustGeneralIcon(
|
||||
};
|
||||
}
|
||||
|
||||
if (command.enforceCooldown) {
|
||||
if (general.picture === command.picture && general.imageServer === command.imageServer) {
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: true,
|
||||
generalId: general.id,
|
||||
updated: false,
|
||||
};
|
||||
}
|
||||
const changedAt = general.meta.generalIconChangedAt;
|
||||
if (typeof changedAt === 'string' && isCanonicalIsoTimestamp(changedAt)) {
|
||||
const availableAt = new Date(new Date(changedAt).getTime() + 24 * 60 * 60 * 1000);
|
||||
if (availableAt.getTime() > acceptedAt.getTime()) {
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: false,
|
||||
code: 'TOO_MANY_REQUESTS',
|
||||
reason: `${availableAt.toISOString()}부터 전용 아이콘을 다시 바꿀 수 있습니다.`,
|
||||
availableAt: availableAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
ctx.world.updateGeneral(general.id, {
|
||||
picture: command.picture,
|
||||
imageServer: command.imageServer,
|
||||
meta: {
|
||||
...general.meta,
|
||||
generalIconChangedAt: acceptedAt.toISOString(),
|
||||
},
|
||||
});
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: true,
|
||||
generalId: general.id,
|
||||
updated: true,
|
||||
};
|
||||
}
|
||||
|
||||
const currentRevision = general.meta.accountIconUpdatedAt;
|
||||
if (currentRevision !== undefined) {
|
||||
if (typeof currentRevision !== 'string' || !isCanonicalIsoTimestamp(currentRevision)) {
|
||||
|
||||
Reference in New Issue
Block a user