feat: update this highly outdated app to use latest packages and custom configs (#1)

Reviewed-on: https://codeberg.org/nhcarrigan/tingle-bot/pulls/1
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2024-09-26 19:46:33 +00:00 committed by Naomi the Technomancer
parent 1339b63378
commit 7c0bd7ad10
74 changed files with 6348 additions and 8905 deletions

View File

@ -1,73 +0,0 @@
{
"env": {
"es2020": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:jsdoc/recommended",
"plugin:import/recommended",
"plugin:import/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "jsdoc", "import"],
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { "allowTemplateLiterals": true }],
"semi": ["error", "always"],
"prefer-const": "error",
"eqeqeq": ["error", "always"],
"curly": ["error"],
"require-atomic-updates": ["error"],
"no-var": ["error"],
"camelcase": ["error"],
"init-declarations": ["error", "always"],
"require-await": ["error"],
"no-param-reassign": ["error"],
"jsdoc/require-jsdoc": [
"error",
{
"require": {
"ArrowFunctionExpression": true,
"ClassDeclaration": true,
"ClassExpression": true,
"FunctionDeclaration": true,
"FunctionExpression": true,
"MethodDefinition": true
},
"publicOnly": true
}
],
"jsdoc/require-description-complete-sentence": "error",
"import/first": "error",
"import/exports-last": "error",
"import/newline-after-import": "error",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type",
"unknown"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
}
}

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/node_modules/ /node_modules/
.env .env
/prod/ /prod/
/coverage/

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"]
}

13
eslint.config.js Normal file
View File

@ -0,0 +1,13 @@
import NaomisConifg from "@nhcarrigan/eslint-config";
export default [
...NaomisConifg,
{
rules: {
"@typescript-eslint/naming-convention": "off",
"max-lines": "off",
"max-lines-per-function": "off",
"max-statements": "off",
}
}
]

7722
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,47 +3,34 @@
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"lint": "eslint src --max-warnings 0", "lint": "eslint src test --max-warnings 0",
"start": "node -r dotenv/config prod/index.js", "start": "op run --env-file=./prod.env -- node prod/index.js",
"test": "ts-mocha -u tdd tests/**/*.spec.ts" "test": "vitest run --coverage"
}, },
"author": "", "author": "",
"license": "ISC", "license": "See license in LICENSE.md",
"engines": { "engines": {
"node": "16.11.0", "node": "20",
"npm": "8.0.0" "pnpm": "9"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.3.0", "@nhcarrigan/eslint-config": "5.0.0-rc1",
"@types/mocha": "^9.1.0", "@nhcarrigan/typescript-config": "4.0.0",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "2.1.7",
"@types/sharp": "^0.29.5", "@types/uuid": "10.0.0",
"@types/uuid": "^8.3.4", "@vitest/coverage-istanbul": "2.1.1",
"@typescript-eslint/eslint-plugin": "^5.10.2", "eslint": "9.11.1",
"@typescript-eslint/parser": "^5.10.2", "typescript": "5.6.2",
"chai": "^4.3.6", "vitest": "2.1.1"
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsdoc": "^37.7.1",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^9.2.0",
"prettier": "^2.5.1",
"ts-mocha": "^9.0.2",
"typescript": "^4.5.5"
}, },
"dependencies": { "dependencies": {
"@discordjs/builders": "^0.12.0", "discord.js": "14.16.2",
"@discordjs/rest": "^0.3.0", "node-schedule": "2.1.1",
"@sentry/integrations": "^6.17.7", "sharp": "0.33.5",
"@sentry/node": "^6.17.7", "uuid": "10.0.0",
"discord.js": "^13.6.0", "winston": "3.14.2"
"dotenv": "^16.0.0",
"node-schedule": "^2.1.0",
"sharp": "^0.30.1",
"uuid": "^8.3.2",
"winston": "^3.6.0"
} }
} }

4999
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
prod.env Normal file
View File

@ -0,0 +1,6 @@
DISCORD_TOKEN="op://Environment Variables - Naomi/Tingle Bot/token"
HOME_GUILD_ID="op://Environment Variables - Naomi/Tingle Bot/home_guild"
RUDANIA_ID="op://Environment Variables - Naomi/Tingle Bot/rudania_channel"
INARIKO_ID="op://Environment Variables - Naomi/Tingle Bot/inariko_channel"
VHINTL_ID="op://Environment Variables - Naomi/Tingle Bot/vhintl_channel"
DEBUG_HOOK="op://Environment Variables - Naomi/Tingle Bot/debug_hook"

View File

@ -1,6 +0,0 @@
import { forecast } from "./forecast";
/**
* TODO: Migrate this to an automated import.
*/
export const CommandList = [forecast];

View File

@ -0,0 +1,11 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { forecast } from "./forecast.js";
/**
* TODO: Migrate this to an automated import.
*/
export const commandList = [ forecast ];

View File

@ -1,35 +1,44 @@
import { SlashCommandBuilder } from "@discordjs/builders"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { SlashCommandBuilder } from "discord.js";
import { forecastChoices } from "../config/forecastChoices.js";
import { generateWeatherEmbed } from "../modules/generateWeatherEmbed.js";
import { getWeatherForecast } from "../modules/getWeatherForecast.js";
import { errorHandler } from "../utils/errorHandler.js";
import type { Command } from "../interfaces/commands/command.js";
import type { RegionName } from "../interfaces/weather/names/regionName.js";
import { ForecastChoices } from "../config/ForecastChoices"; const isRegionName = (region: string): region is RegionName => {
import { Command } from "../interfaces/commands/Command"; return [ "Rudania", "Inariko", "Vhintl" ].includes(region);
import { RegionName } from "../interfaces/weather/names/RegionName"; };
import { generateWeatherEmbed } from "../modules/generateWeatherEmbed";
import { getWeatherForecast } from "../modules/getWeatherForecast";
import { errorHandler } from "../utils/errorHandler";
export const forecast: Command = { export const forecast: Command = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder().
.setName("forecast") setName("forecast").
.setDescription("Get the weather forecast for a specific region.") setDescription("Get the weather forecast for a specific region.").
.addStringOption((option) => addStringOption((option) => {
option return option.
.setName("region") setName("region").
.setDescription("The region to get a forecast for.") setDescription("The region to get a forecast for.").
.setRequired(true) setRequired(true).
.addChoices(ForecastChoices) addChoices(forecastChoices);
), }),
run: async (interaction, CACHE) => { run: async(interaction, cache) => {
try { try {
await interaction.deferReply(); await interaction.deferReply();
const region = interaction.options.getString( const region = interaction.options.getString("region", true);
"region", if (!isRegionName(region)) {
true await interaction.editReply({ content: `Invalid region: ${region}` });
) as RegionName; return;
const forecast = CACHE[region] || getWeatherForecast(region); }
const response = await generateWeatherEmbed(forecast); const forecastResult = cache[region] ?? getWeatherForecast(region);
const response = await generateWeatherEmbed(forecastResult);
await interaction.editReply(response); await interaction.editReply(response);
} catch (err) { } catch (error) {
const response = await errorHandler(err, "forecast command"); const response = await errorHandler(error, "forecast command");
await interaction.editReply(response); await interaction.editReply(response);
} }
}, },

View File

@ -1,7 +0,0 @@
import { RegionName } from "../interfaces/weather/names/RegionName";
export const ForecastChoices: [string, RegionName][] = [
["Rudania", "Rudania"],
["Inariko", "Inariko"],
["Vhintl", "Vhintl"],
];

View File

@ -1,3 +0,0 @@
import { IntentsString } from "discord.js";
export const IntentOptions: IntentsString[] = ["GUILDS"];

View File

@ -0,0 +1,14 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { RegionName } from "../interfaces/weather/names/regionName.js";
import type { APIApplicationCommandOptionChoice } from "discord.js";
export const forecastChoices:
Array<APIApplicationCommandOptionChoice<RegionName>> = [
{ name: "Rudania", value: "Rudania" },
{ name: "Inariko", value: "Inariko" },
{ name: "Vhintl", value: "Vhintl" },
];

View File

@ -0,0 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { GatewayIntentBits } from "discord.js";
export const intentOptions = [ GatewayIntentBits.Guilds ];

View File

@ -1,43 +1,49 @@
import { Precipitation } from "../../interfaces/weather/Precipitation"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Precipitation } from "../../interfaces/weather/precipitation.js";
export const precipitations: Precipitation[] = [ export const precipitations: Array<Precipitation> = [
{ {
emote: "❄️",
name: "Blizzard", name: "Blizzard",
temps: [ "Cold", "Freezing", "Frigid" ], temps: [ "Cold", "Freezing", "Frigid" ],
winds: [ "Strong", "Gale", "Storm", "Hurricane" ], winds: [ "Strong", "Gale", "Storm", "Hurricane" ],
emote: "❄️",
}, },
{ {
emote: "🔥",
name: "Cinder Storm", name: "Cinder Storm",
temps: "any", temps: "any",
winds: [ "Strong", "Gale", "Storm", "Hurricane" ], winds: [ "Strong", "Gale", "Storm", "Hurricane" ],
emote: "🔥",
}, },
{ {
emote: "☁️",
name: "Cloudy", name: "Cloudy",
temps: "any", temps: "any",
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "☁️",
}, },
{ {
emote: "🌫️",
name: "Fog", name: "Fog",
temps: "any", temps: "any",
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌫️",
}, },
{ {
emote: "☁️🧊",
name: "Hail", name: "Hail",
temps: "any", temps: "any",
winds: "any", winds: "any",
emote: "☁️🧊",
}, },
{ {
emote: "🌡️⚡",
name: "Heat Lightning", name: "Heat Lightning",
temps: [ "Warm", "Hot", "Scorching", "Heat Wave" ], temps: [ "Warm", "Hot", "Scorching", "Heat Wave" ],
winds: "any", winds: "any",
emote: "🌡️⚡",
}, },
{ {
emote: "🌧️",
name: "Heavy Rain", name: "Heavy Rain",
temps: [ temps: [
"Brisk", "Brisk",
@ -50,15 +56,15 @@ export const precipitations: Precipitation[] = [
"Heat Wave", "Heat Wave",
], ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌧️",
}, },
{ {
emote: "🌨️",
name: "Heavy Snow", name: "Heavy Snow",
temps: [ "Chilly", "Cold", "Freezing", "Frigid" ], temps: [ "Chilly", "Cold", "Freezing", "Frigid" ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌨️",
}, },
{ {
emote: "☔",
name: "Light Rain", name: "Light Rain",
temps: [ temps: [
"Brisk", "Brisk",
@ -71,21 +77,21 @@ export const precipitations: Precipitation[] = [
"Heat Wave", "Heat Wave",
], ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "☔",
}, },
{ {
emote: "🌨️",
name: "Light Snow", name: "Light Snow",
temps: [ "Chilly", "Cold", "Freezing", "Frigid" ], temps: [ "Chilly", "Cold", "Freezing", "Frigid" ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌨️",
}, },
{ {
emote: "⛅",
name: "Partly Cloudy", name: "Partly Cloudy",
temps: "any", temps: "any",
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "⛅",
}, },
{ {
emote: "🌧️",
name: "Rain", name: "Rain",
temps: [ temps: [
"Brisk", "Brisk",
@ -98,27 +104,27 @@ export const precipitations: Precipitation[] = [
"Heat Wave", "Heat Wave",
], ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌧️",
}, },
{ {
emote: "🌈",
name: "Rainbow", name: "Rainbow",
temps: "any", temps: "any",
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌈",
}, },
{ {
emote: "☁️🧊",
name: "Sleet", name: "Sleet",
temps: [ "Brisk", "Chilly" ], temps: [ "Brisk", "Chilly" ],
winds: "any", winds: "any",
emote: "☁️🧊",
}, },
{ {
emote: "🌨️",
name: "Snow", name: "Snow",
temps: [ "Chilly", "Cold", "Freezing", "Frigid" ], temps: [ "Chilly", "Cold", "Freezing", "Frigid" ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌨️",
}, },
{ {
emote: "🌦️",
name: "Sun Shower", name: "Sun Shower",
temps: [ temps: [
"Brisk", "Brisk",
@ -131,21 +137,21 @@ export const precipitations: Precipitation[] = [
"Heat Wave", "Heat Wave",
], ],
winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ], winds: [ "Gale", "Strong", "Fresh", "Moderate", "Breeze", "Calm" ],
emote: "🌦️",
}, },
{ {
emote: "☀️",
name: "Sunny", name: "Sunny",
temps: "any", temps: "any",
winds: "any", winds: "any",
emote: "☀️",
}, },
{ {
emote: "🌨️⚡",
name: "Thundersnow", name: "Thundersnow",
temps: [ "Chilly", "Cold", "Freezing", "Frigid" ], temps: [ "Chilly", "Cold", "Freezing", "Frigid" ],
winds: "any", winds: "any",
emote: "🌨️⚡",
}, },
{ {
emote: "⛈️",
name: "Thunderstorm", name: "Thunderstorm",
temps: [ temps: [
"Brisk", "Brisk",
@ -158,6 +164,5 @@ export const precipitations: Precipitation[] = [
"Heat Wave", "Heat Wave",
], ],
winds: "any", winds: "any",
emote: "⛈️",
}, },
]; ];

View File

@ -1,8 +1,32 @@
import { RegionRestriction } from "../../../interfaces/weather/regions/RegionRestriction"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { RegionRestriction }
from "../../../interfaces/weather/regions/regionRestriction.js";
export const inarikoSeasons: RegionRestriction[] = [ export const inarikoSeasons: Array<RegionRestriction> = [
{ {
precipitation: [
"Blizzard",
"Cloudy",
"Fog",
"Hail",
"Heavy Rain",
"Heavy Snow",
"Light Rain",
"Light Snow",
"Partly Cloudy",
"Rain",
"Sleet",
"Snow",
"Sunny",
"Thundersnow",
"Thunderstorm",
],
season: "Winter", season: "Winter",
special: [ "Avalanche", "Meteor Shower", "Blight Rain" ],
temps: [ "Frigid", "Freezing", "Cold", "Chilly", "Brisk" ], temps: [ "Frigid", "Freezing", "Cold", "Chilly", "Brisk" ],
wind: [ wind: [
"Calm", "Calm",
@ -14,27 +38,33 @@ export const inarikoSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Blizzard",
"Cloudy", "Cloudy",
"Fog", "Fog",
"Hail", "Hail",
"Heavy Rain", "Heavy Rain",
"Heavy Snow",
"Light Rain", "Light Rain",
"Light Snow", "Light Snow",
"Partly Cloudy", "Partly Cloudy",
"Rain", "Rain",
"Rainbow",
"Sleet", "Sleet",
"Snow", "Snow",
"Sun Shower",
"Sunny", "Sunny",
"Thundersnow",
"Thunderstorm", "Thunderstorm",
], ],
special: ["Avalanche", "Meteor Shower", "Blight Rain"],
},
{
season: "Spring", season: "Spring",
special: [
"Fairy Circle",
"Flood",
"Flower Bloom",
"Jubilee",
"Meteor Shower",
"Blight Rain",
],
temps: [ "Chilly", "Brisk", "Cool", "Mild", "Perfect", "Warm" ], temps: [ "Chilly", "Brisk", "Cool", "Mild", "Perfect", "Warm" ],
wind: [ wind: [
"Calm", "Calm",
@ -46,44 +76,8 @@ export const inarikoSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
precipitation: [
"Cloudy",
"Fog",
"Hail",
"Heavy Rain",
"Light Rain",
"Light Snow",
"Partly Cloudy",
"Rain",
"Rainbow",
"Sleet",
"Snow",
"Sun Shower",
"Sunny",
"Thunderstorm",
],
special: [
"Fairy Circle",
"Flood",
"Flower Bloom",
"Jubilee",
"Meteor Shower",
"Blight Rain",
],
}, },
{ {
season: "Summer",
temps: ["Mild", "Perfect", "Warm", "Hot", "Scorching"],
wind: [
"Calm",
"Breeze",
"Moderate",
"Fresh",
"Strong",
"Gale",
"Storm",
"Hurricane",
],
precipitation: [ precipitation: [
"Cloudy", "Cloudy",
"Fog", "Fog",
@ -98,6 +92,7 @@ export const inarikoSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Summer",
special: [ special: [
"Fairy Circle", "Fairy Circle",
"Flood", "Flood",
@ -107,10 +102,7 @@ export const inarikoSeasons: RegionRestriction[] = [
"Muggy", "Muggy",
"Blight Rain", "Blight Rain",
], ],
}, temps: [ "Mild", "Perfect", "Warm", "Hot", "Scorching" ],
{
season: "Fall",
temps: ["Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -121,6 +113,8 @@ export const inarikoSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Blizzard", "Blizzard",
"Cloudy", "Cloudy",
@ -140,6 +134,7 @@ export const inarikoSeasons: RegionRestriction[] = [
"Thundersnow", "Thundersnow",
"Thunderstorm", "Thunderstorm",
], ],
season: "Fall",
special: [ special: [
"Avalanche", "Avalanche",
"Fairy Circle", "Fairy Circle",
@ -150,5 +145,16 @@ export const inarikoSeasons: RegionRestriction[] = [
"Muggy", "Muggy",
"Blight Rain", "Blight Rain",
], ],
temps: [ "Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect" ],
wind: [
"Calm",
"Breeze",
"Moderate",
"Fresh",
"Strong",
"Gale",
"Storm",
"Hurricane",
],
}, },
]; ];

View File

@ -1,19 +1,13 @@
import { RegionRestriction } from "../../../interfaces/weather/regions/RegionRestriction"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { RegionRestriction }
from "../../../interfaces/weather/regions/regionRestriction.js";
export const rudaniaSeasons: RegionRestriction[] = [ export const rudaniaSeasons: Array<RegionRestriction> = [
{ {
season: "Winter",
temps: ["Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect"],
wind: [
"Calm",
"Breeze",
"Moderate",
"Fresh",
"Strong",
"Gale",
"Storm",
"Hurricane",
],
precipitation: [ precipitation: [
"Cloudy", "Cloudy",
"Fog", "Fog",
@ -30,11 +24,9 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Winter",
special: [ "Flood", "Meteor Shower", "Rock Slide", "Blight Rain" ], special: [ "Flood", "Meteor Shower", "Rock Slide", "Blight Rain" ],
}, temps: [ "Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect" ],
{
season: "Spring",
temps: ["Brisk", "Cool", "Mild", "Perfect", "Warm", "Hot", "Scorching"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -45,6 +37,8 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Cinder Storm", "Cinder Storm",
"Cloudy", "Cloudy",
@ -59,6 +53,7 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Spring",
special: [ special: [
"Drought", "Drought",
"Fairy Circle", "Fairy Circle",
@ -68,10 +63,7 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Muggy", "Muggy",
"Blight Rain", "Blight Rain",
], ],
}, temps: [ "Brisk", "Cool", "Mild", "Perfect", "Warm", "Hot", "Scorching" ],
{
season: "Summer",
temps: ["Mild", "Perfect", "Warm", "Hot", "Scorching", "Heat Wave"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -82,6 +74,8 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Cinder Storm", "Cinder Storm",
"Cloudy", "Cloudy",
@ -98,6 +92,7 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Summer",
special: [ special: [
"Drought", "Drought",
"Fairy Circle", "Fairy Circle",
@ -109,10 +104,7 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Rock Slide", "Rock Slide",
"Blight Rain", "Blight Rain",
], ],
}, temps: [ "Mild", "Perfect", "Warm", "Hot", "Scorching", "Heat Wave" ],
{
season: "Fall",
temps: ["Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -123,6 +115,8 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Cinder Storm", "Cinder Storm",
"Cloudy", "Cloudy",
@ -140,6 +134,7 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Fall",
special: [ special: [
"Drought", "Drought",
"Fairy Circle", "Fairy Circle",
@ -149,5 +144,16 @@ export const rudaniaSeasons: RegionRestriction[] = [
"Rock Slide", "Rock Slide",
"Blight Rain", "Blight Rain",
], ],
temps: [ "Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect" ],
wind: [
"Calm",
"Breeze",
"Moderate",
"Fresh",
"Strong",
"Gale",
"Storm",
"Hurricane",
],
}, },
]; ];

View File

@ -1,19 +1,13 @@
import { RegionRestriction } from "../../../interfaces/weather/regions/RegionRestriction"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { RegionRestriction }
from "../../../interfaces/weather/regions/regionRestriction.js";
export const vhintlSeasons: RegionRestriction[] = [ export const vhintlSeasons: Array<RegionRestriction> = [
{ {
season: "Winter",
temps: ["Cold", "Chilly", "Brisk", "Cool", "Mild"],
wind: [
"Calm",
"Breeze",
"Moderate",
"Fresh",
"Strong",
"Gale",
"Storm",
"Hurricane",
],
precipitation: [ precipitation: [
"Cloudy", "Cloudy",
"Fog", "Fog",
@ -29,11 +23,9 @@ export const vhintlSeasons: RegionRestriction[] = [
"Thundersnow", "Thundersnow",
"Thunderstorm", "Thunderstorm",
], ],
season: "Winter",
special: [ "Fairy Circle", "Meteor Shower", "Blight Rain" ], special: [ "Fairy Circle", "Meteor Shower", "Blight Rain" ],
}, temps: [ "Cold", "Chilly", "Brisk", "Cool", "Mild" ],
{
season: "Spring",
temps: ["Chilly", "Brisk", "Cool", "Mild", "Perfect", "Warm", "Hot"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -44,6 +36,8 @@ export const vhintlSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Cloudy", "Cloudy",
"Fog", "Fog",
@ -59,6 +53,7 @@ export const vhintlSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Spring",
special: [ special: [
"Fairy Circle", "Fairy Circle",
"Flood", "Flood",
@ -68,10 +63,7 @@ export const vhintlSeasons: RegionRestriction[] = [
"Muggy", "Muggy",
"Blight Rain", "Blight Rain",
], ],
}, temps: [ "Chilly", "Brisk", "Cool", "Mild", "Perfect", "Warm", "Hot" ],
{
season: "Summer",
temps: ["Mild", "Perfect", "Warm", "Hot", "Scorching", "Heat Wave"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -82,6 +74,8 @@ export const vhintlSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Cloudy", "Cloudy",
"Fog", "Fog",
@ -96,6 +90,7 @@ export const vhintlSeasons: RegionRestriction[] = [
"Sunny", "Sunny",
"Thunderstorm", "Thunderstorm",
], ],
season: "Summer",
special: [ special: [
"Fairy Circle", "Fairy Circle",
"Flood", "Flood",
@ -105,10 +100,7 @@ export const vhintlSeasons: RegionRestriction[] = [
"Muggy", "Muggy",
"Blight Rain", "Blight Rain",
], ],
}, temps: [ "Mild", "Perfect", "Warm", "Hot", "Scorching", "Heat Wave" ],
{
season: "Fall",
temps: ["Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect"],
wind: [ wind: [
"Calm", "Calm",
"Breeze", "Breeze",
@ -119,6 +111,8 @@ export const vhintlSeasons: RegionRestriction[] = [
"Storm", "Storm",
"Hurricane", "Hurricane",
], ],
},
{
precipitation: [ precipitation: [
"Cloudy", "Cloudy",
"Fog", "Fog",
@ -134,6 +128,7 @@ export const vhintlSeasons: RegionRestriction[] = [
"Thundersnow", "Thundersnow",
"Thunderstorm", "Thunderstorm",
], ],
season: "Fall",
special: [ special: [
"Fairy Circle", "Fairy Circle",
"Flood", "Flood",
@ -143,5 +138,16 @@ export const vhintlSeasons: RegionRestriction[] = [
"Muggy", "Muggy",
"Blight Rain", "Blight Rain",
], ],
temps: [ "Cold", "Chilly", "Brisk", "Cool", "Mild", "Perfect" ],
wind: [
"Calm",
"Breeze",
"Moderate",
"Fresh",
"Strong",
"Gale",
"Storm",
"Hurricane",
],
}, },
]; ];

View File

@ -1,17 +1,28 @@
import { Special } from "../../interfaces/weather/Special"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Special } from "../../interfaces/weather/special.js";
export const specials: Special[] = [ export const specials: Array<Special> = [
{ {
name: "Avalanche",
temps: ["Chilly", "Cold", "Freezing", "Frigid"],
winds: "any",
precipitations: ["Snow"],
description: description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"There has been an avalanche and some roads are blocked! Travel to and from this village today is impossible.", "There has been an avalanche and some roads are blocked! Travel to and from this village today is impossible.",
emote: "🏔️", emote: "🏔️",
name: "Avalanche",
precipitations: [ "Snow" ],
temps: [ "Chilly", "Cold", "Freezing", "Frigid" ],
winds: "any",
}, },
{ {
description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"Blighted rain falls from the sky, staining the ground and creating sickly maroon-tinged puddles... if you roll for gathering today you must also `/tableroll blightrain` to see if you get infected! If you miss doing this roll with your gather, blighting will be automatic.",
emote: "🌧️🧿",
name: "Blight Rain", name: "Blight Rain",
precipitations: [ "Rain" ],
temps: [ temps: [
"Brisk", "Brisk",
"Cool", "Cool",
@ -23,31 +34,34 @@ export const specials: Special[] = [
"Heat Wave", "Heat Wave",
], ],
winds: "any", winds: "any",
precipitations: ["Rain"],
description:
"Blighted rain falls from the sky, staining the ground and creating sickly maroon-tinged puddles... if you roll for gathering today you must also `/tableroll blightrain` to see if you get infected! If you miss doing this roll with your gather, blighting will be automatic.",
emote: "🌧️🧿",
}, },
{ {
name: "Drought",
temps: ["Scorching", "Heat Wave"],
winds: "any",
precipitations: ["Sunny"],
description: description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"A drought has dried up the smaller vegetation surrounding the village... any plants or mushrooms rolled today are found dead and will not be gathered.", "A drought has dried up the smaller vegetation surrounding the village... any plants or mushrooms rolled today are found dead and will not be gathered.",
emote: "🌵", emote: "🌵",
name: "Drought",
precipitations: [ "Sunny" ],
temps: [ "Scorching", "Heat Wave" ],
winds: "any",
}, },
{ {
name: "Fairy Circle",
temps: "any",
winds: "any",
precipitations: "any",
description: description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"Fairy circles have popped up all over Hyrule! All residents and visitors may use `/tableroll fairycircle` to gather mushrooms today!", "Fairy circles have popped up all over Hyrule! All residents and visitors may use `/tableroll fairycircle` to gather mushrooms today!",
emote: "🍄", emote: "🍄",
name: "Fairy Circle",
precipitations: "any",
temps: "any",
winds: "any",
}, },
{ {
description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"There has been a Flood! Travelling to and from this village is impossible today due to the danger.",
emote: "🌊",
name: "Flood", name: "Flood",
precipitations: [ "Rain" ],
temps: [ temps: [
"Cold", "Cold",
"Chilly", "Chilly",
@ -61,54 +75,55 @@ export const specials: Special[] = [
"Heat Wave", "Heat Wave",
], ],
winds: "any", winds: "any",
precipitations: ["Rain"],
description:
"There has been a Flood! Travelling to and from this village is impossible today due to the danger.",
emote: "🌊",
}, },
{ {
name: "Flower Bloom",
temps: ["Perfect", "Warm", "Hot", "Scorching", "Heat Wave"],
winds: "any",
precipitations: "any",
description: description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"An overabundance of plants and flowers have been spotted growing in and around the village! All residents and visitors may `/tableroll flowerbloom` to gather today!", "An overabundance of plants and flowers have been spotted growing in and around the village! All residents and visitors may `/tableroll flowerbloom` to gather today!",
emote: "🌼", emote: "🌼",
}, name: "Flower Bloom",
{
name: "Jubilee",
temps: "any",
winds: "any",
precipitations: "any", precipitations: "any",
description:
"Fish are practically jumping out of the water! All residents and visitors may `/tableroll jubilee` to catch some fish!",
emote: "🐟",
},
{
name: "Meteor Shower",
temps: "any",
winds: "any",
precipitations: ["Sunny"],
description:
"Shooting starts have been spotted streaking through the sky! Quick, all residents and visitors make a wish and use `/tableroll meteorshower` for a chance to find a star fragment!",
emote: "☄️",
},
{
name: "Muggy",
temps: [ "Perfect", "Warm", "Hot", "Scorching", "Heat Wave" ], temps: [ "Perfect", "Warm", "Hot", "Scorching", "Heat Wave" ],
winds: "any", winds: "any",
precipitations: ["Rain", "Fog", "Cloudy"],
description:
"Oof! Sure is humid today! Critters are out and about more than usual. All residents and visitors may use `/tableroll muggy` to catch some critters!",
emote: "🐛",
}, },
{ {
name: "Rock Slide", description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"Fish are practically jumping out of the water! All residents and visitors may `/tableroll jubilee` to catch some fish!",
emote: "🐟",
name: "Jubilee",
precipitations: "any",
temps: "any", temps: "any",
winds: "any", winds: "any",
precipitations: "any", },
{
description: description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"Shooting starts have been spotted streaking through the sky! Quick, all residents and visitors make a wish and use `/tableroll meteorshower` for a chance to find a star fragment!",
emote: "☄️",
name: "Meteor Shower",
precipitations: [ "Sunny" ],
temps: "any",
winds: "any",
},
{
description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"Oof! Sure is humid today! Critters are out and about more than usual. All residents and visitors may use `/tableroll muggy` to catch some critters!",
emote: "🐛",
name: "Muggy",
precipitations: [ "Rain", "Fog", "Cloudy" ],
temps: [ "Perfect", "Warm", "Hot", "Scorching", "Heat Wave" ],
winds: "any",
},
{
description:
// eslint-disable-next-line stylistic/max-len -- We can't break this string up.
"Oh no there's been a rock slide! Travelling to and from this village is impossible today. All residents and visitors may use `/tableroll rockslide` to help clear the road! You might just find something interesting while you work...", "Oh no there's been a rock slide! Travelling to and from this village is impossible today. All residents and visitors may use `/tableroll rockslide` to help clear the road! You might just find something interesting while you work...",
emote: "⛏️", emote: "⛏️",
name: "Rock Slide",
precipitations: "any",
temps: "any",
winds: "any",
}, },
]; ];

View File

@ -1,76 +1,81 @@
import { Temperature } from "../../interfaces/weather/Temperature"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Temperature } from "../../interfaces/weather/temperature.js";
export const temperatures: Temperature[] = [ export const temperatures: Array<Temperature> = [
{ {
fahrenheit: 0,
celsius: -18, celsius: -18,
name: "Frigid",
emote: "🥶", emote: "🥶",
fahrenheit: 0,
name: "Frigid",
}, },
{ {
fahrenheit: 8,
celsius: -14, celsius: -14,
name: "Freezing",
emote: "🐧", emote: "🐧",
fahrenheit: 8,
name: "Freezing",
}, },
{ {
fahrenheit: 24,
celsius: -4, celsius: -4,
name: "Cold",
emote: "☃️", emote: "☃️",
},
{
fahrenheit: 36,
celsius: 2,
name: "Chilly",
emote: "🧊",
},
{
fahrenheit: 44,
celsius: 6,
name: "Brisk",
emote: "🔷",
},
{
fahrenheit: 52,
celsius: 11,
name: "Cool",
emote: "🆒",
},
{
fahrenheit: 61,
celsius: 16,
name: "Mild",
emote: "😐",
},
{
fahrenheit: 72,
celsius: 22,
name: "Perfect",
emote: "👌",
},
{
fahrenheit: 24, fahrenheit: 24,
name: "Cold",
},
{
celsius: 2,
emote: "🧊",
fahrenheit: 36,
name: "Chilly",
},
{
celsius: 6,
emote: "🔷",
fahrenheit: 44,
name: "Brisk",
},
{
celsius: 11,
emote: "🆒",
fahrenheit: 52,
name: "Cool",
},
{
celsius: 16,
emote: "😐",
fahrenheit: 61,
name: "Mild",
},
{
celsius: 22,
emote: "👌",
fahrenheit: 72,
name: "Perfect",
},
{
celsius: 28, celsius: 28,
name: "Warm",
emote: "🌡️", emote: "🌡️",
fahrenheit: 24,
name: "Warm",
}, },
{ {
fahrenheit: 89,
celsius: 32, celsius: 32,
name: "Hot",
emote: "🌶️", emote: "🌶️",
fahrenheit: 89,
name: "Hot",
}, },
{ {
fahrenheit: 97,
celsius: 36, celsius: 36,
name: "Scorching",
emote: "🥵", emote: "🥵",
fahrenheit: 97,
name: "Scorching",
}, },
{ {
fahrenheit: 100,
celsius: 38, celsius: 38,
name: "Heat Wave",
emote: "💯", emote: "💯",
fahrenheit: 100,
name: "Heat Wave",
}, },
]; ];

View File

@ -1,52 +1,57 @@
import { Wind } from "../../interfaces/weather/Wind"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Wind } from "../../interfaces/weather/wind.js";
export const winds: Wind[] = [ export const winds: Array<Wind> = [
{ {
name: "Calm",
lowSpeed: 0,
highSpeed: 1,
emote: "😌", emote: "😌",
highSpeed: 1,
lowSpeed: 0,
name: "Calm",
}, },
{ {
name: "Breeze",
lowSpeed: 2,
highSpeed: 12,
emote: "🎐", emote: "🎐",
highSpeed: 12,
lowSpeed: 2,
name: "Breeze",
}, },
{ {
name: "Moderate",
lowSpeed: 13,
highSpeed: 30,
emote: "🍃", emote: "🍃",
highSpeed: 30,
lowSpeed: 13,
name: "Moderate",
}, },
{ {
name: "Fresh",
lowSpeed: 31,
highSpeed: 40,
emote: "🌬️", emote: "🌬️",
highSpeed: 40,
lowSpeed: 31,
name: "Fresh",
}, },
{ {
name: "Strong",
lowSpeed: 41,
highSpeed: 62,
emote: "💫", emote: "💫",
highSpeed: 62,
lowSpeed: 41,
name: "Strong",
}, },
{ {
name: "Gale",
lowSpeed: 63,
highSpeed: 87,
emote: "💨", emote: "💨",
highSpeed: 87,
lowSpeed: 63,
name: "Gale",
}, },
{ {
name: "Storm",
lowSpeed: 88,
highSpeed: 117,
emote: "🌀", emote: "🌀",
highSpeed: 117,
lowSpeed: 88,
name: "Storm",
}, },
{ {
name: "Hurricane",
lowSpeed: 118,
highSpeed: 150,
emote: "🌪️", emote: "🌪️",
highSpeed: 150,
lowSpeed: 118,
name: "Hurricane",
}, },
]; ];

View File

@ -1,21 +1,27 @@
import { Client } from "discord.js"; /**
* @copyright nhcarrigan
import { WeatherCache } from "../interfaces/WeatherCache"; * @license Naomi's Public License
* @author Naomi Carrigan
import { onInteraction } from "./handlers/onInteraction"; */
import { onReady } from "./handlers/onReady"; import { onInteraction } from "./handlers/onInteraction.js";
import { onReady } from "./handlers/onReady.js";
import type { WeatherCache } from "../interfaces/weatherCache.js";
import type { Client } from "discord.js";
/** /**
* Mounts listeners for the Discord gateway events. * Mounts listeners for the Discord gateway events.
* * @param bot - The bot's discord instance.
* @param {Client} BOT The bot's discord instance. * @param cache - The cache of weather data.
* @param { WeatherCache } CACHE The cache of weather data.
*/ */
export const handleEvents = (BOT: Client, CACHE: WeatherCache) => { export const handleEvents = (bot: Client, cache: WeatherCache): void => {
BOT.on("ready", async () => await onReady(BOT, CACHE)); bot.on("ready", async() => {
await onReady(bot, cache);
});
BOT.on( bot.on(
"interactionCreate", "interactionCreate",
async (interaction) => await onInteraction(interaction, CACHE) async(interaction) => {
await onInteraction(interaction, cache);
},
); );
}; };

View File

@ -1,25 +1,30 @@
import { Interaction } from "discord.js"; /**
* @copyright nhcarrigan
import { CommandList } from "../../commands/_CommandList"; * @license Naomi's Public License
import { WeatherCache } from "../../interfaces/WeatherCache"; * @author Naomi Carrigan
*/
import { commandList } from "../../commands/_commandList.js";
import type { WeatherCache } from "../../interfaces/weatherCache.js";
import type { Interaction } from "discord.js";
/** /**
* Handles the INTERACTION_CREATE event from Discord. Checks if the interaction is * Handles the INTERACTION_CREATE event from Discord. Checks if the interaction is
* an application command, and if there is a matching command, and runs it. * an application command, and if there is a matching command, and runs it.
* * @param interaction - The interaction payload from Discord.
* @param {Interaction} interaction The interaction payload from Discord. * @param cache - The cache of weather data.
* @param { WeatherCache } CACHE The cache of weather data.
*/ */
export const onInteraction = async( export const onInteraction = async(
interaction: Interaction, interaction: Interaction,
CACHE: WeatherCache cache: WeatherCache,
) => { ): Promise<void> => {
if (!interaction.isCommand()) { if (!interaction.isChatInputCommand()) {
return; return;
} }
const target = CommandList.find( const target = commandList.find(
(command) => command.data.name === interaction.commandName (command) => {
return command.data.name === interaction.commandName;
},
); );
if (!target) { if (!target) {
@ -27,5 +32,5 @@ export const onInteraction = async (
return; return;
} }
await target.run(interaction, CACHE); await target.run(interaction, cache);
}; };

View File

@ -1,40 +1,46 @@
import { REST } from "@discordjs/rest"; /**
import { Routes } from "discord-api-types/v9"; * @copyright nhcarrigan
import { Client, WebhookClient } from "discord.js"; * @license Naomi's Public License
* @author Naomi Carrigan
import { CommandList } from "../../commands/_CommandList"; */
import { WeatherCache } from "../../interfaces/WeatherCache"; import { REST, Routes, type Client, WebhookClient } from "discord.js";
import { scheduleForecasts } from "../../modules/scheduleForecasts"; import { commandList } from "../../commands/_commandList.js";
import { logHandler } from "../../utils/logHandler"; import { scheduleForecasts } from "../../modules/scheduleForecasts.js";
import { logHandler } from "../../utils/logHandler.js";
import type { WeatherCache } from "../../interfaces/weatherCache.js";
/** /**
* Handler for the READY event from Discord. Logs that the bot is connected, * Handler for the READY event from Discord. Logs that the bot is connected,
* then registers the guild slash commands. * then registers the guild slash commands.
* * @param bot - The bot's Discord instance.
* @param {Client} BOT The bot's Discord instance. * @param cache - The cache of weather data.
* @param {WeatherCache} CACHE The cache of weather data.
*/ */
export const onReady = async (BOT: Client, CACHE: WeatherCache) => { export const onReady = async (
bot: Client,
cache: WeatherCache
): Promise<void> => {
const webhook = new WebhookClient({ url: process.env.DEBUG_HOOK as string }); const webhook = new WebhookClient({ url: process.env.DEBUG_HOOK as string });
await webhook.send("Ruu Bot is online!"); await webhook.send("Ruu Bot is online!");
logHandler.log("info", "Connected to Discord!"); logHandler.log("info", "Connected to Discord!");
const rest = new REST({ version: "9" }).setToken( const rest = new REST({ version: "9" }).setToken(
process.env.DISCORD_TOKEN as string process.env.DISCORD_TOKEN ?? ""
); );
const commandData = CommandList.map((command) => command.data.toJSON()); const commandData = commandList.map((command) => {
return command.data.toJSON();
});
await rest.put( await rest.put(
Routes.applicationGuildCommands( Routes.applicationGuildCommands(
BOT.user?.id || "oopsie whoopsie", bot.user?.id ?? "oopsie whoopsie",
process.env.HOME_GUILD_ID as string process.env.HOME_GUILD_ID ?? ""
), ),
{ body: commandData } { body: commandData }
); );
logHandler.log("info", "Registered commands!"); logHandler.log("info", "Registered commands!");
scheduleForecasts(CACHE); scheduleForecasts(cache);
}; };

View File

@ -1,37 +1,27 @@
import { RewriteFrames } from "@sentry/integrations"; /**
import * as Sentry from "@sentry/node"; * @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Client } from "discord.js"; import { Client } from "discord.js";
import { intentOptions } from "./config/intentOptions.js";
import { handleEvents } from "./events/handleEvents.js";
import { getWeatherForecast } from "./modules/getWeatherForecast.js";
import { loadChannels } from "./modules/loadChannels.js";
import { errorHandler } from "./utils/errorHandler.js";
import type { WeatherCache } from "./interfaces/weatherCache.js";
import { IntentOptions } from "./config/IntentOptions"; const bot = new Client({ intents: intentOptions });
import { handleEvents } from "./events/handleEvents";
import { WeatherCache } from "./interfaces/WeatherCache";
import { getWeatherForecast } from "./modules/getWeatherForecast";
import { loadChannels } from "./modules/loadChannels";
import { errorHandler } from "./utils/errorHandler";
(async () => { const cache: WeatherCache = {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
integrations: [
new RewriteFrames({
root: global.__dirname,
}),
],
});
const BOT = new Client({ intents: IntentOptions });
const CACHE: WeatherCache = {
Rudania: getWeatherForecast("Rudania"),
Inariko: getWeatherForecast("Inariko"), Inariko: getWeatherForecast("Inariko"),
Rudania: getWeatherForecast("Rudania"),
Vhintl: getWeatherForecast("Vhintl"), Vhintl: getWeatherForecast("Vhintl"),
channels: await loadChannels(BOT), channels: await loadChannels(bot),
}; };
handleEvents(BOT, CACHE); handleEvents(bot, cache);
await BOT.login(process.env.DISCORD_TOKEN as string).catch( await bot.login(process.env.DISCORD_TOKEN ?? "").catch(async(error: unknown) => {
async (err) => await errorHandler(err, "login") return await errorHandler(error, "login");
); });
})();

View File

@ -1,14 +0,0 @@
import { NewsChannel, TextChannel } from "discord.js";
import { WeatherForecast } from "./weather/WeatherForecast";
export interface WeatherCache {
Rudania: WeatherForecast | null;
Inariko: WeatherForecast | null;
Vhintl: WeatherForecast | null;
channels: {
Rudania: TextChannel | NewsChannel;
Inariko: TextChannel | NewsChannel;
Vhintl: TextChannel | NewsChannel;
};
}

View File

@ -1,4 +0,0 @@
export interface AttachmentData {
attachmentString: string;
filePath: string;
}

View File

@ -1,14 +0,0 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from "@discordjs/builders";
import { CommandInteraction } from "discord.js";
import { WeatherCache } from "../WeatherCache";
export interface Command {
data:
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">
| SlashCommandSubcommandsOnlyBuilder;
run: (interaction: CommandInteraction, CACHE: WeatherCache) => Promise<void>;
}

View File

@ -0,0 +1,9 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface AttachmentData {
attachmentString: string;
filePath: string;
}

View File

@ -0,0 +1,20 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { WeatherCache } from "../weatherCache.js";
import type {
SlashCommandSubcommandsOnlyBuilder,
ChatInputCommandInteraction,
SlashCommandOptionsOnlyBuilder,
} from "discord.js";
export interface Command {
data:
SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder;
run: (
interaction: ChatInputCommandInteraction,
cache: WeatherCache
)=> Promise<void>;
}

View File

@ -1,10 +0,0 @@
import { PrecipitationName } from "./names/PrecipitationName";
import { TemperatureName } from "./names/TemperatureName";
import { WindName } from "./names/WindName";
export interface Precipitation {
name: PrecipitationName;
temps: TemperatureName[] | "any";
winds: WindName[] | "any";
emote: string;
}

View File

@ -1,13 +0,0 @@
import { PrecipitationName } from "./names/PrecipitationName";
import { SpecialName } from "./names/SpecialName";
import { TemperatureName } from "./names/TemperatureName";
import { WindName } from "./names/WindName";
export interface Special {
name: SpecialName;
temps: TemperatureName[] | "any";
winds: WindName[] | "any";
precipitations: PrecipitationName[] | "any";
description: string;
emote: string;
}

View File

@ -1,8 +0,0 @@
import { TemperatureName } from "./names/TemperatureName";
export interface Temperature {
fahrenheit: number;
celsius: number;
name: TemperatureName;
emote: string;
}

View File

@ -1,15 +0,0 @@
import { RegionName } from "./names/RegionName";
import { Season } from "./names/Season";
import { Precipitation } from "./Precipitation";
import { Special } from "./Special";
import { Temperature } from "./Temperature";
import { Wind } from "./Wind";
export interface WeatherForecast {
region: RegionName;
season: Season;
temperature: Temperature;
wind: Wind;
precipitation: Precipitation | null;
special: Special | null;
}

View File

@ -1,8 +0,0 @@
import { WindName } from "./names/WindName";
export interface Wind {
lowSpeed: number;
highSpeed: number;
name: WindName;
emote: string;
}

View File

@ -1 +0,0 @@
export type RegionName = "Rudania" | "Inariko" | "Vhintl";

View File

@ -1 +0,0 @@
export type Season = "Spring" | "Summer" | "Fall" | "Winter";

View File

@ -1,3 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type PrecipitationName = export type PrecipitationName =
| "Blizzard" | "Blizzard"
| "Cinder Storm" | "Cinder Storm"

View File

@ -0,0 +1,6 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type RegionName = "Rudania" | "Inariko" | "Vhintl";

View File

@ -0,0 +1,6 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type Season = "Spring" | "Summer" | "Fall" | "Winter";

View File

@ -1,3 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type SpecialName = export type SpecialName =
| "Avalanche" | "Avalanche"
| "Blight Rain" | "Blight Rain"

View File

@ -1,3 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type TemperatureName = export type TemperatureName =
| "Frigid" | "Frigid"
| "Freezing" | "Freezing"

View File

@ -1,3 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type WindName = export type WindName =
| "Calm" | "Calm"
| "Breeze" | "Breeze"

View File

@ -0,0 +1,15 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { PrecipitationName } from "./names/precipitationName.js";
import type { TemperatureName } from "./names/temperatureName.js";
import type { WindName } from "./names/windName.js";
export interface Precipitation {
name: PrecipitationName;
temps: Array<TemperatureName> | "any";
winds: Array<WindName> | "any";
emote: string;
}

View File

@ -1,13 +0,0 @@
import { PrecipitationName } from "../names/PrecipitationName";
import { Season } from "../names/Season";
import { SpecialName } from "../names/SpecialName";
import { TemperatureName } from "../names/TemperatureName";
import { WindName } from "../names/WindName";
export interface RegionRestriction {
season: Season;
temps: TemperatureName[];
wind: WindName[];
precipitation: PrecipitationName[];
special: SpecialName[];
}

View File

@ -0,0 +1,18 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { PrecipitationName } from "../names/precipitationName.js";
import type { Season } from "../names/season.js";
import type { SpecialName } from "../names/specialName.js";
import type { TemperatureName } from "../names/temperatureName.js";
import type { WindName } from "../names/windName.js";
export interface RegionRestriction {
season: Season;
temps: Array<TemperatureName>;
wind: Array<WindName>;
precipitation: Array<PrecipitationName>;
special: Array<SpecialName>;
}

View File

@ -0,0 +1,18 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { PrecipitationName } from "./names/precipitationName.js";
import type { SpecialName } from "./names/specialName.js";
import type { TemperatureName } from "./names/temperatureName.js";
import type { WindName } from "./names/windName.js";
export interface Special {
name: SpecialName;
temps: Array<TemperatureName> | "any";
winds: Array<WindName> | "any";
precipitations: Array<PrecipitationName> | "any";
description: string;
emote: string;
}

View File

@ -0,0 +1,13 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { TemperatureName } from "./names/temperatureName.js";
export interface Temperature {
fahrenheit: number;
celsius: number;
name: TemperatureName;
emote: string;
}

View File

@ -0,0 +1,20 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { RegionName } from "./names/regionName.js";
import type { Season } from "./names/season.js";
import type { Precipitation } from "./precipitation.js";
import type { Special } from "./special.js";
import type { Temperature } from "./temperature.js";
import type { Wind } from "./wind.js";
export interface WeatherForecast {
region: RegionName;
season: Season;
temperature: Temperature;
wind: Wind;
precipitation: Precipitation | null;
special: Special | null;
}

View File

@ -0,0 +1,13 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { WindName } from "./names/windName.js";
export interface Wind {
lowSpeed: number;
highSpeed: number;
name: WindName;
emote: string;
}

View File

@ -0,0 +1,18 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { WeatherForecast } from "./weather/weatherForecast.js";
import type { NewsChannel, TextChannel } from "discord.js";
export interface WeatherCache {
Rudania: WeatherForecast | null;
Inariko: WeatherForecast | null;
Vhintl: WeatherForecast | null;
channels: {
Rudania: TextChannel | NewsChannel;
Inariko: TextChannel | NewsChannel;
Vhintl: TextChannel | NewsChannel;
};
}

View File

@ -1,49 +1,61 @@
import { MessageEmbed, MessagePayload } from "discord.js"; /**
* @copyright nhcarrigan
import { WeatherForecast } from "../interfaces/weather/WeatherForecast"; * @license Naomi's Public License
* @author Naomi Carrigan
import { generateBanner } from "./images/generateBanner"; */
import { getSeasonIcon } from "./images/getSeasonIcon"; import { EmbedBuilder, type APIEmbedField, type MessageCreateOptions }
from "discord.js";
import { generateBanner } from "./images/generateBanner.js";
import { getSeasonIcon } from "./images/getSeasonIcon.js";
import type { WeatherForecast } from "../interfaces/weather/weatherForecast.js";
/** /**
* Parses a weather forecast into a Discord message embed. * Parses a weather forecast into a Discord message embed.
* * @param forecast - The forecast to parse.
* @param {WeatherForecast} forecast The forecast to parse. * @returns A Discord message embed.
* @returns {MessageEmbed} A Discord message embed.
*/ */
export const generateWeatherEmbed = async( export const generateWeatherEmbed = async(
forecast: WeatherForecast | null forecast: WeatherForecast | null,
): Promise<MessagePayload["options"]> => { ): Promise<MessageCreateOptions> => {
if (!forecast) { if (!forecast) {
const embed = new MessageEmbed() const embed = new EmbedBuilder().
.setTitle("Error") setTitle("Error").
.setDescription("No forecast was generated."); setDescription("No forecast was generated.");
return { embeds: [ embed ] }; return { embeds: [ embed ] };
} }
const weatherEmbed = new MessageEmbed(); const weatherEmbed = new EmbedBuilder();
weatherEmbed.setTitle(`${forecast.region}'s Daily Weather Forecast`); weatherEmbed.setTitle(`${forecast.region}'s Daily Weather Forecast`);
let emoteString = forecast.temperature.emote + forecast.wind.emote; let emoteString = forecast.temperature.emote + forecast.wind.emote;
weatherEmbed.addField( const fields: Array<APIEmbedField> = [
"Temperature", {
`${forecast.temperature.fahrenheit}°F / ${forecast.temperature.celsius}°C [${forecast.temperature.name}]` name: "Temperature",
); value: `${String(forecast.temperature.fahrenheit)}°F / ${String(forecast.temperature.celsius)}°C [${forecast.temperature.name}]`,
weatherEmbed.addField( },
"Wind", {
`${forecast.wind.name} [${forecast.wind.lowSpeed} - ${forecast.wind.highSpeed}kph]` name: "Wind",
); value: `${forecast.wind.name} [${String(forecast.wind.lowSpeed)} - ${String(forecast.wind.highSpeed)}kph]`,
},
];
if (forecast.precipitation) { if (forecast.precipitation) {
weatherEmbed.addField("Precipitation", `${forecast.precipitation.name}`); fields.push({
emoteString += forecast.precipitation.emote; name: "Precipitation",
value: forecast.precipitation.name,
});
emoteString = emoteString + forecast.precipitation.emote;
} }
if (forecast.special) { if (forecast.special) {
weatherEmbed.addField("Description", `${forecast.special.description}`); fields.push({
emoteString += forecast.special.emote; name: "Special Conditions",
value: forecast.special.name,
});
emoteString = emoteString + forecast.special.emote;
} }
weatherEmbed.setDescription(emoteString); weatherEmbed.setDescription(emoteString);
weatherEmbed.addFields(fields);
const seasonIcon = getSeasonIcon(forecast.season); const seasonIcon = getSeasonIcon(forecast.season);
weatherEmbed.setThumbnail(seasonIcon.attachmentString); weatherEmbed.setThumbnail(seasonIcon.attachmentString);
@ -52,14 +64,16 @@ export const generateWeatherEmbed = async (
switch (forecast.region) { switch (forecast.region) {
case "Rudania": case "Rudania":
weatherEmbed.setColor("RED"); weatherEmbed.setColor(0xFF_00_00);
break; break;
case "Inariko": case "Inariko":
weatherEmbed.setColor("BLUE"); weatherEmbed.setColor(0x00_00_FF);
break; break;
case "Vhintl": case "Vhintl":
weatherEmbed.setColor("GREEN"); weatherEmbed.setColor(0x00_FF_00);
break; break;
default:
weatherEmbed.setColor(0x00_00_00);
} }
return { return {

View File

@ -1,26 +1,34 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/** /**
* Module to select a random value from an array. * Module to select a random value from an array.
* * @param array - The array to select from.
* @param {any[]} array The array to select from. * @param weight - Optional parameter to prefer lower or higher values.
* @param {"low" | "high"} weight Optional parameter to prefer lower or higher values. * @returns The selected value.
* @returns {any} The selected value.
*/ */
export const getRandomValue = <T>(array: T[], weight?: "low" | "high"): T => { export const getRandomValue
= <T>(array: Array<T>, weight?: "low" | "high"): T => {
let random = Math.floor(Math.random() * array.length); let random = Math.floor(Math.random() * array.length);
switch (weight) { switch (weight) {
case "low": case "low":
random = Math.min( random = Math.min(
Math.floor(Math.random() * array.length), Math.floor(Math.random() * array.length),
Math.floor(Math.random() * array.length), Math.floor(Math.random() * array.length),
Math.floor(Math.random() * array.length) Math.floor(Math.random() * array.length),
); );
break; break;
case "high": case "high":
random = Math.max( random = Math.max(
Math.floor(Math.random() * array.length), Math.floor(Math.random() * array.length),
Math.floor(Math.random() * array.length), Math.floor(Math.random() * array.length),
Math.floor(Math.random() * array.length) Math.floor(Math.random() * array.length),
); );
break;
default:
break;
} }
return array[random]; return array[random] as T;
}; };

View File

@ -1,24 +1,27 @@
import { temperatures } from "../data/weather/temperatures"; /**
import { winds } from "../data/weather/winds"; * @copyright nhcarrigan
import { RegionName } from "../interfaces/weather/names/RegionName"; * @license Naomi's Public License
import { WeatherForecast } from "../interfaces/weather/WeatherForecast"; * @author Naomi Carrigan
import { errorHandler } from "../utils/errorHandler"; */
import { temperatures } from "../data/weather/temperatures.js";
import { getRandomValue } from "./getRandomValue"; import { winds } from "../data/weather/winds.js";
import { getPrecipitation } from "./weather/getPrecipitation"; import { errorHandler } from "../utils/errorHandler.js";
import { getRegionRestrictions } from "./weather/getRegionRestrictions"; import { getRandomValue } from "./getRandomValue.js";
import { getSeason } from "./weather/getSeason"; import { getPrecipitation } from "./weather/getPrecipitation.js";
import { getSpecial } from "./weather/getSpecial"; import { getRegionRestrictions } from "./weather/getRegionRestrictions.js";
import { getSeason } from "./weather/getSeason.js";
import { getSpecial } from "./weather/getSpecial.js";
import type { RegionName } from "../interfaces/weather/names/regionName.js";
import type { WeatherForecast } from "../interfaces/weather/weatherForecast.js";
/** /**
* Generates a weather forecast for the specified region, using the current * Generates a weather forecast for the specified region, using the current
* date to determine the season. * date to determine the season.
* * @param region - The name of the region to forecast for. Must be one of "Rudania", "Inariko", or "Vhintl".
* @param {RegionName} region The name of the region to forecast for. * @returns The weather forecast.
* @returns {WeatherForecast | null} The weather forecast.
*/ */
export const getWeatherForecast = ( export const getWeatherForecast = (
region: RegionName region: RegionName,
): WeatherForecast | null => { ): WeatherForecast | null => {
try { try {
const season = getSeason(); const season = getSeason();
@ -28,26 +31,30 @@ export const getWeatherForecast = (
return null; return null;
} }
const tempName = getRandomValue(allowedWeather.temps); const temporaryName = getRandomValue(allowedWeather.temps);
const temperature = temperatures.find((el) => el.name === tempName); const temperature = temperatures.find((element) => {
return element.name === temporaryName;
});
const windName = getRandomValue(allowedWeather.wind, "low"); const windName = getRandomValue(allowedWeather.wind, "low");
const wind = winds.find((el) => el.name === windName); const wind = winds.find((element) => {
return element.name === windName;
});
if (!temperature || !wind) { if (!temperature || !wind) {
return null; return null;
} }
const precipitation = getPrecipitation(allowedWeather, tempName, windName); const precipitation = getPrecipitation(allowedWeather, temporaryName, windName);
const special = getSpecial( const special = getSpecial(
allowedWeather, allowedWeather,
tempName, temporaryName,
windName, windName,
precipitation?.name precipitation?.name,
); );
return { region, season, temperature, wind, precipitation, special }; return { precipitation, region, season, special, temperature, wind };
} catch (err) { } catch (error) {
errorHandler(err, "get weather forecast"); void errorHandler(error, "get weather forecast");
return null; return null;
} }
}; };

View File

@ -1,37 +1,34 @@
import { AttachmentData } from "../../interfaces/commands/AttachmentData"; /**
import { WeatherForecast } from "../../interfaces/weather/WeatherForecast"; * @copyright nhcarrigan
* @license Naomi's Public License
import { getBannerImage } from "./getBannerImage"; * @author Naomi Carrigan
import { getOverlayImage } from "./getOverlayImage"; */
import { overlayImages } from "./overlayImages"; import { getBannerImage } from "./getBannerImage.js";
import { getOverlayImage } from "./getOverlayImage.js";
import { overlayImages } from "./overlayImages.js";
import type { AttachmentData }
from "../../interfaces/commands/attachmentData.js";
import type { WeatherForecast }
from "../../interfaces/weather/weatherForecast.js";
/** /**
* Generates the banner image. If an overlay is available, constructs a new banner image by overlaying the overlay on the banner. * Generates the banner image. If an overlay is available, constructs a new banner image by overlaying the overlay on the banner.
* Otherwise, returns the banner itself. * Otherwise, returns the banner itself.
* * @param forecast - The weather forecast.
* @param {WeatherForecast} forecast The weather forecast. * @returns The banner image attachment data.
* @returns {AttachmentData} The banner image attachment data.
*/ */
export const generateBanner = async( export const generateBanner = async(
forecast: WeatherForecast forecast: WeatherForecast,
): Promise<AttachmentData> => { ): Promise<AttachmentData> => {
const background = getBannerImage(forecast.region); const background = getBannerImage(forecast.region);
const overlayQuery = const overlayQuery
forecast.special?.name === "Blight Rain" = forecast.special?.name === "Blight Rain"
? "Blight Rain" ? "Blight Rain"
: forecast.precipitation?.name; : forecast.precipitation?.name;
if (!overlayQuery) { const overlayPath = getOverlayImage(overlayQuery ?? null);
return background;
}
const overlayPath = getOverlayImage(overlayQuery); const overlay = await overlayImages(background.filePath, overlayPath ?? "");
if (!overlayPath) {
return background;
}
const overlay = await overlayImages(background.filePath, overlayPath);
return overlay; return overlay;
}; };

View File

@ -1,16 +1,21 @@
import { join } from "path"; /**
* @copyright nhcarrigan
import { AttachmentData } from "../../interfaces/commands/AttachmentData"; * @license Naomi's Public License
import { RegionName } from "../../interfaces/weather/names/RegionName"; * @author Naomi Carrigan
*/
import { join } from "node:path";
import type { AttachmentData }
from "../../interfaces/commands/attachmentData.js";
import type { RegionName } from "../../interfaces/weather/names/regionName.js";
/** /**
* Selects one of the random banner images from the banner folder. * Selects one of the random banner images from the banner folder.
* * @param region - The name of the region.
* @param {RegionName} region The name of the region. * @returns The banner image attachment data.
* @returns {AttachmentData} The banner image attachment data.
*/ */
export const getBannerImage = (region: RegionName): AttachmentData => { export const getBannerImage = (region: RegionName): AttachmentData => {
const fileName = `${region}${Math.ceil(Math.random() * 3)}.png`; const fileName
= `${region}${String(Math.ceil(Math.random() * 3))}.png`;
const filePath = join(process.cwd(), "src", "assets", "banners", fileName); const filePath = join(process.cwd(), "src", "assets", "banners", fileName);
return { return {
attachmentString: `attachment://${fileName}`, attachmentString: `attachment://${fileName}`,

View File

@ -1,18 +1,23 @@
import { join } from "path"; /**
* @copyright nhcarrigan
import { PrecipitationName } from "../../interfaces/weather/names/PrecipitationName"; * @license Naomi's Public License
import { SpecialName } from "../../interfaces/weather/names/SpecialName"; * @author Naomi Carrigan
*/
import { join } from "node:path";
import type { PrecipitationName }
from "../../interfaces/weather/names/precipitationName.js";
import type { SpecialName }
from "../../interfaces/weather/names/specialName.js";
/** /**
* Checks if the current weather conditions have an overlay. * Checks if the current weather conditions have an overlay.
* * @param name - The name of the weather condition to look for.
* @param {SpecialName | PrecipitationName} name The name of the weather condition to look for. * @returns The file path to the overlay, or null if there is no overlay.
* @returns {string | null} The file path to the overlay, or null if there is no overlay.
*/ */
export const getOverlayImage = ( export const getOverlayImage = (
name: SpecialName | PrecipitationName name: SpecialName | PrecipitationName | null,
): string | null => { ): string | null => {
let fileName = null; let fileName: string | null = null;
switch (name) { switch (name) {
case "Blight Rain": case "Blight Rain":
fileName = "ROOTS-blightrain.png"; fileName = "ROOTS-blightrain.png";
@ -58,8 +63,10 @@ export const getOverlayImage = (
case "Thunderstorm": case "Thunderstorm":
fileName = "ROOTS-thunderstorm.png"; fileName = "ROOTS-thunderstorm.png";
break; break;
default:
fileName = null;
} }
if (!fileName) { if (fileName === null) {
return null; return null;
} }

View File

@ -1,13 +1,17 @@
import { join } from "path"; /**
* @copyright nhcarrigan
import { AttachmentData } from "../../interfaces/commands/AttachmentData"; * @license Naomi's Public License
import { Season } from "../../interfaces/weather/names/Season"; * @author Naomi Carrigan
*/
import { join } from "node:path";
import type { AttachmentData } from "../../interfaces/commands/attachmentData.js";
import type { Season }
from "../../interfaces/weather/names/season.js";
/** /**
* Module to generate the season icon attachment. * Module to generate the season icon attachment.
* * @param season - The season for which to get the icon.
* @param {Season} season The season. * @returns The icon attachment data.
* @returns {AttachmentData} The icon attachment data.
*/ */
export const getSeasonIcon = (season: Season): AttachmentData => { export const getSeasonIcon = (season: Season): AttachmentData => {
const fileName = `${season.toLowerCase()}.png`; const fileName = `${season.toLowerCase()}.png`;

View File

@ -1,23 +1,26 @@
import { join } from "path"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { join } from "node:path";
import sharp from "sharp"; import sharp from "sharp";
import type { AttachmentData }
import { AttachmentData } from "../../interfaces/commands/AttachmentData"; from "../../interfaces/commands/attachmentData.js";
/** /**
* Module to combine an overlay and a banner. * Module to combine an overlay and a banner.
* * @param banner - The file path for the banner.
* @param {string} banner The file path for the banner. * @param overlay - The file path for the overlay.
* @param {string} overlay The file path for the overlay. * @returns The attachment data for the new banner.
* @returns {AttachmentData} The attachment data for the new banner.
*/ */
export const overlayImages = async( export const overlayImages = async(
banner: string, banner: string,
overlay: string overlay: string,
): Promise<AttachmentData> => { ): Promise<AttachmentData> => {
await sharp(banner) await sharp(banner).
.composite([{ input: overlay, gravity: "center" }]) composite([ { gravity: "center", input: overlay } ]).
.toFile(join(process.cwd(), "src", "assets", "overlay.png")); toFile(join(process.cwd(), "src", "assets", "overlay.png"));
return { return {
attachmentString: `attachment://overlay.png`, attachmentString: `attachment://overlay.png`,
filePath: join(process.cwd(), "src", "assets", "overlay.png"), filePath: join(process.cwd(), "src", "assets", "overlay.png"),

View File

@ -1,42 +1,47 @@
import { Client } from "discord.js"; /**
* @copyright nhcarrigan
import { WeatherCache } from "../interfaces/WeatherCache"; * @license Naomi's Public License
import { logHandler } from "../utils/logHandler"; * @author Naomi Carrigan
*/
import { ChannelType, type Client } from "discord.js";
import { logHandler } from "../utils/logHandler.js";
import type { WeatherCache } from "../interfaces/weatherCache.js";
/** /**
* Module to fetch the configured channels for sending weather forecasts. * Module to fetch the configured channels for sending weather forecasts.
* * @param bot - The bot's Discord instance.
* @param {Client} BOT The bot's Discord instance. * @returns The configured channels for sending weather forecasts.
* @returns {WeatherCache["channels"]} The configured channels for sending weather forecasts.
*/ */
export const loadChannels = async( export const loadChannels = async(
BOT: Client bot: Client,
): Promise<WeatherCache["channels"]> => { ): Promise<WeatherCache["channels"]> => {
try { try {
const guildId = process.env.HOME_GUILD_ID as string; const guildId = process.env.HOME_GUILD_ID ?? "";
const rudaniaId = process.env.RUDANIA_ID as string; const rudaniaId = process.env.RUDANIA_ID ?? "";
const inarikoId = process.env.INARIKO_ID as string; const inarikoId = process.env.INARIKO_ID ?? "";
const vhintlId = process.env.VHINTL_ID as string; const vhintlId = process.env.VHINTL_ID ?? "";
const guild = await BOT.guilds.fetch(guildId); const guild = await bot.guilds.fetch(guildId);
const Rudania = await guild.channels.fetch(rudaniaId); const Rudania = await guild.channels.fetch(rudaniaId);
const Inariko = await guild.channels.fetch(inarikoId); const Inariko = await guild.channels.fetch(inarikoId);
const Vhintl = await guild.channels.fetch(vhintlId); const Vhintl = await guild.channels.fetch(vhintlId);
if ( if (
!guild || !Rudania
!Rudania || || !Inariko
!Inariko || || !Vhintl
!Vhintl || || !("send" in Rudania && "send" in Inariko && "send" in Vhintl)
!("send" in Rudania && "send" in Inariko && "send" in Vhintl) || Rudania.type !== ChannelType.GuildText
|| Inariko.type !== ChannelType.GuildText
|| Vhintl.type !== ChannelType.GuildText
) { ) {
logHandler.log("error", "Cannot locate the forecast channels!"); logHandler.log("error", "Cannot locate the forecast channels!");
process.exit(1); process.exit(1);
} }
return { Rudania, Inariko, Vhintl }; return { Inariko, Rudania, Vhintl };
} catch (err) { } catch (error) {
logHandler.log("error", err); logHandler.log("error", error);
process.exit(1); process.exit(1);
} }
}; };

View File

@ -1,35 +1,38 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { errorHandler } from "../utils/errorHandler.js";
import { WeatherCache } from "../interfaces/WeatherCache"; import { logHandler } from "../utils/logHandler.js";
import { errorHandler } from "../utils/errorHandler"; import { generateWeatherEmbed } from "./generateWeatherEmbed.js";
import { logHandler } from "../utils/logHandler"; import { getWeatherForecast } from "./getWeatherForecast.js";
import type { WeatherCache } from "../interfaces/weatherCache.js";
import { generateWeatherEmbed } from "./generateWeatherEmbed";
import { getWeatherForecast } from "./getWeatherForecast";
/** /**
* Schedules a CRON job for sending weather forecasts to the appropriate channels. * Schedules a CRON job for sending weather forecasts to the appropriate channels.
* * @param cache - The weather cache.
* @param {WeatherCache} CACHE The weather cache.
*/ */
export const scheduleForecasts = (CACHE: WeatherCache) => { export const scheduleForecasts = (cache: WeatherCache): void => {
logHandler.log("info", "Scheduling weather forecasts..."); logHandler.log("info", "Scheduling weather forecasts...");
// Run daily at 5AM PST to get 8AM EST. // Run daily at 5AM PST to get 8AM EST.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
scheduleJob("0 0 5 * * *", async() => { scheduleJob("0 0 5 * * *", async() => {
try { try {
CACHE.Rudania = getWeatherForecast("Rudania"); cache.Rudania = getWeatherForecast("Rudania");
CACHE.Inariko = getWeatherForecast("Inariko"); cache.Inariko = getWeatherForecast("Inariko");
CACHE.Vhintl = getWeatherForecast("Vhintl"); cache.Vhintl = getWeatherForecast("Vhintl");
const rudania = await generateWeatherEmbed(CACHE.Rudania); const rudania = await generateWeatherEmbed(cache.Rudania);
await CACHE.channels.Rudania.send(rudania); await cache.channels.Rudania.send(rudania);
const inariko = await generateWeatherEmbed(CACHE.Inariko); const inariko = await generateWeatherEmbed(cache.Inariko);
await CACHE.channels.Inariko.send(inariko); await cache.channels.Inariko.send(inariko);
const vhintl = await generateWeatherEmbed(CACHE.Vhintl); const vhintl = await generateWeatherEmbed(cache.Vhintl);
await CACHE.channels.Vhintl.send(vhintl); await cache.channels.Vhintl.send(vhintl);
} catch (err) { } catch (error) {
const errorId = await errorHandler(err, "scheduled forecast"); const errorId = await errorHandler(error, "scheduled forecast");
await CACHE.channels.Rudania.send(errorId); await cache.channels.Rudania.send(errorId);
} }
}); });
}; };

View File

@ -1,32 +1,40 @@
import { precipitations } from "../../data/weather/precipitations"; /**
import { TemperatureName } from "../../interfaces/weather/names/TemperatureName"; * @copyright nhcarrigan
import { WindName } from "../../interfaces/weather/names/WindName"; * @license Naomi's Public License
import { Precipitation } from "../../interfaces/weather/Precipitation"; * @author Naomi Carrigan
import { RegionRestriction } from "../../interfaces/weather/regions/RegionRestriction"; */
import { getRandomValue } from "../getRandomValue"; import { precipitations } from "../../data/weather/precipitations.js";
import { getRandomValue } from "../getRandomValue.js";
import type { TemperatureName }
from "../../interfaces/weather/names/temperatureName.js";
import type { WindName } from "../../interfaces/weather/names/windName.js";
import type { Precipitation }
from "../../interfaces/weather/precipitation.js";
import type { RegionRestriction } from "../../interfaces/weather/regions/regionRestriction.js";
/** /**
* * Get a precipitation forecast for the region and season.
* @param {RegionRestriction} options The allowed weather for the region + season. * @param options - The allowed weather for the region + season.
* @param {TemperatureName} temp The current temperature. * @param temperature - The current temperature.
* @param {WindName} wind The current wind. * @param wind - The current wind.
* @returns {Precipitation | null} The precipitation, or null if there are no valid forecasts. * @returns The precipitation, or null if there are no valid forecasts.
*/ */
export const getPrecipitation = ( export const getPrecipitation = (
options: RegionRestriction, options: RegionRestriction,
temp: TemperatureName, temperature: TemperatureName,
wind: WindName wind: WindName,
): Precipitation | null => { ): Precipitation | null => {
const restrictedPrecipitation = precipitations.filter((el) => const restrictedPrecipitation = precipitations.filter((element) => {
options.precipitation.includes(el.name) return options.precipitation.includes(element.name);
); });
const validPrecipitations = restrictedPrecipitation.filter( const validPrecipitations = restrictedPrecipitation.filter(
(el) => (element) => {
(el.temps.includes(temp) || el.temps === "any") && return (element.temps.includes(temperature) || element.temps === "any")
(el.winds.includes(wind) || el.winds === "any") && (element.winds.includes(wind) || element.winds === "any");
},
); );
if (!validPrecipitations.length) { if (validPrecipitations.length === 0) {
return null; return null;
} }

View File

@ -1,21 +1,25 @@
import { inarikoSeasons } from "../../data/weather/regions/inarikoSeasons"; /**
import { rudaniaSeasons } from "../../data/weather/regions/rudaniaSeasons"; * @copyright nhcarrigan
import { vhintlSeasons } from "../../data/weather/regions/vhintlSeasons"; * @license Naomi's Public License
import { RegionName } from "../../interfaces/weather/names/RegionName"; * @author Naomi Carrigan
import { Season } from "../../interfaces/weather/names/Season"; */
import { RegionRestriction } from "../../interfaces/weather/regions/RegionRestriction"; import { inarikoSeasons } from "../../data/weather/regions/inarikoSeasons.js";
import { rudaniaSeasons } from "../../data/weather/regions/rudaniaSeasons.js";
import { vhintlSeasons } from "../../data/weather/regions/vhintlSeasons.js";
import type { RegionName } from "../../interfaces/weather/names/regionName.js";
import type { Season } from "../../interfaces/weather/names/season.js";
import type { RegionRestriction } from "../../interfaces/weather/regions/regionRestriction.js";
/** /**
* Module to get the allowed weather for a region based on the season. * Module to get the allowed weather for a region based on the season.
* Will throw an error if the data is not found. * Will throw an error if the data is not found.
* * @param region - The name of the region.
* @param {RegionName} region The name of the region. * @param season - The season the region is in.
* @param {Season} season The season. * @returns The allowed weather for the region.
* @returns {RegionRestriction} The allowed weather for the region.
*/ */
export const getRegionRestrictions = ( export const getRegionRestrictions = (
region: RegionName, region: RegionName,
season: Season season: Season,
): RegionRestriction | null => { ): RegionRestriction | null => {
let restrictions = null; let restrictions = null;
switch (region) { switch (region) {
@ -32,7 +36,13 @@ export const getRegionRestrictions = (
return null; return null;
} }
const seasonalRestriction = restrictions.find((el) => el.season === season); if (restrictions === null) {
return null;
}
const seasonalRestriction = restrictions.find((element) => {
return element.season === season;
});
if (!seasonalRestriction) { if (!seasonalRestriction) {
return null; return null;

View File

@ -1,4 +1,9 @@
import { Season } from "../../interfaces/weather/names/Season"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Season } from "../../interfaces/weather/names/season.js";
/** /**
* Module to get the season based on today's date. * Module to get the season based on today's date.
@ -6,21 +11,20 @@ import { Season } from "../../interfaces/weather/names/Season";
* Spring: March 21 - June 20. * Spring: March 21 - June 20.
* Summer: June 21st - September 22. * Summer: June 21st - September 22.
* Fall: September 23 - December 21. * Fall: September 23 - December 21.
* * @returns The season name.
* @returns {Season} The season name.
*/ */
export const getSeason = (): Season => { export const getSeason = (): Season => {
const date = new Date(); const date = new Date();
const month = date.getMonth(); const month = date.getMonth();
const day = date.getDate(); const day = date.getDate();
if (month < 2 || (month === 2 && day < 21) || (month === 11 && day > 20)) { if (month < 2 || month === 2 && day < 21 || month === 11 && day > 20) {
return "Winter"; return "Winter";
} }
if (month < 5 || (month === 5 && day < 21)) { if (month < 5 || month === 5 && day < 21) {
return "Spring"; return "Spring";
} }
if (month < 8 || (month === 8 && day < 23)) { if (month < 8 || month === 8 && day < 23) {
return "Summer"; return "Summer";
} }
return "Fall"; return "Fall";

View File

@ -1,45 +1,51 @@
import { specials } from "../../data/weather/specials"; /**
import { PrecipitationName } from "../../interfaces/weather/names/PrecipitationName"; * @copyright nhcarrigan
import { TemperatureName } from "../../interfaces/weather/names/TemperatureName"; * @license Naomi's Public License
import { WindName } from "../../interfaces/weather/names/WindName"; * @author Naomi Carrigan
import { RegionRestriction } from "../../interfaces/weather/regions/RegionRestriction"; */
import { Special } from "../../interfaces/weather/Special"; import { specials } from "../../data/weather/specials.js";
import { getRandomValue } from "../getRandomValue"; import { getRandomValue } from "../getRandomValue.js";
import type { PrecipitationName } from "../../interfaces/weather/names/precipitationName.js";
import type { TemperatureName } from "../../interfaces/weather/names/temperatureName.js";
import type { WindName } from "../../interfaces/weather/names/windName.js";
import type { RegionRestriction } from "../../interfaces/weather/regions/regionRestriction.js";
import type { Special } from "../../interfaces/weather/special.js";
/** /**
* Module to get a special weather event for a region. Checks if the temp, wind, and precipitation * Module to get a special weather event for a region. Checks if the temp, wind, and precipitation
* allow for a special event. If so, has a 30% chance to trigger one. * allow for a special event. If so, has a 30% chance to trigger one.
* * @param options - The allowed weather for the region + season.
* @param {RegionRestriction} options The allowed weather for the region + season. * @param temperature - The current temperature.
* @param {TemperatureName} temp The current temperature. * @param wind - The current wind.
* @param {WindName} wind The current wind. * @param precipitation - The current precipitation.
* @param {PrecipitationName | undefined} precipitation The current precipitation. * @returns The special event, or null if there are no valid forecasts.
* @returns {Special | null} The special event, or null if there are no valid forecasts.
*/ */
export const getSpecial = ( export const getSpecial = (
options: RegionRestriction, options: RegionRestriction,
temp: TemperatureName, temperature: TemperatureName,
wind: WindName, wind: WindName,
precipitation: PrecipitationName | undefined precipitation: PrecipitationName | undefined,
): Special | null => { ): Special | null => {
const restrictedSpecial = specials.filter((el) => const restrictedSpecial = specials.filter((element) => {
options.special.includes(el.name) return options.special.includes(element.name);
); });
const validSpecials = precipitation const validSpecials = precipitation !== undefined
? restrictedSpecial.filter( ? restrictedSpecial.filter(
(el) => (element) => {
(el.temps.includes(temp) || el.temps === "any") && return (element.temps.includes(temperature) || element.temps === "any")
(el.winds.includes(wind) || el.winds === "any") && && (element.winds.includes(wind) || element.winds === "any")
(el.precipitations.includes(precipitation) || && (element.precipitations.includes(precipitation)
el.precipitations === "any") || element.precipitations === "any");
},
) )
: restrictedSpecial.filter( : restrictedSpecial.filter(
(el) => (element) => {
(el.temps.includes(temp) || el.temps === "any") && return (element.temps.includes(temperature) || element.temps === "any")
(el.winds.includes(wind) || el.winds === "any") && (element.winds.includes(wind) || element.winds === "any");
},
); );
if (!validSpecials.length) { if (validSpecials.length === 0) {
return null; return null;
} }

View File

@ -1,47 +1,48 @@
import { captureException } from "@sentry/node"; /**
import { MessageEmbed, WebhookClient } from "discord.js"; * @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { EmbedBuilder, WebhookClient } from "discord.js";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { logHandler } from "./logHandler.js";
import { logHandler } from "./logHandler";
/** /**
* Formats an error into an embed and sends it to the developer debug webhook. * Formats an error into an embed and sends it to the developer debug webhook.
* * @param error - The error instance from Node.
* @param {Error} err The error. * @param context - A description of where the error occurred.
* @param {string} context A description of where the error occurred. * @returns The UUID tied to the error.
* @returns {string} The UUID tied to the error.
*/ */
export const errorHandler = async( export const errorHandler = async(
err: unknown, error: unknown,
context: string context: string,
): Promise<string> => { ): Promise<string> => {
const error = err as Error; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const typedError = error as Error;
logHandler.log("error", `There was an error in the ${context}:`); logHandler.log("error", `There was an error in the ${context}:`);
logHandler.log( logHandler.log(
"error", "error",
JSON.stringify( JSON.stringify(
{ errorMessage: error.message, errorStack: error.stack }, { errorMessage: typedError.message, errorStack: typedError.stack },
null, null,
2 2,
) ),
); );
captureException(error);
const errorId = v4(); const errorId = v4();
const errorEmbed = new MessageEmbed(); const errorEmbed = new EmbedBuilder();
errorEmbed.setTitle(`RuuBot had a ${context} error!`); errorEmbed.setTitle(`RuuBot had a ${context} error!`);
errorEmbed.setColor("DARK_RED"); errorEmbed.setColor(0xFF_00_00);
errorEmbed.setDescription(error.message.substring(0, 4000)); errorEmbed.setDescription(typedError.message.slice(0, 4000));
errorEmbed.addField( errorEmbed.addFields([
"Stack Trace:", { name: "Stack Trace",
`\`\`\`${error.stack?.slice(0, 1000)}\`\`\`` value: typedError.stack?.slice(0, 1000) ?? "No stack trace available." },
); { name: "Error ID", value: errorId },
errorEmbed.addField("Error ID:", errorId); ]);
errorEmbed.setTimestamp(); errorEmbed.setTimestamp();
const webhook = new WebhookClient({ url: process.env.DEBUG_HOOK as string }); const webhook = new WebhookClient({ url: process.env.DEBUG_HOOK ?? "" });
await webhook.send({ embeds: [ errorEmbed ] }); await webhook.send({ embeds: [ errorEmbed ] });

View File

@ -1,3 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { createLogger, format, transports, config } from "winston"; import { createLogger, format, transports, config } from "winston";
const { combine, timestamp, colorize, printf } = format; const { combine, timestamp, colorize, printf } = format;
@ -5,20 +10,21 @@ const { combine, timestamp, colorize, printf } = format;
/** /**
* Standard log handler, using winston to wrap and format * Standard log handler, using winston to wrap and format
* messages. Call with `logHandler.log(level, message)`. * messages. Call with `logHandler.log(level, message)`.
*
* @param {string} level - The log level to use. * @param {string} level - The log level to use.
* @param {string} message - The message to log. * @param {string} message - The message to log.
*/ */
export const logHandler = createLogger({ export const logHandler = createLogger({
levels: config.npm.levels, exitOnError: false,
level: "silly",
transports: [new transports.Console()],
format: combine( format: combine(
timestamp({ timestamp({
format: "YYYY-MM-DD HH:mm:ss", format: "YYYY-MM-DD HH:mm:ss",
}), }),
colorize(), colorize(),
printf((info) => `${info.level}: ${[info.timestamp]}: ${info.message}`) printf((info) => {
return `${info.level}: ${info.timestamp}: ${info.message}`;
}),
), ),
exitOnError: false, level: "silly",
levels: config.npm.levels,
transports: [ new transports.Console() ],
}); });

132
test/regions.spec.ts Normal file
View File

@ -0,0 +1,132 @@
import { describe, it, expect } from "vitest";
import { inarikoSeasons } from "../src/data/weather/regions/inarikoSeasons.ts";
import { rudaniaSeasons } from "../src/data/weather/regions/rudaniaSeasons.ts";
import { vhintlSeasons } from "../src/data/weather/regions/vhintlSeasons.ts";
describe("region Tests", () => {
describe("inariko", () => {
it("seasons should be unique", () => {
const seasons = new Set(inarikoSeasons.map((element) => {
return element.season;
}));
expect(seasons.size, "seasons are not unique!").toBe(
inarikoSeasons.length,
);
});
for (const season of inarikoSeasons) {
it(`${season.season} should have unique temps`, () => {
const temps = new Set(season.temps);
expect(temps.size, `${season.season} temps are not unique!`).toBe(
season.temps.length,
);
});
it(`${season.season} should have unique winds`, () => {
const winds = new Set(season.wind);
expect(winds.size, `${season.season} winds are not unique!`).toBe(
season.wind.length,
);
});
it(`${season.season} should have unique precipitation`, () => {
const precipitation = new Set(season.precipitation);
expect(
precipitation.size,
`${season.season} precipitation is not unique!`,
).toBe(season.precipitation.length);
});
it(`${season.season} should have unique specials`, () => {
const specials = new Set(season.special);
expect(specials.size, `${season.season} specials are not unique!`).toBe(
season.special.length,
);
});
}
});
describe("rudania", () => {
it("seasons should be unique", () => {
const seasons = new Set(rudaniaSeasons.map((element) => {
return element.season;
}));
expect(seasons.size, "seasons are not unique!").toBe(
rudaniaSeasons.length,
);
});
for (const season of rudaniaSeasons) {
it(`${season.season} should have unique temps`, () => {
const temps = new Set(season.temps);
expect(temps.size, `${season.season} temps are not unique!`).toBe(
season.temps.length,
);
});
it(`${season.season} should have unique winds`, () => {
const winds = new Set(season.wind);
expect(winds.size, `${season.season} winds are not unique!`).toBe(
season.wind.length,
);
});
it(`${season.season} should have unique precipitation`, () => {
const precipitation = new Set(season.precipitation);
expect(
precipitation.size,
`${season.season} precipitation is not unique!`,
).toBe(season.precipitation.length);
});
it(`${season.season} should have unique specials`, () => {
const specials = new Set(season.special);
expect(specials.size, `${season.season} specials are not unique!`).toBe(
season.special.length,
);
});
}
});
describe("vhintl", () => {
it("seasons should be unique", () => {
const seasons = new Set(vhintlSeasons.map((element) => {
return element.season;
}));
expect(seasons.size, "seasons are not unique!").toBe(
vhintlSeasons.length,
);
});
for (const season of vhintlSeasons) {
it(`${season.season} should have unique temps`, () => {
const temps = new Set(season.temps);
expect(temps.size, `${season.season} temps are not unique!`).toBe(
season.temps.length,
);
});
it(`${season.season} should have unique winds`, () => {
const winds = new Set(season.wind);
expect(winds.size, `${season.season} winds are not unique!`).toBe(
season.wind.length,
);
});
it(`${season.season} should have unique precipitation`, () => {
const precipitation = new Set(season.precipitation);
expect(
precipitation.size,
`${season.season} precipitation is not unique!`,
).toBe(season.precipitation.length);
});
it(`${season.season} should have unique specials`, () => {
const specials = new Set(season.special);
expect(specials.size, `${season.season} specials are not unique!`).toBe(
season.special.length,
);
});
}
});
});

102
test/weather.spec.ts Normal file
View File

@ -0,0 +1,102 @@
import { describe, it, expect } from "vitest";
import { precipitations } from "../src/data/weather/precipitations.ts";
import { specials } from "../src/data/weather/specials.ts";
import { temperatures } from "../src/data/weather/temperatures.ts";
import { winds } from "../src/data/weather/winds.ts";
describe("weather Tests", () => {
describe("temperature", () => {
const temps = new Set(temperatures.map((element) => {
return element.name;
}));
it("temperatures should be unique", () => {
expect(temps.size, "temperatures are not unique!").toBe(
temperatures.length,
);
});
});
describe("winds", () => {
const wind = new Set(winds.map((element) => {
return element.name;
}));
it("winds should be unique", () => {
expect(wind.size, "winds are not unique!").toBe(winds.length);
});
});
describe("precipitations", () => {
const precip = new Set(precipitations.map((element) => {
return element.name;
}));
it("precipitations should be unique", () => {
expect(precip.size, "precipitations are not unique!").toBe(
precipitations.length,
);
});
for (const precipitation of precipitations) {
it(`${precipitation.name} should have unique temps`, () => {
if (precipitation.temps === "any") {
return;
}
const temps = new Set(precipitation.temps);
expect(temps.size, `${precipitation.name} temps are not unique!`).toBe(
precipitation.temps.length,
);
});
it(`${precipitation.name} should have unique winds`, () => {
if (precipitation.winds === "any") {
return;
}
const winds = new Set(precipitation.winds);
expect(winds.size, `${precipitation.name} winds are not unique!`).toBe(
precipitation.winds.length,
);
});
}
});
describe("specials", () => {
const spec = new Set(specials.map((element) => {
return element.name;
}));
it("specials should be unique", () => {
expect(spec.size, "specials are not unique!").toBe(specials.length);
});
for (const special of specials) {
it(`${special.name} should have unique temps`, () => {
if (special.temps === "any") {
return;
}
const temps = new Set(special.temps);
expect(temps.size, `${special.name} temps are not unique!`).toBe(
special.temps.length,
);
});
it(`${special.name} should have unique winds`, () => {
if (special.winds === "any") {
return;
}
const winds = new Set(special.winds);
expect(special.winds, "Special winds are not unique!").toHaveLength(
winds.size,
);
});
it(`${special.name} should have unique precipitation`, () => {
if (special.precipitations === "any") {
return;
}
const precip = new Set(special.precipitations);
expect(
precip.size,
`${special.name} precipitation is not unique!`,
).toBe(special.precipitations.length);
});
}
});
});

View File

@ -1,154 +0,0 @@
import { assert } from "chai";
import { inarikoSeasons } from "../src/data/weather/regions/inarikoSeasons";
import { rudaniaSeasons } from "../src/data/weather/regions/rudaniaSeasons";
import { vhintlSeasons } from "../src/data/weather/regions/vhintlSeasons";
suite("Region Tests", () => {
suite("Inariko", () => {
test("Seasons should be unique", () => {
const seasons = new Set(inarikoSeasons.map((el) => el.season));
assert.equal(
seasons.size,
inarikoSeasons.length,
"seasons are not unique!"
);
});
for (const season of inarikoSeasons) {
test(`${season.season} should have unique temps`, () => {
const temps = new Set(season.temps);
assert.equal(
temps.size,
season.temps.length,
`${season.season} temps are not unique!`
);
});
test(`${season.season} should have unique winds`, () => {
const winds = new Set(season.wind);
assert.equal(
winds.size,
season.wind.length,
`${season.season} winds are not unique!`
);
});
test(`${season.season} should have unique precipitation`, () => {
const precipitation = new Set(season.precipitation);
assert.equal(
precipitation.size,
season.precipitation.length,
`${season.season} precipitation is not unique!`
);
});
test(`${season.season} should have unique specials`, () => {
const specials = new Set(season.special);
assert.equal(
specials.size,
season.special.length,
`${season.season} specials are not unique!`
);
});
}
});
suite("Rudania", () => {
test("Seasons should be unique", () => {
const seasons = new Set(rudaniaSeasons.map((el) => el.season));
assert.equal(
seasons.size,
rudaniaSeasons.length,
"seasons are not unique!"
);
});
for (const season of rudaniaSeasons) {
test(`${season.season} should have unique temps`, () => {
const temps = new Set(season.temps);
assert.equal(
temps.size,
season.temps.length,
`${season.season} temps are not unique!`
);
});
test(`${season.season} should have unique winds`, () => {
const winds = new Set(season.wind);
assert.equal(
winds.size,
season.wind.length,
`${season.season} winds are not unique!`
);
});
test(`${season.season} should have unique precipitation`, () => {
const precipitation = new Set(season.precipitation);
assert.equal(
precipitation.size,
season.precipitation.length,
`${season.season} precipitation is not unique!`
);
});
test(`${season.season} should have unique specials`, () => {
const specials = new Set(season.special);
assert.equal(
specials.size,
season.special.length,
`${season.season} specials are not unique!`
);
});
}
});
suite("Vhintl", () => {
test("Seasons should be unique", () => {
const seasons = new Set(vhintlSeasons.map((el) => el.season));
assert.equal(
seasons.size,
vhintlSeasons.length,
"seasons are not unique!"
);
});
for (const season of vhintlSeasons) {
test(`${season.season} should have unique temps`, () => {
const temps = new Set(season.temps);
assert.equal(
temps.size,
season.temps.length,
`${season.season} temps are not unique!`
);
});
test(`${season.season} should have unique winds`, () => {
const winds = new Set(season.wind);
assert.equal(
winds.size,
season.wind.length,
`${season.season} winds are not unique!`
);
});
test(`${season.season} should have unique precipitation`, () => {
const precipitation = new Set(season.precipitation);
assert.equal(
precipitation.size,
season.precipitation.length,
`${season.season} precipitation is not unique!`
);
});
test(`${season.season} should have unique specials`, () => {
const specials = new Set(season.special);
assert.equal(
specials.size,
season.special.length,
`${season.season} specials are not unique!`
);
});
}
});
});

View File

@ -1,108 +0,0 @@
import { assert } from "chai";
import { precipitations } from "../src/data/weather/precipitations";
import { specials } from "../src/data/weather/specials";
import { temperatures } from "../src/data/weather/temperatures";
import { winds } from "../src/data/weather/winds";
suite("Weather Tests", () => {
suite("Temperature", () => {
const temps = new Set(temperatures.map((el) => el.name));
test("Temperatures should be unique", () => {
assert.equal(
temps.size,
temperatures.length,
"temperatures are not unique!"
);
});
});
suite("Winds", () => {
const wind = new Set(winds.map((el) => el.name));
test("Winds should be unique", () => {
assert.equal(wind.size, winds.length, "winds are not unique!");
});
});
suite("Precipitations", () => {
const precip = new Set(precipitations.map((el) => el.name));
test("Precipitations should be unique", () => {
assert.equal(
precip.size,
precipitations.length,
"precipitations are not unique!"
);
});
for (const precipitation of precipitations) {
test(`${precipitation.name} should have unique temps`, () => {
if (precipitation.temps === "any") {
return;
}
const temps = new Set(precipitation.temps);
assert.equal(
temps.size,
precipitation.temps.length,
`${precipitation.name} temps are not unique!`
);
});
test(`${precipitation.name} should have unique winds`, () => {
if (precipitation.winds === "any") {
return;
}
const winds = new Set(precipitation.winds);
assert.equal(
winds.size,
precipitation.winds.length,
`${precipitation.name} winds are not unique!`
);
});
}
});
suite("Specials", () => {
const spec = new Set(specials.map((el) => el.name));
test("Specials should be unique", () => {
assert.equal(spec.size, specials.length, "specials are not unique!");
});
for (const special of specials) {
test(`${special.name} should have unique temps`, () => {
if (special.temps === "any") {
return;
}
const temps = new Set(special.temps);
assert.equal(
temps.size,
special.temps.length,
`${special.name} temps are not unique!`
);
});
test(`${special.name} should have unique winds`, () => {
if (special.winds === "any") {
return;
}
const winds = new Set(special.winds);
assert.equal(
winds.size,
special.winds.length,
`${special.name} winds are not unique!`
);
});
test(`${special.name} should have unique precipitation`, () => {
if (special.precipitations === "any") {
return;
}
const precip = new Set(special.precipitations);
assert.equal(
precip.size,
special.precipitations.length,
`${special.name} precipitation is not unique!`
);
});
}
});
});

View File

@ -1,14 +1,9 @@
{ {
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": { "compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"rootDir": "./src", "rootDir": "./src",
"outDir": "./prod", "outDir": "./prod",
"strict": true, "exactOptionalPropertyTypes": false
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}, },
"exclude": ["tests/**/*.spec.ts"] "exclude": ["./test", "vitest.config.ts"]
} }

15
vitest.config.ts Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "istanbul",
reporter: ["text", "html"],
all: true,
allowExternal: true,
thresholds: {
lines: 0,
},
},
},
});