Add fishing rarity rewards and result art #5

Merged
myong merged 1 commits from myong_dev into main 2026-04-01 08:20:43 +00:00
7 changed files with 526 additions and 25 deletions

View File

@ -13,6 +13,7 @@
"ioredis": "^5.10.1", "ioredis": "^5.10.1",
"pg": "^8.20.0", "pg": "^8.20.0",
"prism-media": "^1.3.5", "prism-media": "^1.3.5",
"sharp": "^0.34.5",
"youtubei.js": "^17.0.1" "youtubei.js": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -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"
}
]
}

View File

@ -278,13 +278,14 @@ export const en: TranslationSchema = {
titleActive: 'Fishing Session', titleActive: 'Fishing Session',
titleEnded: 'Fishing Session Ended', titleEnded: 'Fishing Session Ended',
status: 'Status', status: 'Status',
rarity: 'Rarity',
targetFish: 'Target Fish', targetFish: 'Target Fish',
distance: 'Distance', distance: 'Distance',
tension: 'Line Tension', tension: 'Line Tension',
reward: 'Reward', reward: 'Reward',
threadHint: 'Use /fishing cast to play again, or /fishing end to delete the thread.', threadHint: 'Use /fishing cast to play again, or /fishing end to delete the thread.',
catchResultTitle: 'Big Catch!', catchResultTitle: 'Big Catch!',
catchResultBody: 'You caught **{{fish}}** and earned **{{reward}} G**.', catchResultBody: 'You caught a **{{rarity}} {{fish}}** and earned **{{reward}} G**.',
states: { states: {
hooked: 'Hooked', hooked: 'Hooked',
resting: 'Resting', resting: 'Resting',

View File

@ -278,13 +278,14 @@ export const ko: TranslationSchema = {
titleActive: '낚시 세션', titleActive: '낚시 세션',
titleEnded: '낚시 세션 종료', titleEnded: '낚시 세션 종료',
status: '상태', status: '상태',
rarity: '레어도',
targetFish: '대상 물고기', targetFish: '대상 물고기',
distance: '거리', distance: '거리',
tension: '끊어짐 게이지', tension: '끊어짐 게이지',
reward: '보상', reward: '보상',
threadHint: '/fishing cast로 다시 시작하거나 /fishing end로 스레드를 삭제할 수 있습니다.', threadHint: '/fishing cast로 다시 시작하거나 /fishing end로 스레드를 삭제할 수 있습니다.',
catchResultTitle: '낚시 성공!', catchResultTitle: '낚시 성공!',
catchResultBody: '**{{fish}}**를 낚았습니다. **{{reward}} G**를 획득했습니다.', catchResultBody: '**{{rarity}} {{fish}}**를 낚았습니다. **{{reward}} G**를 획득했습니다.',
states: { states: {
hooked: '입질 중', hooked: '입질 중',
resting: '휴식 중', resting: '휴식 중',

View File

@ -232,6 +232,7 @@ export interface TranslationSchema {
titleActive: string; titleActive: string;
titleEnded: string; titleEnded: string;
status: string; status: string;
rarity: string;
targetFish: string; targetFish: string;
distance: string; distance: string;
tension: string; tension: string;

View File

@ -14,6 +14,7 @@
} from 'discord.js'; } from 'discord.js';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import sharp from 'sharp';
import { SupportedLocale, t } from '../i18n'; import { SupportedLocale, t } from '../i18n';
import { RefinementService } from './RefinementService'; import { RefinementService } from './RefinementService';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
@ -41,6 +42,21 @@ interface FishingCatalogFile {
fish: FishingCatalogEntry[]; 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 { interface FishingSession {
guildId: string; guildId: string;
userId: string; userId: string;
@ -50,6 +66,7 @@ interface FishingSession {
thread: ThreadChannel; thread: ThreadChannel;
controlMessage: Message; controlMessage: Message;
currentFish: FishingCatalogEntry; currentFish: FishingCatalogEntry;
currentRarity: FishingRarityEntry;
fishPosition: FishingDirection; fishPosition: FishingDirection;
selectedAction: FishingAction | null; selectedAction: FishingAction | null;
phaseStartedAt: number; phaseStartedAt: number;
@ -71,7 +88,7 @@ const REST_DISTANCE_INCREASE = 8;
const DISTANCE_BAR_WIDTH = 12; const DISTANCE_BAR_WIDTH = 12;
const TENSION_BAR_WIDTH = 12; const TENSION_BAR_WIDTH = 12;
const ROUND_DURATION_MS = 2500; const ROUND_DURATION_MS = 2500;
const REACTION_MIN_MS = 1200; const REACTION_MIN_MS = 1000;
const REACTION_GRACE_MS = 300; const REACTION_GRACE_MS = 300;
const SESSION_TICK_MS = 250; const SESSION_TICK_MS = 250;
@ -80,6 +97,7 @@ export class FishingService {
private static sessionsByThread = new Map<string, FishingSession>(); private static sessionsByThread = new Map<string, FishingSession>();
private static threadEnterPromises = new Map<string, Promise<{ thread: ThreadChannel; existed: boolean }>>(); private static threadEnterPromises = new Map<string, Promise<{ thread: ThreadChannel; existed: boolean }>>();
private static fishingCatalog = this.loadFishingCatalog(); private static fishingCatalog = this.loadFishingCatalog();
private static fishingRarities = this.loadFishingRarities();
static async enterThread(interaction: ChatInputCommandInteraction) { static async enterThread(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId || !interaction.channel || interaction.channel.type !== ChannelType.GuildText) { if (!interaction.guildId || !interaction.channel || interaction.channel.type !== ChannelType.GuildText) {
@ -206,7 +224,7 @@ export class FishingService {
} }
const elapsed = Date.now() - session.phaseStartedAt; const elapsed = Date.now() - session.phaseStartedAt;
const reactionWindowMs = this.getReactionWindowMs(session.currentFish); const reactionWindowMs = this.getReactionWindowMs(session.currentFish, session.currentRarity);
if ( if (
session.selectedAction session.selectedAction
@ -242,11 +260,17 @@ export class FishingService {
private static async resolveSuccessfulPull(session: FishingSession) { private static async resolveSuccessfulPull(session: FishingSession) {
const distanceReduction = this.rollRange(session.currentFish.distanceReductionByPosition[session.fishPosition]); const distanceReduction = this.rollRange(session.currentFish.distanceReductionByPosition[session.fishPosition]);
session.distance = Math.max(0, session.distance - distanceReduction); 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'; session.status = session.lineTension >= MAX_TENSION ? 'failed' : 'tense';
if (session.distance <= 0) { 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; session.reward = reward;
await RefinementService.addGold(session.userId, session.guildId, reward); await RefinementService.addGold(session.userId, session.guildId, reward);
await this.finishSession(session, 'success', false); await this.finishSession(session, 'success', false);
@ -263,7 +287,10 @@ export class FishingService {
} }
private static async resolveMiss(session: FishingSession) { 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'; session.status = session.selectedAction && session.selectedAction !== 'rest' ? 'missed' : 'hooked';
if (session.lineTension >= MAX_TENSION) { if (session.lineTension >= MAX_TENSION) {
@ -338,7 +365,7 @@ export class FishingService {
} }
private static buildEmbed( private static buildEmbed(
session: Pick<FishingSession, 'locale' | 'currentFish' | 'fishPosition' | 'selectedAction' | 'phaseStartedAt' | 'distance' | 'lineTension' | 'status' | 'reward'>, session: Pick<FishingSession, 'locale' | 'currentFish' | 'currentRarity' | 'fishPosition' | 'selectedAction' | 'phaseStartedAt' | 'distance' | 'lineTension' | 'status' | 'reward'>,
) { ) {
const distanceProgress = MAX_DISTANCE - session.distance; const distanceProgress = MAX_DISTANCE - session.distance;
const distanceBar = this.buildGauge(distanceProgress, MAX_DISTANCE, DISTANCE_BAR_WIDTH); const distanceBar = this.buildGauge(distanceProgress, MAX_DISTANCE, DISTANCE_BAR_WIDTH);
@ -347,7 +374,13 @@ export class FishingService {
const selectedAction = this.formatSelectedAction(session.selectedAction); const selectedAction = this.formatSelectedAction(session.selectedAction);
const embed = new EmbedBuilder() 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( .setTitle(
session.status === 'success' || session.status === 'failed' session.status === 'success' || session.status === 'failed'
? t(session.locale, 'commands.fishing.titleEnded') ? t(session.locale, 'commands.fishing.titleEnded')
@ -430,6 +463,7 @@ export class FishingService {
thread: ThreadChannel; thread: ThreadChannel;
}) { }) {
const currentFish = this.pickFishByRate(); const currentFish = this.pickFishByRate();
const currentRarity = this.pickRarityByRate();
const sessionBase = { const sessionBase = {
guildId: params.guildId, guildId: params.guildId,
userId: params.userId, userId: params.userId,
@ -438,6 +472,7 @@ export class FishingService {
threadId: params.thread.id, threadId: params.thread.id,
thread: params.thread, thread: params.thread,
currentFish, currentFish,
currentRarity,
fishPosition: this.randomDirection(), fishPosition: this.randomDirection(),
selectedAction: null, selectedAction: null,
phaseStartedAt: Date.now(), phaseStartedAt: Date.now(),
@ -463,7 +498,9 @@ export class FishingService {
this.sessionsByUser.set(this.getUserKey(session.guildId, session.userId), session); this.sessionsByUser.set(this.getUserKey(session.guildId, session.userId), session);
this.sessionsByThread.set(session.threadId, 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(() => { session.tickInterval = setInterval(() => {
void this.tickSession(session); void this.tickSession(session);
@ -507,27 +544,52 @@ export class FishingService {
private static async sendCatchResult(session: FishingSession) { private static async sendCatchResult(session: FishingSession) {
const artPath = this.pickRandomArtPath(session.currentFish); 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() const embed = new EmbedBuilder()
.setColor(0x57f287) .setColor(this.hexToColorInt(session.currentRarity.backgroundColor))
.setTitle(t(session.locale, 'commands.fishing.catchResultTitle')) .setTitle(`${rarityBadge} ${t(session.locale, 'commands.fishing.catchResultTitle')} · ${rarityName}`)
.setDescription( .setDescription(
t(session.locale, 'commands.fishing.catchResultBody', { t(session.locale, 'commands.fishing.catchResultBody', {
rarity: rarityName,
fish: session.currentFish.displayName, fish: session.currentFish.displayName,
reward: String(session.reward ?? 0), reward: String(session.reward ?? 0),
}), }),
); )
.addFields({
name: t(session.locale, 'commands.fishing.rarity'),
value: rarityName,
inline: true,
});
if (artPath && fs.existsSync(artPath)) { if (artPath && fs.existsSync(artPath)) {
const fileName = path.basename(artPath); const fileName = `${session.currentFish.id}_${session.currentRarity.id}.png`;
embed.setImage(`attachment://${fileName}`); try {
await session.thread.send({ const composite = await this.composeRarityArt(artPath, session.currentRarity.backgroundColor);
embeds: [embed], embed.setImage(`attachment://${fileName}`);
files: [new AttachmentBuilder(artPath, { name: fileName })], await session.thread.send({
}); embeds: [embed],
return; 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) { static previewFishLane(position: FishingDirection) {
@ -580,9 +642,24 @@ export class FishingService {
return this.fishingCatalog[this.fishingCatalog.length - 1]; 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); 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) { private static rollRange(range: FishingRange) {
@ -606,6 +683,58 @@ export class FishingService {
return path.resolve(__dirname, '..', '..', relativePath); 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) { private static formatSelectedAction(action: FishingAction | null) {
if (action === 'left') return '⬅️'; if (action === 'left') return '⬅️';
if (action === 'center') return '⏺️'; if (action === 'center') return '⏺️';

316
yarn.lock
View File

@ -582,7 +582,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@emnapi/runtime@npm:^1.4.3": "@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.7.0":
version: 1.9.1 version: 1.9.1
resolution: "@emnapi/runtime@npm:1.9.1" resolution: "@emnapi/runtime@npm:1.9.1"
dependencies: dependencies:
@ -893,6 +893,233 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@ioredis/commands@npm:1.5.1":
version: 1.5.1 version: 1.5.1
resolution: "@ioredis/commands@npm:1.5.1" resolution: "@ioredis/commands@npm:1.5.1"
@ -2967,7 +3194,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"detect-libc@npm:^2.0.0": "detect-libc@npm:^2.0.0, detect-libc@npm:^2.1.2":
version: 2.1.2 version: 2.1.2
resolution: "detect-libc@npm:2.1.2" resolution: "detect-libc@npm:2.1.2"
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
@ -4587,6 +4814,7 @@ __metadata:
prettier: "npm:^3.8.1" prettier: "npm:^3.8.1"
prism-media: "npm:^1.3.5" prism-media: "npm:^1.3.5"
prisma: "npm:7.6.0" prisma: "npm:7.6.0"
sharp: "npm:^0.34.5"
ts-jest: "npm:^29.4.6" ts-jest: "npm:^29.4.6"
tsx: "npm:^4.21.0" tsx: "npm:^4.21.0"
typescript: "npm:^6.0.2" typescript: "npm:^6.0.2"
@ -5743,6 +5971,90 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "shebang-command@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "shebang-command@npm:2.0.0" resolution: "shebang-command@npm:2.0.0"