Kanaele werden auch angezeigt, Monate statt Schluessel, Sorten auf Deutsch
Die Kanalauswahl blieb leer, obwohl "einsatzbereit" gruen daneben stand — ein Widerspruch, der die Ursache schon verraet: der Wert lag in der Datenbank, das Formular bekam ihn nur nie zu sehen. Ich hatte beim letzten Mal das Speichern repariert und uebersehen, dass currentSettings() dieselbe von Hand gepflegte Liste fuehrt. Jetzt holt auch sie die Schluessel aus dem Modul-Register. Geheimes bleibt aussen vor: gitea_api_token steht unter requires, faellt aber durch die Namenspruefung und wird weiterhin nur als "ist gesetzt" gemeldet. Dazu die Uebersetzung. Im Embed standen wheat, clover_windrow und seeds, weil der Feed nun mal Englisch spricht. Jetzt Weizen, Rotklee (Schwad) und Saatgut, mit lesbarem Rueckfall fuer alles, was nicht in der Liste steht — 371 Sorten uebersetzt niemand von Hand. Und der Monat. Die zwoelf Perioden sind schlicht Monate, EARLY_SPRING ist Maerz. Also nimmt das Feld jetzt "September", "sept" oder "9" statt EARLY_AUTUMN; Mehrdeutiges wie "Ju" wird abgelehnt statt geraten. Weiterzaehlen kann der Bot selbst, seit klar ist, dass ein Monat ueber einen Tag laeuft. Nicht ueber playTime — die steht im Savegame und wird nur beim Autosave geschrieben: ueber zwei Stunden gemessen stieg sie um 50, waehrend die Spieluhr 18,7 Stunden weiterlief. dayTime in der Statusabfrage ist dagegen live, und jeder Ruecksprung um Mitternacht ist ein Monatswechsel. Steht der Server leer, steht die Spielzeit — und der Monat bleibt richtigerweise stehen. Geprueft: alle zwoelf Monate hin und zurueck, Abkuerzungen, Monatszahlen, Mehrdeutiges, acht Sorten Unsinn, der Jahreswechsel von Februar auf Maerz, der Ruecksprung um Mitternacht samt Erstlauf, und dass jede Sorte der Standardauswahl einen deutschen Namen hat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+136
-9
@@ -15,7 +15,7 @@ import { brandEmbed } from '../embeds.js';
|
||||
import { brandFooter } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||
import { getSetting, listGameservers, lsState, setLsMessage, setLsBesitz } from '../db.js';
|
||||
import { getSetting, setSetting, listGameservers, lsState, setLsMessage, setLsBesitz, setLsTageszeit } from '../db.js';
|
||||
|
||||
/** Alle Attribute eines XML-Tags als Objekt */
|
||||
const attrsVon = (roh) =>
|
||||
@@ -44,6 +44,10 @@ export function statsLesen(xml) {
|
||||
// Ohne mapSize lässt sich nichts zeichnen — dann lieber gar keine Karte
|
||||
groesse: Number(server.mapSize) || null,
|
||||
version: server.version ?? null,
|
||||
// Millisekunden seit Tagesbeginn. Die einzige Uhr im Feed, die wirklich
|
||||
// live ist — playTime aus dem Savegame wird nur beim Autosave neu
|
||||
// geschrieben und hinkt bis zu einer Viertelstunde hinterher.
|
||||
tageszeit: Number(server.dayTime) || 0,
|
||||
parzellen: tags(xml, 'Farmland').map((f) => ({
|
||||
id: Number(f.id), name: f.name ?? String(f.id),
|
||||
hof: Number(f.owner), // 0 = noch zu kaufen
|
||||
@@ -103,11 +107,56 @@ export const PERIODEN = [
|
||||
'EARLY_AUTUMN', 'MID_AUTUMN', 'LATE_AUTUMN',
|
||||
'EARLY_WINTER', 'MID_WINTER', 'LATE_WINTER',
|
||||
];
|
||||
/**
|
||||
* Die zwölf Perioden sind schlicht die Monate — das Spiel zeigt sie auch so
|
||||
* an. `EARLY_SPRING` ist März, und ab da geht es der Reihe nach weiter.
|
||||
* Im Panel und im Embed steht deshalb der Monat und nicht der Schlüssel.
|
||||
*/
|
||||
export const PERIODE_LABEL = {
|
||||
EARLY_SPRING: 'Früh-Frühling', MID_SPRING: 'Mitt-Frühling', LATE_SPRING: 'Spät-Frühling',
|
||||
EARLY_SUMMER: 'Früh-Sommer', MID_SUMMER: 'Hoch-Sommer', LATE_SUMMER: 'Spät-Sommer',
|
||||
EARLY_AUTUMN: 'Früh-Herbst', MID_AUTUMN: 'Mitt-Herbst', LATE_AUTUMN: 'Spät-Herbst',
|
||||
EARLY_WINTER: 'Früh-Winter', MID_WINTER: 'Hoch-Winter', LATE_WINTER: 'Spät-Winter',
|
||||
EARLY_SPRING: 'März', MID_SPRING: 'April', LATE_SPRING: 'Mai',
|
||||
EARLY_SUMMER: 'Juni', MID_SUMMER: 'Juli', LATE_SUMMER: 'August',
|
||||
EARLY_AUTUMN: 'September', MID_AUTUMN: 'Oktober', LATE_AUTUMN: 'November',
|
||||
EARLY_WINTER: 'Dezember', MID_WINTER: 'Januar', LATE_WINTER: 'Februar',
|
||||
};
|
||||
|
||||
/**
|
||||
* Was im Panel steht, in eine Periode übersetzen.
|
||||
*
|
||||
* Nimmt den Monatsnamen („September", „sept"), die Monatszahl und weiterhin
|
||||
* den rohen Schlüssel. Niemand soll `EARLY_AUTUMN` nachschlagen müssen, um zu
|
||||
* sagen, dass gerade September ist.
|
||||
*
|
||||
* @returns {string|null} Periodenschlüssel oder null bei Unsinn
|
||||
*/
|
||||
export function periodeLesen(text) {
|
||||
const roh = String(text ?? '').trim();
|
||||
if (!roh) return null;
|
||||
const gross = roh.toUpperCase();
|
||||
if (PERIODEN.includes(gross)) return gross;
|
||||
|
||||
// Monatszahl: 1 = Januar, nicht 1 = erste Periode. Wer „9" schreibt, meint
|
||||
// September und nicht November.
|
||||
if (/^\d{1,2}$/.test(roh)) {
|
||||
const monat = Number(roh);
|
||||
if (monat < 1 || monat > 12) return null;
|
||||
return PERIODEN.find((p) => MONAT_NUMMER[p] === monat) ?? null;
|
||||
}
|
||||
// Name, auch abgekürzt: "sept" trifft September, "mä" trifft März
|
||||
const klein = roh.toLowerCase().replace(/[^a-zäöü]/g, '');
|
||||
const treffer = PERIODEN.filter((p) => {
|
||||
const monat = PERIODE_LABEL[p].toLowerCase();
|
||||
return monat.startsWith(klein) || klein.startsWith(monat);
|
||||
});
|
||||
// "ju" passt auf Juni und Juli — dann lieber nichts als das falsche
|
||||
return treffer.length === 1 ? treffer[0] : null;
|
||||
}
|
||||
|
||||
/** Periodenschlüssel → Monatszahl im Kalender */
|
||||
const MONAT_NUMMER = {
|
||||
EARLY_SPRING: 3, MID_SPRING: 4, LATE_SPRING: 5,
|
||||
EARLY_SUMMER: 6, MID_SUMMER: 7, LATE_SUMMER: 8,
|
||||
EARLY_AUTUMN: 9, MID_AUTUMN: 10, LATE_AUTUMN: 11,
|
||||
EARLY_WINTER: 12, MID_WINTER: 1, LATE_WINTER: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -286,7 +335,7 @@ export function hofEmbed(client, stand, namen = {}) {
|
||||
if (ladungen.size > 0) {
|
||||
const zeilen = [...ladungen.entries()]
|
||||
.sort((a, b) => b[1] - a[1]).slice(0, 8)
|
||||
.map(([art, menge]) => `\`${art.toLowerCase()}\` ${Math.round(menge).toLocaleString('de-DE')} l`);
|
||||
.map(([art, menge]) => `**${sortenName(art)}** ${Math.round(menge).toLocaleString('de-DE')} l`);
|
||||
embed.addFields({ name: 'In den Fahrzeugen', value: zeilen.join(' · ').slice(0, 1000) });
|
||||
}
|
||||
return embed;
|
||||
@@ -315,8 +364,48 @@ export const STANDARD_SORTEN = [
|
||||
'GRAPE', 'OLIVE', 'MILK', 'WOOL', 'EGG', 'SILAGE', 'WOODCHIPS',
|
||||
];
|
||||
|
||||
/** "SUGARBEET_CUT" → "sugarbeet cut" */
|
||||
const sortenName = (art) => art.toLowerCase().replace(/_/g, ' ');
|
||||
/**
|
||||
* Deutsche Namen für das, was der Feed als `WHEAT` oder `CLOVER_WINDROW`
|
||||
* liefert. Bewusst nicht vollständig — 371 Sorten übersetzt niemand von Hand,
|
||||
* und was fehlt, fällt lesbar zurück statt zu verschwinden.
|
||||
*/
|
||||
export const SORTEN_NAMEN = {
|
||||
// Feldfrüchte
|
||||
WHEAT: 'Weizen', BARLEY: 'Gerste', OAT: 'Hafer', RYE: 'Roggen',
|
||||
TRITICALE: 'Triticale', SPELT: 'Dinkel', CANOLA: 'Raps', MAIZE: 'Mais',
|
||||
SUNFLOWER: 'Sonnenblumen', SOYBEAN: 'Sojabohnen', POTATO: 'Kartoffeln',
|
||||
SUGARBEET: 'Zuckerrüben', SUGARBEET_CUT: 'Zuckerrübenschnitzel',
|
||||
SUGARCANE: 'Zuckerrohr', COTTON: 'Baumwolle', SORGHUM: 'Sorghum',
|
||||
RICE: 'Reis', RICELONGGRAIN: 'Langkornreis', MILLET: 'Hirse',
|
||||
GRAPE: 'Trauben', OLIVE: 'Oliven', POPLAR: 'Pappel',
|
||||
// Gemüse
|
||||
CARROT: 'Karotten', PARSNIP: 'Pastinaken', BEETROOT: 'Rote Beete',
|
||||
SPINACH: 'Spinat', PEA: 'Erbsen', GREENBEAN: 'Buschbohnen',
|
||||
LETTUCE: 'Kopfsalat', TOMATO: 'Tomaten', STRAWBERRY: 'Erdbeeren',
|
||||
ASPARAGUSWHITE: 'Spargel, weiß', ASPARAGUSGREEN: 'Spargel, grün',
|
||||
// Futter und Schnittgut
|
||||
GRASS: 'Gras', GRASS_WINDROW: 'Gras (Schwad)', DRYGRASS_WINDROW: 'Heu',
|
||||
CLOVER: 'Rotklee', CLOVER_WINDROW: 'Rotklee (Schwad)',
|
||||
ALFALFA: 'Luzerne', ALFALFA_WINDROW: 'Luzerne (Schwad)',
|
||||
OILRADISH: 'Ölrettich', STRAW: 'Stroh', CHAFF: 'Häcksel',
|
||||
SILAGE: 'Silage', FORAGE: 'Mischration', HAY: 'Heu',
|
||||
// Tierisches
|
||||
MILK: 'Milch', WOOL: 'Wolle', EGG: 'Eier', HONEY: 'Honig',
|
||||
MANURE: 'Mist', LIQUIDMANURE: 'Gülle', DIGESTATE: 'Gärrest',
|
||||
// Betriebsstoffe
|
||||
SEEDS: 'Saatgut', FERTILIZER: 'Dünger', LIQUIDFERTILIZER: 'Flüssigdünger',
|
||||
HERBICIDE: 'Herbizid', LIME: 'Kalk', WATER: 'Wasser',
|
||||
DIESEL: 'Diesel', DEF: 'AdBlue', ELECTRICCHARGE: 'Strom',
|
||||
WOODCHIPS: 'Hackschnitzel', WOOD: 'Holz', STONE: 'Steine',
|
||||
};
|
||||
|
||||
/** `SUGARBEET_CUT` → „Zuckerrübenschnitzel"; Unbekanntes bleibt lesbar */
|
||||
export function sortenName(art) {
|
||||
const schluessel = String(art ?? '').toUpperCase();
|
||||
if (SORTEN_NAMEN[schluessel]) return SORTEN_NAMEN[schluessel];
|
||||
// Kein Treffer: wenigstens keine Versalien und keine Unterstriche
|
||||
return schluessel.toLowerCase().replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
/** Preis-Embed: was wann am meisten bringt */
|
||||
export function preisEmbed(client, preise, { jetzt = null, anzahl = 12, sorten = null } = {}) {
|
||||
@@ -487,13 +576,30 @@ export async function lsTick(client) {
|
||||
}
|
||||
setLsBesitz(s.id, stand.parzellen.map((p) => ({ id: p.id, hof: p.hof })));
|
||||
|
||||
// Neuer Spieltag = neuer Monat. Nur weiterstellen, wenn überhaupt
|
||||
// eine Jahreszeit eingetragen ist — ohne Ausgangspunkt liesse sich
|
||||
// nur raten, und ein geratener Monat ist schlimmer als keiner.
|
||||
const gesetzt = periodeLesen(getSetting('ls_period'));
|
||||
if (tagUmgeschlagen(alt?.tageszeit, stand.tageszeit)) {
|
||||
const naechste = naechstePeriode(gesetzt);
|
||||
if (naechste) {
|
||||
setSetting('ls_period', naechste);
|
||||
await alarm?.send({
|
||||
embeds: [brandEmbed(client, 'ZEIT')
|
||||
.setTitle('📅 Neuer Monat auf dem Hof')
|
||||
.setDescription(`Jetzt **${PERIODE_LABEL[naechste]}** — die Preise haben sich verschoben.`)],
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
setLsTageszeit(s.id, stand.tageszeit);
|
||||
|
||||
if (!kanal?.isTextBased()) continue;
|
||||
const payload = await hofNachricht(client, stand, bild, namen, breite);
|
||||
// Preise ändern sich nur mit der Jahreszeit — ein zweites Embed in
|
||||
// derselben Nachricht spart einen Dauerposten im Kanal.
|
||||
if (preise.length > 0) {
|
||||
payload.embeds.push(preisEmbed(client, preise, {
|
||||
jetzt: getSetting('ls_period') || null,
|
||||
jetzt: periodeLesen(getSetting('ls_period')), // nach dem Weiterstellen neu gelesen
|
||||
sorten: String(getSetting('ls_crops') ?? '').split(',').map((s) => s.trim()).filter(Boolean),
|
||||
}));
|
||||
}
|
||||
@@ -515,6 +621,27 @@ export async function lsTick(client) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist seit der letzten Abfrage ein Spieltag vorbei?
|
||||
*
|
||||
* `dayTime` zählt Millisekunden seit Tagesbeginn und springt um Mitternacht
|
||||
* zurück. Läuft ein Monat über einen Tag — bei Farming Simulator die übliche
|
||||
* Einstellung `plannedDaysPerPeriod = 1` —, ist jeder dieser Sprünge ein
|
||||
* Monatswechsel.
|
||||
*
|
||||
* Bewusst über die Uhr und nicht über die Wanduhr: steht der Server leer,
|
||||
* steht auch die Spielzeit, und dann darf der Monat nicht weiterlaufen.
|
||||
*/
|
||||
export const tagUmgeschlagen = (vorher, jetzt) =>
|
||||
Number.isFinite(vorher) && vorher > 0 && jetzt < vorher;
|
||||
|
||||
/** Nächste Jahreszeit; unbekannte Eingabe ergibt null statt eines Ratewerts */
|
||||
export function naechstePeriode(periode) {
|
||||
const jetzt = periodeLesen(periode);
|
||||
if (!jetzt) return null;
|
||||
return PERIODEN[(PERIODEN.indexOf(jetzt) + 1) % PERIODEN.length];
|
||||
}
|
||||
|
||||
/** "1=Alexander, 2=Timo" → { 1: 'Alexander', 2: 'Timo' } */
|
||||
export function hofNamen(roh) {
|
||||
const raus = {};
|
||||
|
||||
@@ -1007,6 +1007,16 @@ const lsBesitzStmt = db.prepare(`
|
||||
INSERT INTO ls_state (server_id, besitz, geprueft) VALUES (?, ?, datetime('now'))
|
||||
ON CONFLICT(server_id) DO UPDATE SET besitz = excluded.besitz, geprueft = datetime('now')
|
||||
`);
|
||||
// Letzte gesehene Tageszeit — daran erkennt das Modul den Tageswechsel
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(ls_state)').all().map((c) => c.name);
|
||||
if (!cols.includes('tageszeit')) db.exec('ALTER TABLE ls_state ADD COLUMN tageszeit INTEGER');
|
||||
}
|
||||
const lsTagStmt = db.prepare(`
|
||||
INSERT INTO ls_state (server_id, tageszeit) VALUES (?, ?)
|
||||
ON CONFLICT(server_id) DO UPDATE SET tageszeit = excluded.tageszeit
|
||||
`);
|
||||
export const setLsTageszeit = (serverId, ms) => lsTagStmt.run(serverId, ms);
|
||||
const lsVergessenStmt = db.prepare('DELETE FROM ls_state WHERE server_id = ?');
|
||||
export const lsState = (serverId) => {
|
||||
const r = lsStateStmt.get(serverId);
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ export const MODULES = [
|
||||
fields: [
|
||||
{ setting: 'ls_event_channel_id', label: 'Ereignis-Kanal', art: 'kanal', hint: 'Land gekauft oder verkauft; leer = in den Karten-Kanal' },
|
||||
{ setting: 'ls_farm_names', label: 'Hof-Namen', art: 'text', hint: 'der Feed kennt nur Nummern — z. B. „1=Alexander, 2=Timo, 3=Kitty"' },
|
||||
{ setting: 'ls_period', label: 'Aktuelle Jahreszeit', art: 'text', hint: 'z. B. MID_SUMMER — steht nicht im Feed. Leer lassen heißt: Kurve ohne „jetzt verkaufen"-Rat.' },
|
||||
{ setting: 'ls_period', label: 'Aktueller Monat', art: 'text', hint: 'z. B. „September" oder „9" — steht nicht im Feed, deshalb einmal eintragen. Danach zählt der Bot bei jedem Tageswechsel selbst weiter. Leer heißt: Kurve ohne „jetzt verkaufen"-Rat.' },
|
||||
{ setting: 'ls_crops', label: 'Sorten in der Preisliste', art: 'text', hint: 'kommagetrennt, leer = die üblichen. Ohne Auswahl verdrängt Spargel jede echte Feldfrucht.' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -882,6 +882,17 @@ ${rssItems}
|
||||
|
||||
function currentSettings() {
|
||||
return {
|
||||
// Alles, was die Modulliste kennt, zuerst — die Einträge darunter
|
||||
// überschreiben es, wo es Standardwerte oder Env-Rückfälle gibt.
|
||||
//
|
||||
// Ohne das gilt für jedes neue Modul: gespeichert wird, angezeigt
|
||||
// nicht. Das Feld steht leer da, obwohl der Wert in der Datenbank
|
||||
// liegt, und der Haken „einsatzbereit" widerspricht dem Formular.
|
||||
...Object.fromEntries(
|
||||
Object.entries(moduleSettingKeys())
|
||||
.filter(([art]) => art !== 'schalter') // Schalter sind Ja/Nein, siehe unten
|
||||
.flatMap(([, keys]) => keys.map((k) => [k, getSetting(k) ?? '']))
|
||||
),
|
||||
commit_channel_id: commitChannelId(),
|
||||
devlog_channel_id: devlogChannelId(),
|
||||
release_channel_id: releaseChannelId() ?? '',
|
||||
|
||||
Reference in New Issue
Block a user