forked from xiangwang25/whale-town-end-v2
feat: consolidate skin policy, furniture and world NPC work
This commit is contained in:
@@ -69,10 +69,9 @@ export interface UpdateAccountProfileRequest {
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const FALLBACK_SKIN_ID = 'classic_whale';
|
||||
const FALLBACK_SKIN_ID = 'human_whale_directional_v2_8x4';
|
||||
const LEGACY_PENDING_INITIAL_SKIN_ID = 'pending_initial_skin';
|
||||
const INITIAL_SKIN_IDS = new Set([
|
||||
'classic_whale',
|
||||
'human_whale_directional_v2_8x4',
|
||||
'girl_sailor_turnaround_v2_8x4',
|
||||
]);
|
||||
@@ -147,6 +146,9 @@ export class AccountProfileService {
|
||||
}
|
||||
|
||||
async updateAccountProfile(userId: bigint, update: UpdateAccountProfileRequest): Promise<AccountProfilePayload> {
|
||||
if (update.skin_image_base64) {
|
||||
this.assertCustomSkinCreationAvailable();
|
||||
}
|
||||
const user = await this.usersService.findOne(userId);
|
||||
let profile = await this.ensureProfile(userId);
|
||||
const isInitialCharacterCreation = this.isInitialCharacterPending(profile);
|
||||
@@ -249,7 +251,7 @@ export class AccountProfileService {
|
||||
user_id: userId,
|
||||
skin_id: skinId,
|
||||
tags: {
|
||||
[REGISTRATION_SKIN_GENERATION_AVAILABLE_TAG_KEY]: true,
|
||||
[REGISTRATION_SKIN_GENERATION_AVAILABLE_TAG_KEY]: false,
|
||||
[INITIAL_SKIN_SELECTION_AVAILABLE_TAG_KEY]: true,
|
||||
},
|
||||
current_map: 'plaza',
|
||||
@@ -330,10 +332,12 @@ export class AccountProfileService {
|
||||
return skinIds.some((skinId) => skinId.startsWith('generated_'));
|
||||
}
|
||||
|
||||
async canUseRegistrationSkinGeneration(userId: bigint): Promise<boolean> {
|
||||
const profile = await this.ensureProfile(userId);
|
||||
const tags = this.getProfileTags(profile);
|
||||
return tags[REGISTRATION_SKIN_GENERATION_AVAILABLE_TAG_KEY] === true;
|
||||
assertCustomSkinCreationAvailable(): void {
|
||||
throw new ForbiddenException('自定义与上传皮肤暂未开放');
|
||||
}
|
||||
|
||||
async canUseRegistrationSkinGeneration(_userId: bigint): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async consumeRegistrationSkinGeneration(userId: bigint): Promise<void> {
|
||||
|
||||
62
src/business/auth/skin_defaults.spec.ts
Normal file
62
src/business/auth/skin_defaults.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { AccountProfileService } from './account_profile.service';
|
||||
import { SkinGenerationService } from '../skin_generation/skin_generation.service';
|
||||
import { MALL_ITEMS } from '../mall/mall_catalog';
|
||||
|
||||
const BOY = 'human_whale_directional_v2_8x4';
|
||||
const GIRL = 'girl_sailor_turnaround_v2_8x4';
|
||||
|
||||
describe('Default character policy', () => {
|
||||
let service: AccountProfileService;
|
||||
let profile: any;
|
||||
let profiles: any;
|
||||
let assets: any;
|
||||
let users: any;
|
||||
beforeEach(() => {
|
||||
profile = undefined;
|
||||
const owned = new Set<string>();
|
||||
profiles = {
|
||||
findByUserId: jest.fn(async () => profile),
|
||||
create: jest.fn(async data => (profile = { id: 10n, ...data })),
|
||||
update: jest.fn(async (_id, data) => Object.assign(profile, data)),
|
||||
};
|
||||
assets = {
|
||||
grantAsset: jest.fn(async (_user, _type, id) => owned.add(id)),
|
||||
hasAsset: jest.fn(async (_user, _type, id) => owned.has(id)),
|
||||
listAssetIds: jest.fn(async () => [...owned]),
|
||||
};
|
||||
users = { findOne: jest.fn(async () => ({id: 7n, username:'test'})) };
|
||||
service = new AccountProfileService(users, profiles, assets,
|
||||
{ensureWallet: jest.fn()} as any, {get: jest.fn()} as any, {sendWelcomeEmail: jest.fn()} as any);
|
||||
});
|
||||
|
||||
it.each([undefined, '', 'classic_whale', 'panda_hero_8x4'])('uses boy instead of unsupported initial skin %s', async id => {
|
||||
expect((await service.ensureProfile(7n, id)).skin_id).toBe(BOY);
|
||||
expect(await assets.listAssetIds()).toEqual([BOY]);
|
||||
});
|
||||
it('lets a new player choose the girl and persists the selection', async () => {
|
||||
await service.ensureProfile(7n);
|
||||
const result = await service.updateAccountProfile(7n, {skin_id:GIRL});
|
||||
expect(result.profile.skin_id).toBe(GIRL);
|
||||
expect(profile.tags.initial_skin_selection_available).toBe(false);
|
||||
expect(profile.tags.registration_skin_generation_available).toBe(false);
|
||||
});
|
||||
it('does not grant the retired classic whale through initial selection', async () => {
|
||||
await service.ensureProfile(7n);
|
||||
await expect(service.updateAccountProfile(7n, {skin_id:'classic_whale'})).rejects.toThrow('尚未拥有');
|
||||
expect(await assets.listAssetIds()).toEqual([BOY]);
|
||||
expect(MALL_ITEMS.some(x=>x.skinId==='classic_whale')).toBe(false);
|
||||
});
|
||||
it('blocks upload before account writes or image processing', async () => {
|
||||
await expect(service.updateAccountProfile(7n, {skin_image_base64:'test'})).rejects.toThrow('暂未开放');
|
||||
expect(users.findOne).not.toHaveBeenCalled();
|
||||
expect(profiles.create).not.toHaveBeenCalled();
|
||||
expect(assets.grantAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
it('blocks generation before checking credentials or launching workers', async () => {
|
||||
const config = {get: jest.fn()};
|
||||
const generator = new SkinGenerationService(config as any, service);
|
||||
await expect(generator.createJob(7n,{source_image_base64:'test'})).rejects.toThrow('暂未开放');
|
||||
expect(config.get).not.toHaveBeenCalled();
|
||||
expect(await service.canUseRegistrationSkinGeneration(7n)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -24,22 +24,11 @@ export const MALL_CATEGORIES = [
|
||||
];
|
||||
|
||||
export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
{
|
||||
itemId: 'skin_classic_whale',
|
||||
itemType: 'skin',
|
||||
skinId: 'classic_whale',
|
||||
name: '经典鲸鱼',
|
||||
category: 'outfit',
|
||||
description: '圆润、轻快的鲸鱼居民皮肤,适合喜欢海洋感角色的玩家。',
|
||||
price: 680,
|
||||
tags: ['可预览', '永久', '皮肤'],
|
||||
sortOrder: 10,
|
||||
},
|
||||
{
|
||||
itemId: 'skin_human_whale_directional_v2_8x4',
|
||||
itemType: 'skin',
|
||||
skinId: 'human_whale_directional_v2_8x4',
|
||||
name: '海风行者',
|
||||
name: '海风少年',
|
||||
category: 'outfit',
|
||||
description: '蓝白海风主题的人类角色皮肤,带有鲸鱼小镇风格的服装细节。',
|
||||
price: 680,
|
||||
@@ -85,7 +74,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_whale_floor_rug',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'whale_floor_rug',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_floor_rug.png',
|
||||
icon: 'res://assets/ui/mall/furniture/whale_floor_rug.png',
|
||||
name: '鲸浪地毯',
|
||||
category: 'space',
|
||||
description: '蓝白鲸鱼主题地毯,适合铺在个人房间地板区域。',
|
||||
@@ -97,7 +86,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_whale_memory_board',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'whale_memory_board',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_memory_board.png',
|
||||
icon: 'res://assets/ui/mall/furniture/whale_memory_board.png',
|
||||
name: '鲸语记忆板',
|
||||
category: 'space',
|
||||
description: '挂在房间里的鲸鱼木质装饰板,适合点缀窗边墙面。',
|
||||
@@ -109,7 +98,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_whale_tail_lamp',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'whale_tail_lamp',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_tail_lamp.png',
|
||||
icon: 'res://assets/ui/mall/furniture/whale_tail_lamp.png',
|
||||
name: '鲸尾暖灯',
|
||||
category: 'space',
|
||||
description: '鲸尾造型的温暖装饰灯,可自由摆放在个人房间中。',
|
||||
@@ -121,7 +110,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_boat_cabin_bed',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'boat_cabin_bed',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_boat_cabin_bed.png',
|
||||
icon: 'res://assets/ui/mall/furniture/boat_cabin_bed.png',
|
||||
name: '船舱小床',
|
||||
category: 'space',
|
||||
description: '白木船舱造型的小床,适合放在个人房间地面区域。',
|
||||
@@ -133,7 +122,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_low_wave_bed',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'low_wave_bed',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_low_wave_bed.png',
|
||||
icon: 'res://assets/ui/mall/furniture/low_wave_bed.png',
|
||||
name: '海浪低床',
|
||||
category: 'space',
|
||||
description: '蓝白海浪被面的低矮小床,适合轻松的海风房间。',
|
||||
@@ -145,7 +134,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_whale_tail_headboard_bed',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'whale_tail_headboard_bed',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_tail_headboard_bed.png',
|
||||
icon: 'res://assets/ui/mall/furniture/whale_tail_headboard_bed.png',
|
||||
name: '鲸尾床头床',
|
||||
category: 'space',
|
||||
description: '鲸尾床头和深蓝被面的主题小床,鲸镇特色更明显。',
|
||||
@@ -157,7 +146,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_dev_whale_bookshelf',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'dev_whale_bookshelf',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_dev_whale_bookshelf.png',
|
||||
icon: 'res://assets/ui/mall/furniture/dev_whale_bookshelf.png',
|
||||
name: '程序员鲸书架',
|
||||
category: 'space',
|
||||
description: '带 GitHub、Datawhale 和代码小物件的蓝白书架,适合程序员风格的个人房间。',
|
||||
@@ -169,7 +158,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_datawhale_bug_feature_badge',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'datawhale_bug_feature_badge',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_datawhale_bug_feature_badge.png',
|
||||
icon: 'res://assets/ui/mall/furniture/datawhale_bug_feature_badge.png',
|
||||
name: 'BUG特性徽章',
|
||||
category: 'space',
|
||||
description: '写着“这不是BUG 这是feature”的佛系学习小徽章,适合贴在个人房间墙面。',
|
||||
@@ -181,7 +170,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_datawhale_buddhist_learning_badge',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'datawhale_buddhist_learning_badge',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_datawhale_buddhist_learning_badge.png',
|
||||
icon: 'res://assets/ui/mall/furniture/datawhale_buddhist_learning_badge.png',
|
||||
name: '佛系学习徽章',
|
||||
category: 'space',
|
||||
description: 'Datawhale 佛系学习主题徽章,适合贴在个人房间墙面。',
|
||||
@@ -193,7 +182,7 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
itemId: 'decor_datawhale_ok_working_badge',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'datawhale_ok_working_badge',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_datawhale_ok_working_badge.png',
|
||||
icon: 'res://assets/ui/mall/furniture/datawhale_ok_working_badge.png',
|
||||
name: '已经在做徽章',
|
||||
category: 'space',
|
||||
description: '写着“OKKKK 已经在做了”的工作状态徽章,适合贴在个人房间墙面。',
|
||||
@@ -201,6 +190,86 @@ export const MALL_ITEMS: MallCatalogItem[] = [
|
||||
tags: ['房间家具', '可拖拽', '徽章'],
|
||||
sortOrder: 220,
|
||||
},
|
||||
{
|
||||
itemId: 'decor_low_platform_bed',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'low_platform_bed',
|
||||
name: '航海低平台床',
|
||||
category: 'space',
|
||||
description: '木质低平台床搭配蓝白寝具,购买后可在个人房间自由摆放。',
|
||||
price: 520,
|
||||
tags: [
|
||||
'家具',
|
||||
'可拖拽',
|
||||
'床'
|
||||
],
|
||||
icon: 'res://assets/ui/mall/furniture/low_platform_bed.png',
|
||||
sortOrder: 230
|
||||
},
|
||||
{
|
||||
itemId: 'decor_low_storage_console',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'low_storage_console',
|
||||
name: '海风矮储物柜',
|
||||
category: 'space',
|
||||
description: '蓝色抽屉与暖木柜面的矮储物柜,购买后可在个人房间自由摆放。',
|
||||
price: 420,
|
||||
tags: [
|
||||
'家具',
|
||||
'可拖拽',
|
||||
'柜子'
|
||||
],
|
||||
icon: 'res://assets/ui/mall/furniture/low_storage_console.png',
|
||||
sortOrder: 240
|
||||
},
|
||||
{
|
||||
itemId: 'decor_sea_glass_floor_lamp',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'sea_glass_floor_lamp',
|
||||
name: '海玻璃落地灯',
|
||||
category: 'space',
|
||||
description: '海蓝玻璃灯罩与暖色灯光,为个人房间增添温暖。',
|
||||
price: 340,
|
||||
tags: [
|
||||
'家具',
|
||||
'可拖拽',
|
||||
'灯具'
|
||||
],
|
||||
icon: 'res://assets/ui/mall/furniture/sea_glass_floor_lamp.png',
|
||||
sortOrder: 250
|
||||
},
|
||||
{
|
||||
itemId: 'decor_tide_chart_worktable',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'tide_chart_worktable',
|
||||
name: '潮汐海图工作台',
|
||||
category: 'space',
|
||||
description: '绘有海图的圆形木质工作台,购买后可在个人房间自由摆放。',
|
||||
price: 480,
|
||||
tags: [
|
||||
'家具',
|
||||
'可拖拽',
|
||||
'桌子'
|
||||
],
|
||||
icon: 'res://assets/ui/mall/furniture/tide_chart_worktable.png',
|
||||
sortOrder: 260
|
||||
},
|
||||
{
|
||||
itemId: 'decor_wave_sea_mat',
|
||||
itemType: 'room_decor',
|
||||
decorId: 'wave_sea_mat',
|
||||
name: '海浪编织地垫',
|
||||
category: 'space',
|
||||
description: '绳编边框与蓝色海浪纹样的地垫,可铺在个人房间地板上。',
|
||||
price: 240,
|
||||
tags: [
|
||||
'家具',
|
||||
'可拖拽',
|
||||
'地面'
|
||||
],
|
||||
icon: 'res://assets/ui/mall/furniture/wave_sea_mat.png',
|
||||
sortOrder: 270
|
||||
},
|
||||
];
|
||||
|
||||
export const MALL_SKIN_ITEMS = MALL_ITEMS.filter((item) => item.itemType === 'skin' && item.skinId);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IsString, Length, Matches } from 'class-validator';
|
||||
export class UpdatePlayerAppearanceDto {
|
||||
@ApiProperty({
|
||||
description: '要穿戴的角色皮肤ID',
|
||||
example: 'classic_whale',
|
||||
example: 'human_whale_directional_v2_8x4',
|
||||
})
|
||||
@IsString({ message: '皮肤ID必须是字符串' })
|
||||
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
|
||||
|
||||
100
src/business/room_decor/room_decor.service.spec.ts
Normal file
100
src/business/room_decor/room_decor.service.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { RoomDecorService } from './room_decor.service';
|
||||
import { ROOM_DECOR_DEFINITIONS, ROOM_DECOR_LEGACY_SCALES, findRoomDecorDefinition } from './room_decor_catalog';
|
||||
import { MallService } from '../mall/mall.service';
|
||||
|
||||
describe('Furniture purchase and placement', () => {
|
||||
const userId = BigInt(7);
|
||||
let assets: Set<string>;
|
||||
let rows: Map<string, any>;
|
||||
let inventory: any;
|
||||
let placements: any;
|
||||
let room: RoomDecorService;
|
||||
|
||||
beforeEach(() => {
|
||||
assets = new Set();
|
||||
rows = new Map();
|
||||
inventory = {
|
||||
listInventory: jest.fn(async () => ({ assets: [], skin_ids: [], room_decor_ids: [...assets] })),
|
||||
hasAsset: jest.fn(async (_user, type, id) => type === 'room_decor' && assets.has(id)),
|
||||
grantAsset: jest.fn(async (_user, _type, id) => assets.add(id)),
|
||||
};
|
||||
placements = {
|
||||
listPlacements: jest.fn(async () => [...rows.values()]),
|
||||
savePlacement: jest.fn(async (_user, row) => { rows.set(row.decor_id, { ...row }); return row; }),
|
||||
};
|
||||
room = new RoomDecorService(placements, inventory);
|
||||
});
|
||||
|
||||
it.each(['low_platform_bed', 'low_storage_console', 'sea_glass_floor_lamp', 'tide_chart_worktable', 'wave_sea_mat'])(
|
||||
'makes %s purchasable, placeable and stable after re-entering', async (decorId) => {
|
||||
let balance = 2000;
|
||||
const wallet = async () => ({ balance, currency: 'whale_coin', user_id: '7' });
|
||||
const spend = jest.fn(async (_user, amount) => { balance -= amount; return wallet(); });
|
||||
const mall = new MallService({ getBalance: wallet } as any, inventory, { getWallet: wallet, spend } as any);
|
||||
const catalog = await mall.getCatalog(userId);
|
||||
const item = catalog.items.find((entry) => entry.decorId === decorId)!;
|
||||
expect(item.status).toBe('available');
|
||||
const purchased = await mall.purchaseItem(userId, item.id);
|
||||
expect(purchased.owned_decor_ids).toContain(decorId);
|
||||
expect(spend).toHaveBeenCalledTimes(1);
|
||||
const definition = findRoomDecorDefinition(decorId)!;
|
||||
const owned = (await room.getInventory(userId)).items[0];
|
||||
expect(owned).toMatchObject({ placed: false, texture: definition.texture, scale: definition.default_scale });
|
||||
await room.savePlacement(userId, { decor_id: decorId, placed: true, position_x: 123, position_y: 45 });
|
||||
expect((await room.getInventory(userId)).items[0]).toMatchObject({
|
||||
placed: true, position_x: 123, position_y: 45, scale: definition.default_scale,
|
||||
});
|
||||
expect((await mall.getCatalog(userId)).items.find((entry) => entry.decorId === decorId)?.status).toBe('owned');
|
||||
await mall.purchaseItem(userId, item.id);
|
||||
expect(spend).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(ROOM_DECOR_DEFINITIONS)('preserves current $decor_id placement through repeated saves', async (definition) => {
|
||||
assets.add(definition.decor_id);
|
||||
let placement: any = {
|
||||
decor_id: definition.decor_id, placed: true,
|
||||
position_x: -137, position_y: 86, scale: definition.default_scale, z_index: definition.default_z_index,
|
||||
};
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await room.savePlacement(userId, placement);
|
||||
placement = (await room.getInventory(userId)).items[0];
|
||||
expect(placement).toMatchObject({ position_x: -137, position_y: 86, scale: definition.default_scale });
|
||||
}
|
||||
});
|
||||
|
||||
it('updates a legacy bed size without moving it, then stays stable', async () => {
|
||||
assets.add('boat_cabin_bed');
|
||||
rows.set('boat_cabin_bed', { decor_id: 'boat_cabin_bed', placed: true, position_x: -161, position_y: 25, scale: 0.7, z_index: -9 });
|
||||
const item = (await room.getInventory(userId)).items[0];
|
||||
expect(item).toMatchObject({ position_x: -161, position_y: 25, scale: 0.15 });
|
||||
await room.savePlacement(userId, item);
|
||||
expect((await room.getInventory(userId)).items[0]).toEqual(item);
|
||||
});
|
||||
|
||||
it('preserves a custom scale instead of treating all small beds as legacy', async () => {
|
||||
assets.add('boat_cabin_bed');
|
||||
await room.savePlacement(userId, { decor_id: 'boat_cabin_bed', placed: true, position_x: 81, position_y: 36, scale: 0.21 });
|
||||
expect((await room.getInventory(userId)).items[0]).toMatchObject({ position_x: 81, position_y: 36, scale: 0.21 });
|
||||
});
|
||||
|
||||
it('fits previous artwork scales once and keeps saved room coordinates', async () => {
|
||||
for (const [decorId, scales] of Object.entries(ROOM_DECOR_LEGACY_SCALES)) {
|
||||
assets.clear();
|
||||
rows.clear();
|
||||
assets.add(decorId);
|
||||
for (const scale of scales) {
|
||||
rows.set(decorId, { decor_id: decorId, placed: true, position_x: 81, position_y: 36, scale, z_index: -9 });
|
||||
const fitted = (await room.getInventory(userId)).items[0];
|
||||
expect(fitted).toMatchObject({ position_x: 81, position_y: 36, scale: findRoomDecorDefinition(decorId)!.default_scale });
|
||||
await room.savePlacement(userId, fitted);
|
||||
expect((await room.getInventory(userId)).items[0]).toEqual(fitted);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unowned furniture without writing a placement', async () => {
|
||||
await expect(room.savePlacement(userId, { decor_id: 'low_platform_bed', placed: true })).rejects.toThrow('尚未拥有');
|
||||
expect(placements.savePlacement).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,8 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { InventoryService } from '../player/inventory.service';
|
||||
import { SaveRoomDecorPlacementDto } from './dto/save_room_decor_placement.dto';
|
||||
import {
|
||||
ROOM_DECOR_BED_DEFAULT_SCALE,
|
||||
ROOM_DECOR_DEFINITIONS,
|
||||
ROOM_DECOR_LEGACY_DEFAULTS,
|
||||
ROOM_DECOR_LEGACY_BED_MAX_SCALE,
|
||||
ROOM_DECOR_LEGACY_WALL_DECOR_SCALES,
|
||||
ROOM_DECOR_ROOM_SCALE,
|
||||
ROOM_DECOR_LEGACY_SCALES,
|
||||
findRoomDecorDefinition,
|
||||
} from './room_decor_catalog';
|
||||
|
||||
@@ -104,10 +100,11 @@ export class RoomDecorService {
|
||||
row: UserRoomDecorRow,
|
||||
definition?: { default_scale: number; default_position: { x: number; y: number } },
|
||||
): RoomDecorPayloadPlacement {
|
||||
const usesLegacyPlacement = this.usesLegacyPlacement(row);
|
||||
return {
|
||||
position_x: this.normalizedPositionValue(row.position_x, definition?.default_position.x ?? 0, usesLegacyPlacement),
|
||||
position_y: this.normalizedPositionValue(row.position_y, definition?.default_position.y ?? 0, usesLegacyPlacement),
|
||||
// Positions are already room coordinates. Re-scaling them on every read
|
||||
// makes furniture drift each time a player saves and re-enters the room.
|
||||
position_x: row.position_x ?? definition?.default_position.x ?? 0,
|
||||
position_y: row.position_y ?? definition?.default_position.y ?? 0,
|
||||
scale: this.normalizedScale(row, definition),
|
||||
};
|
||||
}
|
||||
@@ -117,56 +114,12 @@ export class RoomDecorService {
|
||||
if (!row.placed && definition) {
|
||||
return definition.default_scale;
|
||||
}
|
||||
if (this.usesLegacyPlacement(row)) {
|
||||
return definition?.default_scale ?? scale;
|
||||
if (definition && Math.abs(scale - definition.default_scale) <= 0.001) {
|
||||
return scale;
|
||||
}
|
||||
if (this.isBedDecor(row.decor_id) && scale <= ROOM_DECOR_LEGACY_BED_MAX_SCALE) {
|
||||
return ROOM_DECOR_BED_DEFAULT_SCALE;
|
||||
}
|
||||
if (row.decor_id === 'whale_floor_rug' && scale >= 0.22 && scale <= 0.30) {
|
||||
return definition?.default_scale ?? scale;
|
||||
}
|
||||
if (row.decor_id === 'dev_whale_bookshelf' && (Math.abs(scale - 0.4) <= 0.001 || (scale >= 0.51 && scale <= 0.53))) {
|
||||
// Preserve the old room-fit footprint after switching to the larger mall texture.
|
||||
return definition?.default_scale ?? scale;
|
||||
}
|
||||
if (this.isWallBadgeDecor(row.decor_id) && this.isLegacyWallDecorScale(scale)) {
|
||||
return definition?.default_scale ?? scale;
|
||||
}
|
||||
if (row.decor_id === 'whale_tail_lamp' && scale <= 0.2) {
|
||||
return definition?.default_scale ?? scale;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
private normalizedPositionValue(value: number | null, fallback: number, usesLegacyPlacement: boolean) {
|
||||
if (value === null || value === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
return usesLegacyPlacement ? Math.round(value * ROOM_DECOR_ROOM_SCALE) : value;
|
||||
}
|
||||
|
||||
private usesLegacyPlacement(row: UserRoomDecorRow) {
|
||||
const legacy = ROOM_DECOR_LEGACY_DEFAULTS[row.decor_id];
|
||||
if (!legacy) {
|
||||
return false;
|
||||
}
|
||||
const scale = row.scale ?? legacy.scale;
|
||||
if (this.isBedDecor(row.decor_id) && scale <= ROOM_DECOR_LEGACY_BED_MAX_SCALE) {
|
||||
return true;
|
||||
}
|
||||
return Math.abs(scale - legacy.scale) <= 0.001;
|
||||
}
|
||||
|
||||
private isBedDecor(decorId: string) {
|
||||
return decorId.endsWith('_bed');
|
||||
}
|
||||
|
||||
private isWallBadgeDecor(decorId: string) {
|
||||
return decorId.startsWith('datawhale_') && decorId.endsWith('_badge');
|
||||
}
|
||||
|
||||
private isLegacyWallDecorScale(scale: number) {
|
||||
return ROOM_DECOR_LEGACY_WALL_DECOR_SCALES.some((legacyScale) => Math.abs(scale - legacyScale) <= 0.001);
|
||||
const legacyScales = ROOM_DECOR_LEGACY_SCALES[row.decor_id] ?? [];
|
||||
return legacyScales.some((legacy) => Math.abs(scale - legacy) <= 0.001)
|
||||
? definition?.default_scale ?? scale
|
||||
: scale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface RoomDecorDefinition {
|
||||
item_id: string;
|
||||
icon: string;
|
||||
texture?: string;
|
||||
texture_has_shadow?: boolean;
|
||||
default_scale: number;
|
||||
default_position: {
|
||||
x: number;
|
||||
@@ -20,174 +21,204 @@ export interface RoomDecorDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
export interface RoomDecorLegacyDefault {
|
||||
scale: number;
|
||||
default_position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const ROOM_DECOR_ROOM_SCALE = 0.7;
|
||||
export const ROOM_DECOR_BED_DEFAULT_SCALE = ROOM_DECOR_ROOM_SCALE;
|
||||
export const ROOM_DECOR_BOOKSHELF_DEFAULT_SCALE = 0.12;
|
||||
export const ROOM_DECOR_FLOOR_RUG_DEFAULT_SCALE = 1.0;
|
||||
export const ROOM_DECOR_WALL_DECOR_DEFAULT_SCALE = 0.04;
|
||||
export const ROOM_DECOR_LEGACY_BED_MAX_SCALE = 0.35;
|
||||
export const ROOM_DECOR_LEGACY_WALL_DECOR_SCALES = [0.7, 0.18];
|
||||
|
||||
export const ROOM_DECOR_LEGACY_DEFAULTS: Record<string, RoomDecorLegacyDefault> = {
|
||||
whale_floor_rug: {
|
||||
scale: 0.42,
|
||||
default_position: { x: 0, y: 230 },
|
||||
},
|
||||
whale_memory_board: {
|
||||
scale: 0.16,
|
||||
default_position: { x: 260, y: -295 },
|
||||
},
|
||||
whale_tail_lamp: {
|
||||
scale: 0.16,
|
||||
default_position: { x: 330, y: -250 },
|
||||
},
|
||||
boat_cabin_bed: {
|
||||
scale: 1,
|
||||
default_position: { x: -230, y: 35 },
|
||||
},
|
||||
low_wave_bed: {
|
||||
scale: 1,
|
||||
default_position: { x: -140, y: 55 },
|
||||
},
|
||||
whale_tail_headboard_bed: {
|
||||
scale: 1,
|
||||
default_position: { x: 0, y: 45 },
|
||||
},
|
||||
dev_whale_bookshelf: {
|
||||
scale: 1,
|
||||
default_position: { x: -300, y: -55 },
|
||||
},
|
||||
datawhale_bug_feature_badge: {
|
||||
scale: 1,
|
||||
default_position: { x: -300, y: -290 },
|
||||
},
|
||||
datawhale_buddhist_learning_badge: {
|
||||
scale: 1,
|
||||
default_position: { x: 0, y: -290 },
|
||||
},
|
||||
datawhale_ok_working_badge: {
|
||||
scale: 1,
|
||||
default_position: { x: 300, y: -290 },
|
||||
},
|
||||
// Keep known historical scales aligned with the frontend RoomDecorCatalog.
|
||||
export const ROOM_DECOR_LEGACY_SCALES: Record<string, number[]> = {
|
||||
whale_floor_rug: [0.42, 1, 0.22, 0.25, 0.28, 0.3, 0.2],
|
||||
whale_memory_board: [0.16, 0.11, 0.23],
|
||||
whale_tail_lamp: [0.16, 1, 0.19],
|
||||
boat_cabin_bed: [1, 0.7, 0.3, 0.35, 0.27],
|
||||
low_wave_bed: [1, 0.7, 0.3, 0.35, 0.27],
|
||||
whale_tail_headboard_bed: [1, 0.7, 0.3, 0.35, 0.25],
|
||||
dev_whale_bookshelf: [1, 0.12, 0.4, 0.52, 0.2],
|
||||
datawhale_bug_feature_badge: [1, 0.7, 0.18, 0.04, 0.055],
|
||||
datawhale_buddhist_learning_badge: [1, 0.7, 0.18, 0.04, 0.055],
|
||||
datawhale_ok_working_badge: [1, 0.7, 0.18, 0.04, 0.055],
|
||||
low_platform_bed: [0.4, 0.22],
|
||||
low_storage_console: [0.32],
|
||||
sea_glass_floor_lamp: [0.4],
|
||||
tide_chart_worktable: [0.19],
|
||||
wave_sea_mat: [0.4],
|
||||
};
|
||||
|
||||
export const ROOM_DECOR_DEFINITIONS: RoomDecorDefinition[] = [
|
||||
{
|
||||
texture_has_shadow: true,
|
||||
decor_id: 'whale_floor_rug',
|
||||
item_id: 'decor_whale_floor_rug',
|
||||
name: '鲸浪地毯',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_floor_rug.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_floor_rug_roomfit.png',
|
||||
default_scale: ROOM_DECOR_FLOOR_RUG_DEFAULT_SCALE,
|
||||
default_position: { x: 0, y: 161 },
|
||||
icon: 'res://assets/ui/mall/furniture/whale_floor_rug.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_floor_rug_room_reference_v1.png',
|
||||
default_position: { x: 0, y: 100 },
|
||||
default_scale: 0.14,
|
||||
default_z_index: -8,
|
||||
item_id: 'decor_whale_floor_rug',
|
||||
},
|
||||
{
|
||||
decor_id: 'whale_memory_board',
|
||||
item_id: 'decor_whale_memory_board',
|
||||
name: '鲸语记忆板',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_memory_board.png',
|
||||
default_scale: 0.11,
|
||||
default_position: { x: 182, y: -207 },
|
||||
icon: 'res://assets/ui/mall/furniture/whale_memory_board.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_memory_board_room_reference_v1.png',
|
||||
default_position: { x: 190, y: -235 },
|
||||
default_scale: 0.12,
|
||||
default_z_index: -14,
|
||||
item_id: 'decor_whale_memory_board',
|
||||
},
|
||||
{
|
||||
decor_id: 'whale_tail_lamp',
|
||||
item_id: 'decor_whale_tail_lamp',
|
||||
name: '鲸尾暖灯',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_tail_lamp.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_tail_lamp_roomfit.png',
|
||||
default_scale: 1,
|
||||
default_position: { x: 231, y: -175 },
|
||||
default_z_index: -10,
|
||||
collision_size: { x: 50, y: 32 },
|
||||
collision_offset: { x: 0, y: 56 },
|
||||
icon: 'res://assets/ui/mall/furniture/whale_tail_lamp.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_tail_lamp_room_reference_v1.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: 20, y: -10 },
|
||||
default_scale: 0.09,
|
||||
default_z_index: -8,
|
||||
collision_size: { x: 245, y: 110 },
|
||||
collision_offset: { x: 0, y: 327 },
|
||||
item_id: 'decor_whale_tail_lamp',
|
||||
},
|
||||
{
|
||||
texture_has_shadow: true,
|
||||
decor_id: 'boat_cabin_bed',
|
||||
item_id: 'decor_boat_cabin_bed',
|
||||
name: '船舱小床',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_boat_cabin_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_boat_cabin_bed_roomfit.png',
|
||||
default_scale: ROOM_DECOR_BED_DEFAULT_SCALE,
|
||||
default_position: { x: -161, y: 25 },
|
||||
icon: 'res://assets/ui/mall/furniture/boat_cabin_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_boat_cabin_bed_room_reference_v1.png',
|
||||
default_position: { x: -180, y: 10 },
|
||||
default_scale: 0.15,
|
||||
default_z_index: -9,
|
||||
collision_size: { x: 220, y: 112 },
|
||||
collision_offset: { x: 0, y: 52 },
|
||||
collision_size: { x: 560, y: 520 },
|
||||
collision_offset: { x: 0, y: 95 },
|
||||
item_id: 'decor_boat_cabin_bed',
|
||||
},
|
||||
{
|
||||
decor_id: 'low_wave_bed',
|
||||
item_id: 'decor_low_wave_bed',
|
||||
name: '海浪低床',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_low_wave_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_low_wave_bed_roomfit.png',
|
||||
default_scale: ROOM_DECOR_BED_DEFAULT_SCALE,
|
||||
default_position: { x: -98, y: 39 },
|
||||
icon: 'res://assets/ui/mall/furniture/low_wave_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_low_wave_bed_room_reference_v1.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: -180, y: 10 },
|
||||
default_scale: 0.15,
|
||||
default_z_index: -9,
|
||||
collision_size: { x: 220, y: 112 },
|
||||
collision_offset: { x: 0, y: 56 },
|
||||
collision_size: { x: 560, y: 520 },
|
||||
collision_offset: { x: 0, y: 95 },
|
||||
item_id: 'decor_low_wave_bed',
|
||||
},
|
||||
{
|
||||
texture_has_shadow: true,
|
||||
decor_id: 'whale_tail_headboard_bed',
|
||||
item_id: 'decor_whale_tail_headboard_bed',
|
||||
name: '鲸尾床头床',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_whale_tail_headboard_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_tail_headboard_bed_roomfit.png',
|
||||
default_scale: ROOM_DECOR_BED_DEFAULT_SCALE,
|
||||
default_position: { x: 0, y: 32 },
|
||||
icon: 'res://assets/ui/mall/furniture/whale_tail_headboard_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_whale_tail_headboard_bed_room_reference_v1.png',
|
||||
default_position: { x: 180, y: 10 },
|
||||
default_scale: 0.16,
|
||||
default_z_index: -9,
|
||||
collision_size: { x: 214, y: 112 },
|
||||
collision_offset: { x: 0, y: 62 },
|
||||
collision_size: { x: 540, y: 520 },
|
||||
collision_offset: { x: 0, y: 95 },
|
||||
item_id: 'decor_whale_tail_headboard_bed',
|
||||
},
|
||||
{
|
||||
decor_id: 'dev_whale_bookshelf',
|
||||
item_id: 'decor_dev_whale_bookshelf',
|
||||
name: '程序员鲸书架',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_dev_whale_bookshelf.png',
|
||||
texture: 'res://assets/ui/mall/items/room_decor_dev_whale_bookshelf.png',
|
||||
default_scale: ROOM_DECOR_BOOKSHELF_DEFAULT_SCALE,
|
||||
default_position: { x: -210, y: -39 },
|
||||
icon: 'res://assets/ui/mall/furniture/dev_whale_bookshelf.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_dev_whale_bookshelf_room_reference_v1.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: -195, y: -190 },
|
||||
default_scale: 0.125,
|
||||
default_z_index: -10,
|
||||
collision_size: { x: 626.667, y: 226.667 },
|
||||
collision_offset: { x: 0, y: 580 },
|
||||
collision_size: { x: 790, y: 180 },
|
||||
collision_offset: { x: 0, y: 270 },
|
||||
item_id: 'decor_dev_whale_bookshelf',
|
||||
},
|
||||
{
|
||||
decor_id: 'datawhale_bug_feature_badge',
|
||||
item_id: 'decor_datawhale_bug_feature_badge',
|
||||
name: 'BUG特性徽章',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_datawhale_bug_feature_badge.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_datawhale_bug_feature_badge_hires_clean.png',
|
||||
default_scale: ROOM_DECOR_WALL_DECOR_DEFAULT_SCALE,
|
||||
default_position: { x: -210, y: -203 },
|
||||
icon: 'res://assets/ui/mall/furniture/datawhale_bug_feature_badge.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_datawhale_bug_feature_badge_room_reference_v1.png',
|
||||
default_position: { x: -220, y: -204 },
|
||||
default_scale: 0.033,
|
||||
default_z_index: -14,
|
||||
item_id: 'decor_datawhale_bug_feature_badge',
|
||||
},
|
||||
{
|
||||
decor_id: 'datawhale_buddhist_learning_badge',
|
||||
item_id: 'decor_datawhale_buddhist_learning_badge',
|
||||
name: '佛系学习徽章',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_datawhale_buddhist_learning_badge.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_datawhale_buddhist_learning_badge_hires_clean.png',
|
||||
default_scale: ROOM_DECOR_WALL_DECOR_DEFAULT_SCALE,
|
||||
default_position: { x: 0, y: -203 },
|
||||
icon: 'res://assets/ui/mall/furniture/datawhale_buddhist_learning_badge.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_datawhale_buddhist_learning_badge_room_reference_v1.png',
|
||||
default_position: { x: 0, y: -300 },
|
||||
default_scale: 0.033,
|
||||
default_z_index: -14,
|
||||
item_id: 'decor_datawhale_buddhist_learning_badge',
|
||||
},
|
||||
{
|
||||
decor_id: 'datawhale_ok_working_badge',
|
||||
item_id: 'decor_datawhale_ok_working_badge',
|
||||
name: '已经在做徽章',
|
||||
icon: 'res://assets/ui/mall/items/room_decor_datawhale_ok_working_badge.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_datawhale_ok_working_badge_hires_clean.png',
|
||||
default_scale: ROOM_DECOR_WALL_DECOR_DEFAULT_SCALE,
|
||||
default_position: { x: 210, y: -203 },
|
||||
icon: 'res://assets/ui/mall/furniture/datawhale_ok_working_badge.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_datawhale_ok_working_badge_room_reference_v1.png',
|
||||
default_position: { x: 220, y: -204 },
|
||||
default_scale: 0.033,
|
||||
default_z_index: -14,
|
||||
item_id: 'decor_datawhale_ok_working_badge',
|
||||
},
|
||||
{
|
||||
decor_id: 'low_platform_bed',
|
||||
name: '航海低平台床',
|
||||
icon: 'res://assets/ui/mall/furniture/low_platform_bed.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_low_platform_bed_roomfit.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: -180, y: 20 },
|
||||
default_scale: 0.3,
|
||||
default_z_index: -9,
|
||||
collision_size: { x: 360, y: 170 },
|
||||
collision_offset: { x: 0, y: 100 },
|
||||
item_id: 'decor_low_platform_bed',
|
||||
},
|
||||
{
|
||||
decor_id: 'low_storage_console',
|
||||
name: '海风矮储物柜',
|
||||
icon: 'res://assets/ui/mall/furniture/low_storage_console.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_low_storage_console_roomfit.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: 190, y: -160 },
|
||||
default_scale: 0.19,
|
||||
default_z_index: -10,
|
||||
collision_size: { x: 500, y: 65 },
|
||||
collision_offset: { x: 0, y: 115 },
|
||||
item_id: 'decor_low_storage_console',
|
||||
},
|
||||
{
|
||||
decor_id: 'sea_glass_floor_lamp',
|
||||
name: '海玻璃落地灯',
|
||||
icon: 'res://assets/ui/mall/furniture/sea_glass_floor_lamp.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_sea_glass_floor_lamp_roomfit.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: 300, y: -30 },
|
||||
default_scale: 0.22,
|
||||
default_z_index: -8,
|
||||
collision_size: { x: 150, y: 70 },
|
||||
collision_offset: { x: 0, y: 145 },
|
||||
item_id: 'decor_sea_glass_floor_lamp',
|
||||
},
|
||||
{
|
||||
decor_id: 'tide_chart_worktable',
|
||||
name: '潮汐海图工作台',
|
||||
icon: 'res://assets/ui/mall/furniture/tide_chart_worktable.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_tide_chart_worktable_roomfit.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: 130, y: 70 },
|
||||
default_scale: 0.1,
|
||||
default_z_index: -9,
|
||||
collision_size: { x: 850, y: 180 },
|
||||
collision_offset: { x: 0, y: 250 },
|
||||
item_id: 'decor_tide_chart_worktable',
|
||||
},
|
||||
{
|
||||
decor_id: 'wave_sea_mat',
|
||||
name: '海浪编织地垫',
|
||||
icon: 'res://assets/ui/mall/furniture/wave_sea_mat.png',
|
||||
texture: 'res://assets/maps/personal_space/v1/decor/room_decor_wave_sea_mat_roomfit.png',
|
||||
texture_has_shadow: true,
|
||||
default_position: { x: 0, y: 115 },
|
||||
default_scale: 0.24,
|
||||
default_z_index: -8,
|
||||
item_id: 'decor_wave_sea_mat',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export class SkinGenerationService {
|
||||
) {}
|
||||
|
||||
async createJob(userId: bigint, dto: CreateSkinGenerationJobDto): Promise<SkinGenerationJobResponse> {
|
||||
this.accountProfileService.assertCustomSkinCreationAvailable();
|
||||
const apiKey = this.configService.get<string>('NOVAMAILIO_API_KEY') || process.env.NOVAMAILIO_API_KEY;
|
||||
if (!apiKey || apiKey.trim().length === 0) {
|
||||
throw new BadRequestException('服务端尚未配置 NOVAMAILIO_API_KEY,无法生成角色皮肤');
|
||||
|
||||
@@ -29,7 +29,7 @@ export const WORLD_NPC_DEFINITIONS: readonly WorldNpcDefinition[] = [
|
||||
dailyFocus: '巡视码头与广场,收集水路消息并帮助居民',
|
||||
homeLocationId: 'square_dock_guide',
|
||||
stationary: true,
|
||||
fixedPosition: { x: -825, y: 437 },
|
||||
fixedPosition: { x: -825, y: 475 },
|
||||
scene: 'dock_crayfish',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ export const WORLD_LOCATIONS: readonly WorldLocation[] = [
|
||||
{
|
||||
id: 'square_dock_guide', mapId: 'whale_port', name: '码头向导岗', x: -720, y: 437,
|
||||
tags: ['organize', 'socialize', 'reflect'],
|
||||
slots: [{ x: -900, y: 437 }, { x: -780, y: 437 }, { x: -660, y: 437 }, { x: -540, y: 437 }],
|
||||
// Approach the inland path below the dock's southeast mooring post.
|
||||
slots: [{ x: -900, y: 480 }, { x: -780, y: 437 }, { x: -660, y: 437 }, { x: -540, y: 437 }],
|
||||
},
|
||||
{
|
||||
id: 'square_dock_research', mapId: 'whale_port', name: '广场海边研究点', x: -400, y: -180,
|
||||
@@ -28,10 +29,9 @@ export const WORLD_LOCATIONS: readonly WorldLocation[] = [
|
||||
{
|
||||
id: 'square_forum', mapId: 'whale_port', name: '广场交流区', x: 0, y: -280,
|
||||
tags: ['socialize', 'share'],
|
||||
// Keep every visual standing point clear of the fountain footprint. The
|
||||
// last point is used by Niulai; -150 is too close once his sprite height
|
||||
// and ground anchor are accounted for.
|
||||
slots: [{ x: 0, y: -430 }, { x: 0, y: -340 }, { x: 0, y: 325 }, { x: 0, y: -240 }],
|
||||
// All slots connect directly to the north walkway. Keep them on this side
|
||||
// of the fountain so entering or leaving a slot never crosses the basin.
|
||||
slots: [{ x: 0, y: -430 }, { x: 0, y: -340 }, { x: 160, y: -380 }, { x: 0, y: -240 }],
|
||||
},
|
||||
{
|
||||
id: 'square_notice_board', mapId: 'whale_port', name: '广场公告栏', x: -520, y: 480,
|
||||
|
||||
@@ -146,7 +146,7 @@ export class CreateUserProfileDto {
|
||||
*/
|
||||
@ApiPropertyOptional({
|
||||
description: '角色皮肤ID',
|
||||
example: 'classic_whale',
|
||||
example: 'human_whale_directional_v2_8x4',
|
||||
maxLength: 100
|
||||
})
|
||||
@IsOptional()
|
||||
|
||||
@@ -139,7 +139,7 @@ export class RegisterDto {
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '初始角色皮肤ID(可选)',
|
||||
example: 'classic_whale',
|
||||
example: 'human_whale_directional_v2_8x4',
|
||||
required: false,
|
||||
maxLength: 100
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user