From 9876a331c14a69bc258f4fac0e1a878fe4ff3647 Mon Sep 17 00:00:00 2001 From: MyungHyun Date: Wed, 1 Apr 2026 17:19:23 +0900 Subject: [PATCH] Add fishing rarity rewards and result art --- package.json | 1 + resource/data/fishing/fish_rarities.json | 56 ++++ src/i18n/locales/en.ts | 3 +- src/i18n/locales/ko.ts | 3 +- src/i18n/types.ts | 1 + src/services/FishingService.ts | 171 ++++++++++-- yarn.lock | 316 ++++++++++++++++++++++- 7 files changed, 526 insertions(+), 25 deletions(-) create mode 100644 resource/data/fishing/fish_rarities.json diff --git a/package.json b/package.json index d11e286..08bea9b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "ioredis": "^5.10.1", "pg": "^8.20.0", "prism-media": "^1.3.5", + "sharp": "^0.34.5", "youtubei.js": "^17.0.1" }, "devDependencies": { diff --git a/resource/data/fishing/fish_rarities.json b/resource/data/fishing/fish_rarities.json new file mode 100644 index 0000000..9584006 --- /dev/null +++ b/resource/data/fishing/fish_rarities.json @@ -0,0 +1,56 @@ +{ + "version": 1, + "description": "Fishing mini-game rarity modifiers and result art background palette.", + "rarities": [ + { + "id": "common", + "displayName": "Common", + "displayNameKo": "일반", + "rollRate": 58, + "rewardMultiplier": 1.0, + "reactionWindowMultiplier": 1.0, + "tensionMultiplier": 1.0, + "backgroundColor": "#6B7280" + }, + { + "id": "uncommon", + "displayName": "Uncommon", + "displayNameKo": "고급", + "rollRate": 24, + "rewardMultiplier": 1.2, + "reactionWindowMultiplier": 1.08, + "tensionMultiplier": 1.08, + "backgroundColor": "#22C55E" + }, + { + "id": "rare", + "displayName": "Rare", + "displayNameKo": "희귀", + "rollRate": 11, + "rewardMultiplier": 1.55, + "reactionWindowMultiplier": 1.16, + "tensionMultiplier": 1.14, + "backgroundColor": "#3B82F6" + }, + { + "id": "epic", + "displayName": "Epic", + "displayNameKo": "영웅", + "rollRate": 5, + "rewardMultiplier": 2.1, + "reactionWindowMultiplier": 1.28, + "tensionMultiplier": 1.24, + "backgroundColor": "#A855F7" + }, + { + "id": "legendary", + "displayName": "Legendary", + "displayNameKo": "전설", + "rollRate": 2, + "rewardMultiplier": 3.0, + "reactionWindowMultiplier": 1.42, + "tensionMultiplier": 1.36, + "backgroundColor": "#F59E0B" + } + ] +} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 2a28e1e..b5c7c63 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -278,13 +278,14 @@ export const en: TranslationSchema = { titleActive: 'Fishing Session', titleEnded: 'Fishing Session Ended', status: 'Status', + rarity: 'Rarity', targetFish: 'Target Fish', distance: 'Distance', tension: 'Line Tension', reward: 'Reward', threadHint: 'Use /fishing cast to play again, or /fishing end to delete the thread.', catchResultTitle: 'Big Catch!', - catchResultBody: 'You caught **{{fish}}** and earned **{{reward}} G**.', + catchResultBody: 'You caught a **{{rarity}} {{fish}}** and earned **{{reward}} G**.', states: { hooked: 'Hooked', resting: 'Resting', diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 23bfbad..855a598 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -278,13 +278,14 @@ export const ko: TranslationSchema = { titleActive: '낚시 세션', titleEnded: '낚시 세션 종료', status: '상태', + rarity: '레어도', targetFish: '대상 물고기', distance: '거리', tension: '끊어짐 게이지', reward: '보상', threadHint: '/fishing cast로 다시 시작하거나 /fishing end로 스레드를 삭제할 수 있습니다.', catchResultTitle: '낚시 성공!', - catchResultBody: '**{{fish}}**를 낚았습니다. **{{reward}} G**를 획득했습니다.', + catchResultBody: '**{{rarity}} {{fish}}**를 낚았습니다. **{{reward}} G**를 획득했습니다.', states: { hooked: '입질 중', resting: '휴식 중', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 6975133..a18d9ae 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -232,6 +232,7 @@ export interface TranslationSchema { titleActive: string; titleEnded: string; status: string; + rarity: string; targetFish: string; distance: string; tension: string; diff --git a/src/services/FishingService.ts b/src/services/FishingService.ts index 4121ad0..6edfaf3 100644 --- a/src/services/FishingService.ts +++ b/src/services/FishingService.ts @@ -14,6 +14,7 @@ } from 'discord.js'; import fs from 'node:fs'; import path from 'node:path'; +import sharp from 'sharp'; import { SupportedLocale, t } from '../i18n'; import { RefinementService } from './RefinementService'; import { logger } from '../utils/logger'; @@ -41,6 +42,21 @@ interface FishingCatalogFile { fish: FishingCatalogEntry[]; } +interface FishingRarityEntry { + id: string; + displayName: string; + displayNameKo?: string; + rollRate: number; + rewardMultiplier: number; + reactionWindowMultiplier: number; + tensionMultiplier: number; + backgroundColor: string; +} + +interface FishingRarityFile { + rarities: FishingRarityEntry[]; +} + interface FishingSession { guildId: string; userId: string; @@ -50,6 +66,7 @@ interface FishingSession { thread: ThreadChannel; controlMessage: Message; currentFish: FishingCatalogEntry; + currentRarity: FishingRarityEntry; fishPosition: FishingDirection; selectedAction: FishingAction | null; phaseStartedAt: number; @@ -71,7 +88,7 @@ const REST_DISTANCE_INCREASE = 8; const DISTANCE_BAR_WIDTH = 12; const TENSION_BAR_WIDTH = 12; const ROUND_DURATION_MS = 2500; -const REACTION_MIN_MS = 1200; +const REACTION_MIN_MS = 1000; const REACTION_GRACE_MS = 300; const SESSION_TICK_MS = 250; @@ -80,6 +97,7 @@ export class FishingService { private static sessionsByThread = new Map(); private static threadEnterPromises = new Map>(); private static fishingCatalog = this.loadFishingCatalog(); + private static fishingRarities = this.loadFishingRarities(); static async enterThread(interaction: ChatInputCommandInteraction) { if (!interaction.guildId || !interaction.channel || interaction.channel.type !== ChannelType.GuildText) { @@ -206,7 +224,7 @@ export class FishingService { } const elapsed = Date.now() - session.phaseStartedAt; - const reactionWindowMs = this.getReactionWindowMs(session.currentFish); + const reactionWindowMs = this.getReactionWindowMs(session.currentFish, session.currentRarity); if ( session.selectedAction @@ -242,11 +260,17 @@ export class FishingService { private static async resolveSuccessfulPull(session: FishingSession) { const distanceReduction = this.rollRange(session.currentFish.distanceReductionByPosition[session.fishPosition]); session.distance = Math.max(0, session.distance - distanceReduction); - session.lineTension = Math.min(MAX_TENSION, session.lineTension + MATCH_TENSION_INCREASE); + session.lineTension = Math.min( + MAX_TENSION, + session.lineTension + Math.max(1, Math.round(MATCH_TENSION_INCREASE * session.currentRarity.tensionMultiplier)), + ); session.status = session.lineTension >= MAX_TENSION ? 'failed' : 'tense'; if (session.distance <= 0) { - const reward = this.rollRange(session.currentFish.rewardGold); + const reward = Math.max( + 1, + Math.round(this.rollRange(session.currentFish.rewardGold) * session.currentRarity.rewardMultiplier), + ); session.reward = reward; await RefinementService.addGold(session.userId, session.guildId, reward); await this.finishSession(session, 'success', false); @@ -263,7 +287,10 @@ export class FishingService { } private static async resolveMiss(session: FishingSession) { - session.lineTension = Math.min(MAX_TENSION, session.lineTension + MISS_TENSION_INCREASE); + session.lineTension = Math.min( + MAX_TENSION, + session.lineTension + Math.max(1, Math.round(MISS_TENSION_INCREASE * session.currentRarity.tensionMultiplier)), + ); session.status = session.selectedAction && session.selectedAction !== 'rest' ? 'missed' : 'hooked'; if (session.lineTension >= MAX_TENSION) { @@ -338,7 +365,7 @@ export class FishingService { } private static buildEmbed( - session: Pick, + session: Pick, ) { const distanceProgress = MAX_DISTANCE - session.distance; const distanceBar = this.buildGauge(distanceProgress, MAX_DISTANCE, DISTANCE_BAR_WIDTH); @@ -347,7 +374,13 @@ export class FishingService { const selectedAction = this.formatSelectedAction(session.selectedAction); const embed = new EmbedBuilder() - .setColor(session.status === 'success' ? 0x57f287 : session.status === 'failed' ? 0xed4245 : 0x5865f2) + .setColor( + session.status === 'success' + ? this.hexToColorInt(session.currentRarity.backgroundColor) + : session.status === 'failed' + ? 0xed4245 + : 0x5865f2, + ) .setTitle( session.status === 'success' || session.status === 'failed' ? t(session.locale, 'commands.fishing.titleEnded') @@ -430,6 +463,7 @@ export class FishingService { thread: ThreadChannel; }) { const currentFish = this.pickFishByRate(); + const currentRarity = this.pickRarityByRate(); const sessionBase = { guildId: params.guildId, userId: params.userId, @@ -438,6 +472,7 @@ export class FishingService { threadId: params.thread.id, thread: params.thread, currentFish, + currentRarity, fishPosition: this.randomDirection(), selectedAction: null, phaseStartedAt: Date.now(), @@ -463,7 +498,9 @@ export class FishingService { this.sessionsByUser.set(this.getUserKey(session.guildId, session.userId), session); this.sessionsByThread.set(session.threadId, session); - logger.info(`[Fishing] Started session for ${session.userId} in thread ${session.threadId}.`); + logger.info( + `[Fishing] Started session for ${session.userId} in thread ${session.threadId} (${session.currentRarity.id} ${session.currentFish.id}).`, + ); session.tickInterval = setInterval(() => { void this.tickSession(session); @@ -507,27 +544,52 @@ export class FishingService { private static async sendCatchResult(session: FishingSession) { const artPath = this.pickRandomArtPath(session.currentFish); + const rarityName = this.getRarityDisplayName(session.currentRarity, session.locale); + const rarityBadge = this.getRarityBadge(session.currentRarity.id); const embed = new EmbedBuilder() - .setColor(0x57f287) - .setTitle(t(session.locale, 'commands.fishing.catchResultTitle')) + .setColor(this.hexToColorInt(session.currentRarity.backgroundColor)) + .setTitle(`${rarityBadge} ${t(session.locale, 'commands.fishing.catchResultTitle')} · ${rarityName}`) .setDescription( t(session.locale, 'commands.fishing.catchResultBody', { + rarity: rarityName, fish: session.currentFish.displayName, reward: String(session.reward ?? 0), }), - ); + ) + .addFields({ + name: t(session.locale, 'commands.fishing.rarity'), + value: rarityName, + inline: true, + }); if (artPath && fs.existsSync(artPath)) { - const fileName = path.basename(artPath); - embed.setImage(`attachment://${fileName}`); - await session.thread.send({ - embeds: [embed], - files: [new AttachmentBuilder(artPath, { name: fileName })], - }); - return; + const fileName = `${session.currentFish.id}_${session.currentRarity.id}.png`; + try { + const composite = await this.composeRarityArt(artPath, session.currentRarity.backgroundColor); + embed.setImage(`attachment://${fileName}`); + await session.thread.send({ + embeds: [embed], + files: [ + new AttachmentBuilder(composite ?? artPath, { name: fileName }), + ], + }); + logger.info( + `[Fishing] Sent catch result with art for ${session.userId} (${session.currentRarity.id} ${session.currentFish.id}).`, + ); + return; + } catch (error) { + logger.warn('Failed to send fishing catch result with art, retrying without attachment:', error); + } } - await session.thread.send({ embeds: [embed] }); + try { + await session.thread.send({ embeds: [embed] }); + logger.info( + `[Fishing] Sent catch result without art for ${session.userId} (${session.currentRarity.id} ${session.currentFish.id}).`, + ); + } catch (error) { + logger.error('Failed to send fishing catch result message:', error); + } } static previewFishLane(position: FishingDirection) { @@ -580,9 +642,24 @@ export class FishingService { return this.fishingCatalog[this.fishingCatalog.length - 1]; } - private static getReactionWindowMs(fish: FishingCatalogEntry) { + private static pickRarityByRate() { + const totalRate = this.fishingRarities.reduce((sum, rarity) => sum + rarity.rollRate, 0); + let roll = Math.random() * totalRate; + + for (const rarity of this.fishingRarities) { + roll -= rarity.rollRate; + if (roll <= 0) { + return rarity; + } + } + + return this.fishingRarities[this.fishingRarities.length - 1]; + } + + private static getReactionWindowMs(fish: FishingCatalogEntry, rarity: FishingRarityEntry) { const configured = Math.round(fish.reactionWindowSec * 1000); - return Math.max(REACTION_MIN_MS, Math.min(configured, ROUND_DURATION_MS)); + const adjusted = Math.round(configured / Math.max(1, rarity.reactionWindowMultiplier)); + return Math.max(REACTION_MIN_MS, Math.min(adjusted, ROUND_DURATION_MS)); } private static rollRange(range: FishingRange) { @@ -606,6 +683,58 @@ export class FishingService { return path.resolve(__dirname, '..', '..', relativePath); } + private static loadFishingRarities(): FishingRarityEntry[] { + const rarityPath = this.resolveResourcePath('resource/data/fishing/fish_rarities.json'); + const raw = fs.readFileSync(rarityPath, 'utf-8'); + const parsed = JSON.parse(raw) as FishingRarityFile; + + if (!parsed.rarities?.length) { + throw new Error('Fishing rarity catalog is empty.'); + } + + return parsed.rarities; + } + + private static getRarityDisplayName(rarity: FishingRarityEntry, locale: SupportedLocale) { + return locale === 'ko' && rarity.displayNameKo ? rarity.displayNameKo : rarity.displayName; + } + + private static getRarityBadge(rarityId: string) { + if (rarityId === 'legendary') return '🟠'; + if (rarityId === 'epic') return '🟣'; + if (rarityId === 'rare') return '🔵'; + if (rarityId === 'uncommon') return '🟢'; + return '⚪'; + } + + private static async composeRarityArt(artPath: string, backgroundColor: string) { + try { + const sourceBuffer = fs.readFileSync(artPath); + const metadata = await sharp(sourceBuffer).metadata(); + const width = metadata.width ?? 512; + const height = metadata.height ?? 512; + + return await sharp({ + create: { + width, + height, + channels: 4, + background: backgroundColor, + }, + }) + .composite([{ input: sourceBuffer }]) + .png() + .toBuffer(); + } catch (error) { + logger.warn('Failed to compose rarity fishing art, falling back to original asset:', error); + return null; + } + } + + private static hexToColorInt(value: string) { + return Number.parseInt(value.replace('#', ''), 16); + } + private static formatSelectedAction(action: FishingAction | null) { if (action === 'left') return '⬅️'; if (action === 'center') return '⏺️'; diff --git a/yarn.lock b/yarn.lock index f5c3357..7af0237 100644 --- a/yarn.lock +++ b/yarn.lock @@ -582,7 +582,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.3": +"@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.7.0": version: 1.9.1 resolution: "@emnapi/runtime@npm:1.9.1" dependencies: @@ -893,6 +893,233 @@ __metadata: languageName: node linkType: hard +"@img/colour@npm:^1.0.0": + version: 1.1.0 + resolution: "@img/colour@npm:1.1.0" + checksum: 10c0/2ebea2c0bbaee73b99badcefa04e1e71d83f36e5369337d3121dca841f4569533c4e2faddda6d62dd247f0d5cca143711f9446c59bcce81e427ba433a7a94a17 + languageName: node + linkType: hard + +"@img/sharp-darwin-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-darwin-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-arm@npm:1.2.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-ppc64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-riscv64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-riscv64@npm:1.2.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linux-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.2.4": + version: 1.2.4 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-arm@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-ppc64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-ppc64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-ppc64": + optional: true + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-riscv64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-riscv64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-riscv64": + optional: true + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-s390x@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linux-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-linuxmusl-x64@npm:0.34.5" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-wasm32@npm:0.34.5" + dependencies: + "@emnapi/runtime": "npm:^1.7.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-arm64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-arm64@npm:0.34.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-ia32@npm:0.34.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.34.5": + version: 0.34.5 + resolution: "@img/sharp-win32-x64@npm:0.34.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@ioredis/commands@npm:1.5.1": version: 1.5.1 resolution: "@ioredis/commands@npm:1.5.1" @@ -2967,7 +3194,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0": +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.1.2": version: 2.1.2 resolution: "detect-libc@npm:2.1.2" checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 @@ -4587,6 +4814,7 @@ __metadata: prettier: "npm:^3.8.1" prism-media: "npm:^1.3.5" prisma: "npm:7.6.0" + sharp: "npm:^0.34.5" ts-jest: "npm:^29.4.6" tsx: "npm:^4.21.0" typescript: "npm:^6.0.2" @@ -5743,6 +5971,90 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.34.5": + version: 0.34.5 + resolution: "sharp@npm:0.34.5" + dependencies: + "@img/colour": "npm:^1.0.0" + "@img/sharp-darwin-arm64": "npm:0.34.5" + "@img/sharp-darwin-x64": "npm:0.34.5" + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + "@img/sharp-linux-arm": "npm:0.34.5" + "@img/sharp-linux-arm64": "npm:0.34.5" + "@img/sharp-linux-ppc64": "npm:0.34.5" + "@img/sharp-linux-riscv64": "npm:0.34.5" + "@img/sharp-linux-s390x": "npm:0.34.5" + "@img/sharp-linux-x64": "npm:0.34.5" + "@img/sharp-linuxmusl-arm64": "npm:0.34.5" + "@img/sharp-linuxmusl-x64": "npm:0.34.5" + "@img/sharp-wasm32": "npm:0.34.5" + "@img/sharp-win32-arm64": "npm:0.34.5" + "@img/sharp-win32-ia32": "npm:0.34.5" + "@img/sharp-win32-x64": "npm:0.34.5" + detect-libc: "npm:^2.1.2" + semver: "npm:^7.7.3" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-ppc64": + optional: true + "@img/sharp-libvips-linux-riscv64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-ppc64": + optional: true + "@img/sharp-linux-riscv64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-arm64": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" -- 2.43.0