First commit of group-ironmen-master directory.

This commit is contained in:
2025-10-27 08:25:16 +08:00
commit a8467389ef
26390 changed files with 35396 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
const fs = require('fs');
const glob = require('glob');
const files = [
...glob.sync('public/*.js'),
...glob.sync('public/*.map'),
...glob.sync('public/*.html')
];
for (const file of files) {
fs.rmSync(file);
}

View File

@@ -0,0 +1,12 @@
#!/bin/sh
API_FILE_PATH=./src/data/api.js
echo "[entrypoint] Replacing the API URL with the given HOST_URL"
sed -i -e "s#\"/api\"#\"$HOST_URL/api\"#g" $API_FILE_PATH
echo "[entrypoint] Running bundle"
npm run bundle
echo "[entrypoint] Running serve"
exec "$@"

View File

@@ -0,0 +1,86 @@
const fs = require('fs');
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question('What do you want to name the component? ', (componentName) => {
const isValidComponentName = (
componentName && componentName.includes('-') &&
componentName.toLowerCase() === componentName && !(/\s/g.test(componentName))
);
if (!isValidComponentName) {
console.log('Component name must be in the format "app-component". All lowercase and minimum 2 words separated by hyphens');
readline.close();
return;
}
componentName = componentName.trim();
const path = `./src/${componentName}`;
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
const pascalCase = componentName.split('-').map((s) => {
return capitalizeFirstLetter(s);
}).join('');
const tsPath = `${path}/${componentName}.js`;
if (!fs.existsSync(tsPath)) {
fs.writeFileSync(tsPath, `
import { BaseElement } from '../base-element/base-element';
export class ${pascalCase} extends BaseElement {
constructor() {
super();
}
/* eslint-disable no-unused-vars */
html() {
return \`{{${componentName}.html}}\`;
}
/* eslint-enable no-unused-vars */
connectedCallback() {
super.connectedCallback();
}
disconnectedCallback() {
super.disconnectedCallback();
}
}
customElements.define('${componentName}', ${pascalCase});
`.trim());
}
const cssPath = `${path}/${componentName}.css`;
if (!fs.existsSync(cssPath)) {
fs.writeFileSync(cssPath, '');
}
const htmlPath = `${path}/${componentName}.html`;
if (!fs.existsSync(htmlPath)) {
fs.writeFileSync(htmlPath, '');
}
const components = JSON.parse(fs.readFileSync('components.json', 'utf8'));
if (components.indexOf(componentName) === -1) {
components.push(componentName);
fs.writeFileSync('components.json', JSON.stringify(components));
}
let index = fs.readFileSync('src/index.js', 'utf8').split('\n');
const importString = `import "./${componentName}/${componentName}.js";`;
if (index.indexOf(importString) === -1) {
if (index[index.length - 1] === '') {
index.pop();
}
index.push(importString);
fs.writeFileSync('src/index.js', index.join('\n'));
}
readline.close();
});

View File

@@ -0,0 +1,206 @@
{
"0": "Animal Magnetism",
"1": "Another Slice of H.A.M.",
"3": "The Ascent of Arceuus",
"4": "Alfred Grimhand's Barcrawl",
"5": "Bear Your Soul",
"6": "Below Ice Mountain",
"7": "Between a Rock...",
"8": "Big Chompy Bird Hunting",
"9": "Biohazard",
"10": "Black Knights' Fortress",
"11": "Bone Voyage",
"12": "Cabin Fever",
"13": "Client of Kourend",
"14": "Clock Tower",
"15": "Cold War",
"16": "Contact!",
"17": "Cook's Assistant",
"18": "The Corsair Curse",
"19": "Creature of Fenkenstrain",
"20": "Curse of the Empty Lord",
"21": "Daddy's Home",
"22": "Darkness of Hallowvale",
"23": "Death Plateau",
"24": "Death to the Dorgeshuun",
"25": "Demon Slayer",
"26": "The Depths of Despair",
"27": "Desert Treasure I",
"28": "Devious Minds",
"29": "The Dig Site",
"30": "Doric's Quest",
"31": "Dragon Slayer I",
"32": "Dragon Slayer II",
"33": "Dream Mentor",
"34": "Druidic Ritual",
"35": "Dwarf Cannon",
"36": "Eadgar's Ruse",
"37": "Eagles' Peak",
"38": "Elemental Workshop I",
"39": "Elemental Workshop II",
"40": "Enakhra's Lament",
"41": "The Enchanted Key",
"42": "Enlightened Journey",
"43": "Enter the Abyss",
"44": "Ernest the Chicken",
"45": "The Eyes of Glouphrie",
"46": "Fairytale I - Growing Pains",
"47": "Fairytale II - Cure a Queen",
"48": "Family Crest",
"49": "Family Pest",
"50": "The Feud",
"51": "Fight Arena",
"52": "Fishing Contest",
"53": "Forgettable Tale...",
"54": "The Forsaken Tower",
"55": "The Fremennik Exiles",
"56": "The Fremennik Isles",
"57": "The Fremennik Trials",
"58": "Garden of Tranquillity",
"59": "The General's Shadow",
"60": "Gertrude's Cat",
"61": "Getting Ahead",
"62": "Ghosts Ahoy",
"63": "The Giant Dwarf",
"64": "Goblin Diplomacy",
"65": "The Golem",
"66": "The Grand Tree",
"67": "The Great Brain Robbery",
"68": "Grim Tales",
"69": "The Hand in the Sand",
"70": "Haunted Mine",
"71": "Hazeel Cult",
"72": "Heroes' Quest",
"73": "Holy Grail",
"74": "Horror from the Deep",
"75": "Icthlarin's Little Helper",
"76": "Imp Catcher",
"77": "In Aid of the Myreque",
"78": "In Search of Knowledge",
"79": "In Search of the Myreque",
"80": "Jungle Potion",
"81": "A Kingdom Divided",
"82": "King's Ransom",
"83": "The Knight's Sword",
"84": "Lair of Tarn Razorlor",
"85": "Legends' Quest",
"86": "Lost City",
"87": "The Lost Tribe",
"88": "Lunar Diplomacy",
"89": "Mage Arena I",
"90": "Mage Arena II",
"91": "Making Friends with My Arm",
"92": "Making History",
"93": "Merlin's Crystal",
"94": "Misthalin Mystery",
"95": "Monkey Madness I",
"96": "Monkey Madness II",
"97": "Monk's Friend",
"98": "Mountain Daughter",
"99": "Mourning's End Part I",
"100": "Mourning's End Part II",
"101": "Murder Mystery",
"102": "My Arm's Big Adventure",
"103": "Nature Spirit",
"104": "A Night at the Theatre",
"105": "Observatory Quest",
"106": "Olaf's Quest",
"107": "One Small Favour",
"108": "Pirate's Treasure",
"109": "Plague City",
"110": "A Porcine of Interest",
"111": "Priest in Peril",
"112": "Prince Ali Rescue",
"113": "The Queen of Thieves",
"114": "Rag and Bone Man I",
"115": "Rag and Bone Man II",
"116": "Ratcatchers",
"117": "Recipe for Disaster",
"118": "Recruitment Drive",
"119": "Regicide",
"120": "The Restless Ghost",
"121": "Romeo & Juliet",
"122": "Roving Elves",
"123": "Royal Trouble",
"124": "Rum Deal",
"125": "Rune Mysteries",
"126": "Scorpion Catcher",
"127": "Sea Slug",
"128": "Shades of Mort'ton",
"129": "Shadow of the Storm",
"130": "Sheep Herder",
"131": "Sheep Shearer",
"132": "Shield of Arrav",
"133": "Shilo Village",
"134": "Sins of the Father",
"135": "Skippy and the Mogres",
"136": "The Slug Menace",
"137": "Song of the Elves",
"138": "A Soul's Bane",
"139": "Spirits of the Elid",
"140": "Swan Song",
"141": "Tai Bwo Wannai Trio",
"142": "A Tail of Two Cats",
"143": "Tale of the Righteous",
"144": "A Taste of Hope",
"145": "Tears of Guthix",
"146": "Temple of Ikov",
"147": "Throne of Miscellania",
"148": "The Tourist Trap",
"149": "Tower of Life",
"150": "Tree Gnome Village",
"151": "Tribal Totem",
"152": "Troll Romance",
"153": "Troll Stronghold",
"154": "Underground Pass",
"155": "Vampyre Slayer",
"156": "Wanted!",
"157": "Watchtower",
"158": "Waterfall Quest",
"159": "What Lies Below",
"160": "Witch's House",
"161": "Witch's Potion",
"162": "X Marks the Spot",
"163": "Zogre Flesh Eaters",
"164": "The Frozen Door",
"165": "Land of the Goblins",
"166": "Hopespear's Will",
"167": "Temple of the Eye",
"168": "Beneath Cursed Sands",
"169": "Sleeping Giants",
"180": "The Garden of Death",
"2306": "Into the Tombs",
"2307": "Recipe for Disaster/Another Cook's Quest",
"2308": "Recipe for Disaster/Freeing the Mountain Dwarf",
"2309": "Recipe for Disaster/Freeing the Goblin generals",
"2310": "Recipe for Disaster/Freeing Pirate Pete",
"2311": "Recipe for Disaster/Freeing the Lumbridge Guide",
"2312": "Recipe for Disaster/Freeing Evil Dave",
"2313": "Recipe for Disaster/Freeing Skrach Uglogwee",
"2314": "Recipe for Disaster/Freeing Sir Amik Varze",
"2315": "Recipe for Disaster/Freeing King Awowogei",
"2316": "Recipe for Disaster/Defeating the Culinaromancer",
"2338": "Secrets of the North",
"2343": "Desert Treasure II - The Fallen Empire",
"3250": "His Faithful Servants",
"3425": "The Path of Glouphrie",
"3450": "Children of the Sun",
"3451": "Barbarian Training",
"3466": "Defender of Varrock",
"3467": "While Guthix Sleeps",
"3512": "Twilight's Promise",
"3513": "At First Light",
"3514": "Perilous Moons",
"3515": "The Ribbiting Tale of a Lily Pad Labour Dispute",
"3710": "The Heart of Darkness",
"3711": "Death on the Isle",
"3712": "Meat and Greet",
"3713": "Ethically Acquired Antiquities",
"3937": "The Curse of Arrav",
"5189": "The Final Dawn",
"5190": "Shadows of Custodia",
"5191": "Scrambled!",
"5192": "An Existential Crisis",
"5193": "Impending Chaos",
"5194": "Vale Totems (miniquest)"
}

View File

@@ -0,0 +1,79 @@
const jsdom = require("jsdom");
const { JSDOM } = require('jsdom');
const axios = require("axios");
const fs = require('fs');
const questsMapping = require('./quest-mapping.json');
const questNameToIdMap = new Map();
for (const [questId, questName] of Object.entries(questsMapping)) {
questNameToIdMap.set(questName, parseInt(questId));
}
function getQuestTableData(table) {
const rows = Array.from(table.querySelectorAll("tbody tr"));
const result = [];
const ths = Array.from(table.querySelectorAll('th'));
const headers = ths.map((th) => th.textContent.trim());
for (const row of rows) {
const tds = Array.from(row.querySelectorAll('td'));
if (tds.length === 0) continue;
const name = tds[headers.indexOf('Name')].textContent.trim();
const difficulty = tds[headers.indexOf('Difficulty')].textContent.trim();
const points = tds[headers.indexOf('')]?.textContent.trim() || 0;
result.push({
name,
difficulty,
points
});
}
return result;
}
async function run() {
const questsListHtml = await axios.get("https://oldschool.runescape.wiki/w/Quests/List");
const dom = new JSDOM(questsListHtml.data);
const questTables = Array.from(dom.window.document.querySelectorAll("table")).filter((table) => {
const ths = Array.from(table.querySelectorAll('th'));
if (ths.length === 0) return false;
const headerText = ths.map((th) => th.textContent.trim()).join('');
if (headerText.includes('NameDifficultyLengthSeriesRelease date')) return true;
return false;
});
const freeToPlayQuestTable = questTables[0];
const memberQuestTable = questTables[1];
const miniQuestTable = questTables[2];
const freeToPlayQuests = getQuestTableData(freeToPlayQuestTable);
freeToPlayQuests.forEach((quest) => quest.member = false);
const memberQuests = getQuestTableData(memberQuestTable);
memberQuests.forEach((quest) => quest.member = true);
const miniQuests = getQuestTableData(miniQuestTable);
miniQuests.forEach((quest) => {
quest.member = true
quest.miniquest = true;
});
const result = {};
for (const quest of [...freeToPlayQuests, ...memberQuests, ...miniQuests]) {
if (!questNameToIdMap.has(quest.name)) {
console.error(`quest mapping is missing quest ${quest.name} from the wiki`);
continue;
}
// The points come from the subquests, setting this to 0 so we don't count the points twice
if (quest.name === 'Recipe for Disaster') {
quest.points = 0;
}
result[questNameToIdMap.get(quest.name)] = quest;
}
fs.writeFileSync('./public/data/quest_data.json', JSON.stringify(result));
}
run();

View File

@@ -0,0 +1,83 @@
const express = require('express');
const winston = require('winston');
const expressWinston = require('express-winston');
const path = require('path');
const compression = require('compression');
const axios = require('axios');
const app = express();
const port = 4000;
const args = process.argv.map((arg) => arg.trim());
function getArgValue(arg) {
const i = args.indexOf(arg);
if (i === -1) return;
return args[i + 1];
}
const backend = getArgValue('--backend') === undefined ? process.env.HOST_URL : getArgValue('--backend');
app.use(expressWinston.logger({
transports: [
new winston.transports.Console()
],
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
meta: false,
msg: "HTTP {{req.method}} {{req.url}} {{res.statusCode}}",
expressFormat: false,
colorize: true,
metaField: null
}));
app.use(compression());
app.use(express.static('public'));
app.use(express.static('.'));
if (backend) {
console.log(`Backend for api calls: ${backend}`);
app.use(express.json());
app.use('/api*', (req, res, next) => {
const forwardUrl = backend + req.originalUrl;
console.log(`Calling backend ${forwardUrl}`);
const headers = Object.assign({}, req.headers);
delete headers.host;
delete headers.referer;
axios({
method: req.method,
url: forwardUrl,
responseType: 'stream',
headers,
data: req.body
}).then((response) => {
res.status(response.status);
res.set(response.headers);
response.data.pipe(res);
}).catch((error) => {
if (error.response) {
res.status(error.response.status);
res.set(error.response.headers);
error.response.data.pipe(res);
} else if (error.request) {
res.status(418).end();
} else {
console.error('Error', error.message);
res.status(418).end();
}
});
});
} else {
console.log("No backend supplied for api calls, not going to handle api requests");
}
app.get('*', function (request, response) {
if (request.path.includes('/map') && request.path.includes('.png')) {
response.sendStatus(404);
} else {
response.sendFile(path.resolve('public', 'index.html'));
}
});
const server = app.listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});