From 86f10bfa15c283ccd5f2096304e031bdaca9c6f7 Mon Sep 17 00:00:00 2001 From: PatchRequest Date: Tue, 27 Jan 2026 19:48:51 +0100 Subject: [PATCH 1/5] Add time-based loot drop weight multipliers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Morgens (6-12 Uhr): Kaffeemühle, Krankschreibung, Oettinger Abends (18-24 Uhr): Döner, Ayran, Sahne, Trichter, Gauloises Co-Authored-By: Claude Opus 4.5 --- src/service/lootDrop.ts | 49 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index ce31f4b8..91a3791a 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -362,6 +362,36 @@ type AdjustmentResult = { weights: number[]; }; +function getTimeOfDayMultipliers(weights: readonly number[]): number[] { + const now = new Date(); + const hour = Number( + now.toLocaleString("de-DE", { timeZone: "Europe/Berlin", hour: "numeric", hour12: false }), + ); + + const multipliers = new Array(weights.length).fill(1); + + // Morgens (6:00 - 12:00): Höhere Wahrscheinlichkeit für Frühstücks-Items + const isMorning = hour >= 6 && hour < 12; + // Abends (18:00 - 24:00): Höhere Wahrscheinlichkeit für Feierabend-Items + const isEvening = hour >= 18 && hour < 24; + + if (isMorning) { + multipliers[LootKind.KAFFEEMUEHLE] = 2; + multipliers[LootKind.KRANKSCHREIBUNG] = 2; + multipliers[LootKind.OETTINGER] = 2; + } + + if (isEvening) { + multipliers[LootKind.DOENER] = 2; + multipliers[LootKind.AYRAN] = 2; + multipliers[LootKind.SAHNE] = 2; + multipliers[LootKind.TRICHTER] = 2; + multipliers[LootKind.GAULOISES_BLAU] = 2; + } + + return multipliers; +} + async function getDropWeightAdjustments( user: User, weights: readonly number[], @@ -385,9 +415,26 @@ async function getDropWeightAdjustments( messages.push("Da du privat versichert bist, hast du die doppelte Chance auf eine AU."); } + const timeMultipliers = getTimeOfDayMultipliers(weights); + const newWeights = [...weights]; newWeights[LootKind.NICHTS] = Math.ceil(weights[LootKind.NICHTS] * wasteFactor) | 0; - newWeights[LootKind.KRANKSCHREIBUNG] = (weights[LootKind.KRANKSCHREIBUNG] * pkvFactor) | 0; + newWeights[LootKind.KRANKSCHREIBUNG] = + (weights[LootKind.KRANKSCHREIBUNG] * pkvFactor * timeMultipliers[LootKind.KRANKSCHREIBUNG]) | + 0; + + // Tageszeitabhängige Anpassungen + newWeights[LootKind.KAFFEEMUEHLE] = + (weights[LootKind.KAFFEEMUEHLE] * timeMultipliers[LootKind.KAFFEEMUEHLE]) | 0; + newWeights[LootKind.OETTINGER] = + (weights[LootKind.OETTINGER] * timeMultipliers[LootKind.OETTINGER]) | 0; + newWeights[LootKind.DOENER] = (weights[LootKind.DOENER] * timeMultipliers[LootKind.DOENER]) | 0; + newWeights[LootKind.AYRAN] = (weights[LootKind.AYRAN] * timeMultipliers[LootKind.AYRAN]) | 0; + newWeights[LootKind.SAHNE] = (weights[LootKind.SAHNE] * timeMultipliers[LootKind.SAHNE]) | 0; + newWeights[LootKind.TRICHTER] = + (weights[LootKind.TRICHTER] * timeMultipliers[LootKind.TRICHTER]) | 0; + newWeights[LootKind.GAULOISES_BLAU] = + (weights[LootKind.GAULOISES_BLAU] * timeMultipliers[LootKind.GAULOISES_BLAU]) | 0; return { messages, From 98911df94d594c692e346b20a2626362106da313 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Wed, 4 Feb 2026 22:31:11 +0100 Subject: [PATCH 2/5] Use zonedNow --- src/service/lootDrop.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index 4ce56ca3..eecd9526 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -25,6 +25,7 @@ import type { Loot, LootId } from "#storage/db/model.ts"; import type { LootTemplate } from "#storage/loot.ts"; import { randomBoolean, randomEntry, randomEntryWeighted } from "#service/random.ts"; import * as timeUtils from "#utils/time.ts"; +import { zonedNow } from "#utils/dateUtils.ts"; import * as lootService from "#service/loot.ts"; import { @@ -364,10 +365,7 @@ type AdjustmentResult = { }; function getTimeOfDayMultipliers(weights: readonly number[]): number[] { - const now = new Date(); - const hour = Number( - now.toLocaleString("de-DE", { timeZone: "Europe/Berlin", hour: "numeric", hour12: false }), - ); + const hour = zonedNow().hour; const multipliers = new Array(weights.length).fill(1); @@ -421,7 +419,9 @@ async function getDropWeightAdjustments( const newWeights = [...weights]; newWeights[LootKind.NICHTS] = Math.ceil(weights[LootKind.NICHTS] * wasteFactor) | 0; newWeights[LootKind.KRANKSCHREIBUNG] = - (weights[LootKind.KRANKSCHREIBUNG] * pkvFactor * timeMultipliers[LootKind.KRANKSCHREIBUNG]) | + (weights[LootKind.KRANKSCHREIBUNG] * + pkvFactor * + timeMultipliers[LootKind.KRANKSCHREIBUNG]) | 0; // Tageszeitabhängige Anpassungen From 6b9978bdb2d2418889d4d02f3ef5d0b9f65fbf20 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Wed, 4 Feb 2026 22:43:00 +0100 Subject: [PATCH 3/5] Refactor time-based weights to singel data source --- src/service/lootData.ts | 24 ++++++++++++++ src/service/lootDrop.ts | 71 +++++++++++++---------------------------- src/storage/loot.ts | 6 ++++ 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/service/lootData.ts b/src/service/lootData.ts index acc2a152..29ef1bef 100644 --- a/src/service/lootData.ts +++ b/src/service/lootData.ts @@ -137,6 +137,9 @@ export const lootTemplateMap: Record = { [LootKind.DOENER]: { id: LootKind.DOENER, weight: 5, + timeBasedWeight: { + morning: 10, + }, displayName: "Döner", titleText: "Einen Döner", dropDescription: "Bewahre ihn gut als Geldanlage auf!", @@ -156,6 +159,9 @@ export const lootTemplateMap: Record = { [LootKind.KRANKSCHREIBUNG]: { id: LootKind.KRANKSCHREIBUNG, weight: 0.5, + timeBasedWeight: { + morning: 1, + }, displayName: "Arbeitsunfähigkeitsbescheinigung", titleText: "Einen gelben Urlaubsschein", dropDescription: "Benutze ihn weise!", @@ -222,6 +228,9 @@ export const lootTemplateMap: Record = { [LootKind.AYRAN]: { id: LootKind.AYRAN, weight: 1, + timeBasedWeight: { + morning: 2, + }, displayName: "Ayran", titleText: "Einen Ayran", dropDescription: "Der gute von Müller", @@ -242,6 +251,9 @@ export const lootTemplateMap: Record = { [LootKind.TRICHTER]: { id: LootKind.TRICHTER, weight: 1, + timeBasedWeight: { + morning: 2, + }, displayName: "Trichter", titleText: "Einen Trichter", dropDescription: "Für die ganz großen Schlücke", @@ -313,6 +325,9 @@ export const lootTemplateMap: Record = { [LootKind.OETTINGER]: { id: LootKind.OETTINGER, weight: 1, + timeBasedWeight: { + morning: 2, + }, displayName: "Oettinger", titleText: "Ein warmes Oettinger", dropDescription: "Ja dann Prost ne!", @@ -370,6 +385,9 @@ export const lootTemplateMap: Record = { [LootKind.SAHNE]: { id: LootKind.SAHNE, weight: 1, + timeBasedWeight: { + morning: 2, + }, displayName: "Sprühsahne", titleText: "Sprühsahne", dropDescription: "Fürs Frühstück oder so", @@ -414,6 +432,9 @@ export const lootTemplateMap: Record = { [LootKind.GAULOISES_BLAU]: { id: LootKind.GAULOISES_BLAU, weight: 1, + timeBasedWeight: { + morning: 2, + }, displayName: "Gauloises Blau", titleText: "Eine Schachtel Gauloises Blau", dropDescription: @@ -496,6 +517,9 @@ export const lootTemplateMap: Record = { [LootKind.KAFFEEMUEHLE]: { id: LootKind.KAFFEEMUEHLE, weight: 1, + timeBasedWeight: { + morning: 2, + }, displayName: "Kaffeemühle", titleText: "Eine Kaffeemühle für 400€", dropDescription: "Kann Kaffee mühlen. Und das gut. Mit Gold.", diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index eecd9526..52fd9172 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -22,7 +22,7 @@ import * as sentry from "@sentry/node"; import type { BotContext } from "#context.ts"; import type { Loot, LootId } from "#storage/db/model.ts"; -import type { LootTemplate } from "#storage/loot.ts"; +import type { LootTemplate, TimeBasedWeightConfig } from "#storage/loot.ts"; import { randomBoolean, randomEntry, randomEntryWeighted } from "#service/random.ts"; import * as timeUtils from "#utils/time.ts"; import { zonedNow } from "#utils/dateUtils.ts"; @@ -155,7 +155,12 @@ export async function postLootDrop( return; } - const defaultWeights = lootTemplates.map(t => t.weight); + const timeBasedWeightKey = getCurrentTimeBasedKey(); + + const defaultWeights = timeBasedWeightKey + ? lootTemplates.map(t => t.timeBasedWeight?.[timeBasedWeightKey] ?? t.weight) + : lootTemplates.map(t => t.weight); + const { messages, weights } = await getDropWeightAdjustments(interaction.user, defaultWeights); const template = randomEntryWeighted(lootTemplates, weights); @@ -359,38 +364,25 @@ export async function createDropTakenContent( }; } -type AdjustmentResult = { - messages: string[]; - weights: number[]; -}; - -function getTimeOfDayMultipliers(weights: readonly number[]): number[] { +function getCurrentTimeBasedKey(): keyof TimeBasedWeightConfig | undefined { const hour = zonedNow().hour; - const multipliers = new Array(weights.length).fill(1); - - // Morgens (6:00 - 12:00): Höhere Wahrscheinlichkeit für Frühstücks-Items - const isMorning = hour >= 6 && hour < 12; - // Abends (18:00 - 24:00): Höhere Wahrscheinlichkeit für Feierabend-Items - const isEvening = hour >= 18 && hour < 24; - - if (isMorning) { - multipliers[LootKind.KAFFEEMUEHLE] = 2; - multipliers[LootKind.KRANKSCHREIBUNG] = 2; - multipliers[LootKind.OETTINGER] = 2; + // :shibakek: + switch (true) { + case 6 <= hour && hour <= 12: + return "morning"; + case 18 <= hour && hour <= 24: + return "evening"; + default: + return undefined; } - - if (isEvening) { - multipliers[LootKind.DOENER] = 2; - multipliers[LootKind.AYRAN] = 2; - multipliers[LootKind.SAHNE] = 2; - multipliers[LootKind.TRICHTER] = 2; - multipliers[LootKind.GAULOISES_BLAU] = 2; - } - - return multipliers; } +type AdjustmentResult = { + messages: string[]; + weights: number[]; +}; + async function getDropWeightAdjustments( user: User, weights: readonly number[], @@ -414,28 +406,9 @@ async function getDropWeightAdjustments( messages.push("Da du privat versichert bist, hast du die doppelte Chance auf eine AU."); } - const timeMultipliers = getTimeOfDayMultipliers(weights); - const newWeights = [...weights]; newWeights[LootKind.NICHTS] = Math.ceil(weights[LootKind.NICHTS] * wasteFactor) | 0; - newWeights[LootKind.KRANKSCHREIBUNG] = - (weights[LootKind.KRANKSCHREIBUNG] * - pkvFactor * - timeMultipliers[LootKind.KRANKSCHREIBUNG]) | - 0; - - // Tageszeitabhängige Anpassungen - newWeights[LootKind.KAFFEEMUEHLE] = - (weights[LootKind.KAFFEEMUEHLE] * timeMultipliers[LootKind.KAFFEEMUEHLE]) | 0; - newWeights[LootKind.OETTINGER] = - (weights[LootKind.OETTINGER] * timeMultipliers[LootKind.OETTINGER]) | 0; - newWeights[LootKind.DOENER] = (weights[LootKind.DOENER] * timeMultipliers[LootKind.DOENER]) | 0; - newWeights[LootKind.AYRAN] = (weights[LootKind.AYRAN] * timeMultipliers[LootKind.AYRAN]) | 0; - newWeights[LootKind.SAHNE] = (weights[LootKind.SAHNE] * timeMultipliers[LootKind.SAHNE]) | 0; - newWeights[LootKind.TRICHTER] = - (weights[LootKind.TRICHTER] * timeMultipliers[LootKind.TRICHTER]) | 0; - newWeights[LootKind.GAULOISES_BLAU] = - (weights[LootKind.GAULOISES_BLAU] * timeMultipliers[LootKind.GAULOISES_BLAU]) | 0; + newWeights[LootKind.KRANKSCHREIBUNG] = (weights[LootKind.KRANKSCHREIBUNG] * pkvFactor) | 0; return { messages, diff --git a/src/storage/loot.ts b/src/storage/loot.ts index 944ba889..e6c325d7 100644 --- a/src/storage/loot.ts +++ b/src/storage/loot.ts @@ -32,9 +32,15 @@ export type LootUseCommandInteraction = ChatInputCommandInteraction & { channel: GuildTextBasedChannel; }; +export interface TimeBasedWeightConfig { + morning?: number; + evening?: number; +} + export interface LootTemplate { id: LootKindId; weight: number; + timeBasedWeight?: TimeBasedWeightConfig; displayName: string; titleText: string; dropDescription: string; From e00eda42672659c45f8713d32a969126c70c8414 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Wed, 4 Feb 2026 22:48:56 +0100 Subject: [PATCH 4/5] Fix evening --- src/service/lootData.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/service/lootData.ts b/src/service/lootData.ts index 29ef1bef..7dcb2267 100644 --- a/src/service/lootData.ts +++ b/src/service/lootData.ts @@ -138,7 +138,7 @@ export const lootTemplateMap: Record = { id: LootKind.DOENER, weight: 5, timeBasedWeight: { - morning: 10, + evening: 10, }, displayName: "Döner", titleText: "Einen Döner", @@ -229,7 +229,7 @@ export const lootTemplateMap: Record = { id: LootKind.AYRAN, weight: 1, timeBasedWeight: { - morning: 2, + evening: 2, }, displayName: "Ayran", titleText: "Einen Ayran", @@ -252,7 +252,7 @@ export const lootTemplateMap: Record = { id: LootKind.TRICHTER, weight: 1, timeBasedWeight: { - morning: 2, + evening: 2, }, displayName: "Trichter", titleText: "Einen Trichter", @@ -386,7 +386,7 @@ export const lootTemplateMap: Record = { id: LootKind.SAHNE, weight: 1, timeBasedWeight: { - morning: 2, + evening: 2, }, displayName: "Sprühsahne", titleText: "Sprühsahne", @@ -433,7 +433,7 @@ export const lootTemplateMap: Record = { id: LootKind.GAULOISES_BLAU, weight: 1, timeBasedWeight: { - morning: 2, + evening: 2, }, displayName: "Gauloises Blau", titleText: "Eine Schachtel Gauloises Blau", From 2674da4092e8be918730e34626f19cbc570a328c Mon Sep 17 00:00:00 2001 From: holzmaster Date: Wed, 4 Feb 2026 22:53:13 +0100 Subject: [PATCH 5/5] Document hour edge-case --- src/service/lootDrop.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/service/lootDrop.ts b/src/service/lootDrop.ts index 52fd9172..b070eba4 100644 --- a/src/service/lootDrop.ts +++ b/src/service/lootDrop.ts @@ -365,6 +365,8 @@ export async function createDropTakenContent( } function getCurrentTimeBasedKey(): keyof TimeBasedWeightConfig | undefined { + // Caution: using a ZonedDateTime and comparing the hour to the raw values breaks when daylight-saving-time happens (an hour can happen multiple times) + // We disregard these edge-cases, but keep it in mind const hour = zonedNow().hour; // :shibakek: