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
Showing only changes of commit 9876a331c1 - Show all commits

View File

@ -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": {

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',
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',

View File

@ -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: '휴식 중',

View File

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

View File

@ -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<string, FishingSession>();
private static threadEnterPromises = new Map<string, Promise<{ thread: ThreadChannel; existed: boolean }>>();
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<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 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 '⏺️';

316
yarn.lock
View File

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