Initial commit

This commit is contained in:
Brian Beck 2024-10-15 18:48:11 -07:00
parent 22f31651d7
commit 46c43d9c82
56 changed files with 2451 additions and 0 deletions

133
.gitignore vendored Normal file
View file

@ -0,0 +1,133 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
.tshy

32
app/global.css Normal file
View file

@ -0,0 +1,32 @@
:root {
--system-ui: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Prevent font size inflation */
html {
-moz-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
min-height: 100vh;
background-color: #699697;
background-image: url("../public/noise.png"),
linear-gradient(to bottom, #5be9ee 0%, #1e4172 100%);
background-repeat: repeat, repeat;
}

19
app/layout.tsx Normal file
View file

@ -0,0 +1,19 @@
import { departureMono } from "../src/fonts";
import "./global.css";
export const metadata = {
title: "VL2 Forge",
description: "Create .vl2 files for Tribes 2",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={departureMono.variable}>
<body>{children}</body>
</html>
);
}

6
app/page.tsx Normal file
View file

@ -0,0 +1,6 @@
import React from "react";
import { Forge } from "../src/Forge";
export default function Page() {
return <Forge />;
}

0
docs/.nojekyll Normal file
View file

1
docs/404.html Normal file

File diff suppressed because one or more lines are too long

1
docs/404/index.html Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-7ba65e1336b92748.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View file

@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[699],{9089:function(a,t,n){n.d(t,{AMf:function(){return c}});var u=n(6231);function c(a){return(0,u.w_)({tag:"svg",attr:{viewBox:"0 0 448 512"},child:[{tag:"path",attr:{d:"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"},child:[]}]})(a)}}}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[409],{7589:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return n(3634)}])},3634:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}}),n(7043);let i=n(7437);n(2265);let o={fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},l={display:"inline-block"},r={display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},d={fontSize:14,fontWeight:400,lineHeight:"49px",margin:0};function s(){return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("title",{children:"404: This page could not be found."}),(0,i.jsx)("div",{style:o,children:(0,i.jsxs)("div",{children:[(0,i.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,i.jsx)("h1",{className:"next-error-h1",style:r,children:"404"}),(0,i.jsx)("div",{style:l,children:(0,i.jsx)("h2",{style:d,children:"This page could not be found."})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},function(e){e.O(0,[971,117,744],function(){return e(e.s=7589)}),_N_E=e.O()}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{4345:function(e,n,t){Promise.resolve().then(t.t.bind(t,2085,23)),Promise.resolve().then(t.t.bind(t,2528,23))},2528:function(){},2085:function(e){e.exports={style:{fontFamily:"'__departureMono_6dd175', '__departureMono_Fallback_6dd175'",fontWeight:400,fontStyle:"normal"},className:"__className_6dd175",variable:"__variable_6dd175"}}},function(e){e.O(0,[499,971,117,744],function(){return e(e.s=4345)}),_N_E=e.O()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{9783:function(e,n,t){Promise.resolve().then(t.t.bind(t,2846,23)),Promise.resolve().then(t.t.bind(t,9107,23)),Promise.resolve().then(t.t.bind(t,1060,23)),Promise.resolve().then(t.t.bind(t,4707,23)),Promise.resolve().then(t.t.bind(t,80,23)),Promise.resolve().then(t.t.bind(t,6423,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,117],function(){return n(4278),n(9783)}),_N_E=e.O()}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1597:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return u(8141)}])}},function(n){var _=function(_){return n(n.s=_)};n.O(0,[774,179],function(){return _(1597),_(7253)}),_N_E=n.O()}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(8529)}])}},function(n){n.O(0,[888,774,179],function(){return n(n.s=1981)}),_N_E=n.O()}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function d(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={id:e,loaded:!1,exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,d),r=!1}finally{r&&delete l[e]}return n.loaded=!0,n.exports}d.m=a,e=[],d.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(d.O).every(function(e){return d.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},d.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return d.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},d.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);d.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},d.d(o,u),o},d.d=function(e,t){for(var n in t)d.o(t,n)&&!d.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},d.f={},d.e=function(e){return Promise.all(Object.keys(d.f).reduce(function(t,n){return d.f[n](e,t),t},[]))},d.u=function(e){},d.miniCssF=function(e){},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",d.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,d.nc&&i.setAttribute("nonce",d.nc),i.setAttribute("data-webpack",o+n),i.src=d.tu(e)),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)},d.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},d.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},d.tu=function(e){return d.tt().createScriptURL(e)},d.p="/vl2-forge/_next/",i={272:0,499:0,359:0},d.f.j=function(e,t){var n=d.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(/^(272|359|499)$/.test(e))i[e]=0;else{var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=d.p+d.u(e),u=Error();d.l(o,function(t){if(d.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}}},d.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)d.o(u,n)&&(d.m[n]=u[n]);if(c)var a=c(d)}for(e&&e(t);f<o.length;f++)r=o[f],d.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return d.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();

View file

@ -0,0 +1 @@
.Forge_Forge__dDZFe{flex:1 0 auto;display:grid;grid-template-columns:auto 1fr;grid-template-rows:1fr;align-content:stretch;font-family:var(--font-departure),monospace;color:#fff;text-shadow:2px 1px 0 #000;padding:20px;grid-gap:20px;gap:20px}.Forge_Footer__ghw3O{position:relative;display:flex;align-items:center;justify-content:center;padding:40px;background:rgba(59,55,49,.8);border-image-slice:18 26 26 23;border-image-width:16px 16px 16px 16px;border-image-outset:0 0 0 0;border-image-repeat:round repeat;border-image-source:url(/vl2-forge/_next/static/media/border.d6506fa8.png);border-style:solid;box-shadow:0 0 8px rgba(0,0,0,.3),inset 0 0 12px rgba(0,0,0,.8)}.Forge_Footer__ghw3O:before{top:-3px;left:-3px;right:-3px;bottom:-3px;border-color:rgba(255,250,234,.4) hsla(0,0%,9%,.5) hsla(0,0%,9%,.5) rgba(255,250,234,.4);border-style:solid;border-width:4px 4px 5px}.Forge_Footer__ghw3O:after,.Forge_Footer__ghw3O:before{display:block;content:"";position:absolute;pointer-events:none}.Forge_Footer__ghw3O:after{top:10px;left:10px;right:10px;bottom:10px;border-color:hsla(0,0%,9%,.5) rgba(255,250,234,.4) rgba(255,250,234,.4) hsla(0,0%,9%,.5);border-style:solid;border-width:4px}.Forge_Footer__ghw3O form{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:12px}.Forge_NameInput__lpcsg{position:relative;font-size:18px;font-family:var(--system-ui);font-weight:400;font-style:italic;line-height:1}.Forge_NameInput__lpcsg input{display:block;min-width:240px;max-width:100%;min-height:46px;background:#fff;color:#333;font-size:inherit;font-family:inherit;font-weight:inherit;font-style:inherit;line-height:inherit;padding:2px 50px 2px 10px}.Forge_NameInput__lpcsg:after{display:grid;place-content:center;content:".vl2";position:absolute;top:0;right:0;bottom:0;width:50px;color:#999}.Forge_DownloadButton__CnFTn{border:0;margin:0;padding:0 0 1px;min-width:128px;min-height:46px;font-family:var(--font-departure),monospace;font-size:18px;font-style:inherit;font-weight:inherit;line-height:1;border-radius:2px;background:url(/vl2-forge/_next/static/media/button.69ed12fe.png) transparent;background-repeat:no-repeat;background-size:150% 190%;background-position:50% 50%;border-color:rgba(255,145,105,.5) rgba(36,14,14,.7) rgba(36,14,14,.7) rgba(255,178,150,.5);border-style:solid;border-width:3px;color:#fff;text-shadow:0 0 5px rgba(255,226,82,.358),2px 3px 0 rgba(0,0,0,.5);cursor:pointer}.Forge_ListArea__OpY_R{display:flex;align-items:center;justify-content:center;padding:32px;background:rgba(2,2,84,.252);border-radius:12px}.Forge_FileList__9JOyh{width:100%;display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}.Forge_EmptyMessage__Lrlud{align-self:center;text-align:center}.Forge_AddButton__09pXD{border:0;margin:0;padding:0 0 2px;min-width:48px;min-height:48px;font-family:var(--font-departure),monospace;font-size:40px;font-style:inherit;font-weight:inherit;line-height:1;border-radius:3px;background:url(/vl2-forge/_next/static/media/button.69ed12fe.png) transparent;background-repeat:no-repeat;background-size:150% 190%;background-position:50% 50%;border-color:rgba(255,145,105,.5) rgba(36,14,14,.7) rgba(36,14,14,.7) rgba(255,178,150,.5);border-style:solid;border-width:3px;color:#fff;text-shadow:0 0 5px rgba(255,226,82,.358),2px 3px 0 rgba(0,0,0,.5);cursor:pointer;vertical-align:middle}.Forge_Header__7t3Qc{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:40px}.Forge_File__Mn05Y{display:flex;align-items:center;gap:12px}.Forge_DeleteButton__Csfdg{flex:0 0 auto;border:0;background:transparent;display:grid;place-content:center;font-size:24px;margin:0;padding:0;color:rgba(0,0,0,.4);cursor:pointer}.Forge_DeleteButton__Csfdg:hover{color:#ff5959}.Forge_DeleteButton__Csfdg svg{pointer-events:none}.Forge_IconContainer__AgM_T{flex:0 0 auto;display:grid;place-content:center;width:24px}.Forge_PreviewIcon__HFERe{width:100%;height:auto;max-height:24px;border:1px solid #000}.Forge_Path__GZs81{background:rgba(0,0,0,.3);padding:3px 8px;border-radius:8px}@media (max-width:767px){.Forge_Forge__dDZFe{grid-template-columns:1fr;grid-template-rows:auto 1fr;padding:12px;gap:12px}.Forge_Header__7t3Qc{flex-direction:row}.Forge_Header__7t3Qc img{width:auto;max-height:96px}}

View file

@ -0,0 +1 @@
@font-face{font-family:__departureMono_6dd175;src:url(/vl2-forge/_next/static/media/4df1fe70433cf083-s.p.woff2) format("woff2");font-display:swap;font-weight:400;font-style:normal}@font-face{font-family:__departureMono_Fallback_6dd175;src:local("Arial");ascent-override:71.70%;descent-override:19.56%;line-gap-override:0.00%;size-adjust:139.46%}.__className_6dd175{font-family:__departureMono_6dd175,__departureMono_Fallback_6dd175;font-weight:400;font-style:normal}.__variable_6dd175{--font-departure:"__departureMono_6dd175","__departureMono_Fallback_6dd175"}:root{--system-ui:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}*,:after,:before{box-sizing:border-box}html{text-size-adjust:100%}body,html{margin:0;padding:0}body{display:flex;flex-direction:column;min-height:100vh;background-color:#699697;background-image:url(/vl2-forge/_next/static/media/noise.27f5ca25.png),linear-gradient(180deg,#5be9ee 0,#1e4172);background-repeat:repeat,repeat}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
docs/border.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
docs/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

1
docs/index.html Normal file

File diff suppressed because one or more lines are too long

6
docs/index.txt Normal file
View file

@ -0,0 +1,6 @@
2:I[9330,["699","static/chunks/8e1d74a4-8e519121d6db0557.js","634","static/chunks/634-d90cc75f8d4e4c06.js","931","static/chunks/app/page-960543db4d1917f1.js"],"Forge"]
3:I[4707,[],""]
4:I[6423,[],""]
0:["AaZG0PNUAl--iHAutmB3I",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{}],[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/914a77656c3e670b.css","precedence":"next","crossOrigin":"$undefined"}]]],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/vl2-forge/_next/static/css/c50e9c221475fcb7.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","className":"__variable_6dd175","children":["$","body",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L5",null]]]]
5:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"VL2 Forge"}],["$","meta","3",{"name":"description","content":"Create .vl2 files for Tribes 2"}],["$","meta","4",{"name":"next-size-adjust"}]]
1:null

BIN
docs/logo-lg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/logo-md.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

5
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

7
next.config.js Normal file
View file

@ -0,0 +1,7 @@
module.exports = {
output: "export",
distDir: process.env.NODE_ENV === "production" ? "./docs" : undefined,
basePath: "/vl2-forge",
assetPrefix: "/vl2-forge/",
trailingSlash: true,
};

1525
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "vl2-forge",
"version": "1.0.0",
"description": "",
"author": "Brian Beck <exogen@gmail.com>",
"license": "MIT",
"keywords": [],
"main": "index.js",
"scripts": {
"dev": "next dev demo",
"build": "npm run build:pages",
"build:pages": "next build && touch docs/.nojekyll"
},
"dependencies": {
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"lodash.orderby": "^4.6.0",
"next": "^14.2.15",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.9",
"react-icons": "^5.3.0"
},
"devDependencies": {
"@types/node": "22.7.5",
"@types/react": "18.3.11",
"tshy": "^3.0.2",
"typescript": "^5.6.3"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/border.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logo-lg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/logo-md.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

252
src/Forge.module.css Normal file
View file

@ -0,0 +1,252 @@
.Forge {
flex: 1 0 auto;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
align-content: stretch;
font-family: var(--font-departure), monospace;
color: #fff;
text-shadow: 2px 1px 0 rgba(0, 0, 0, 1);
padding: 20px;
gap: 20px;
}
.Footer {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: rgba(59, 55, 49, 0.8);
border-image-slice: 18 26 26 23;
border-image-width: 16px 16px 16px 16px;
border-image-outset: 0px 0px 0px 0px;
border-image-repeat: round repeat;
border-image-source: url("../public/border.png");
border-style: solid;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3), inset 0 0 12px rgba(0, 0, 0, 0.8);
}
.Footer::before {
display: block;
content: "";
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-top: 4px solid rgba(255, 250, 234, 0.4);
border-left: 4px solid rgba(255, 250, 234, 0.4);
border-right: 4px solid rgba(23, 23, 23, 0.5);
border-bottom: 5px solid rgba(23, 23, 23, 0.5);
pointer-events: none;
}
.Footer::after {
display: block;
content: "";
position: absolute;
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
border-top: 4px solid rgba(23, 23, 23, 0.5);
border-left: 4px solid rgba(23, 23, 23, 0.5);
border-right: 4px solid rgba(255, 250, 234, 0.4);
border-bottom: 4px solid rgba(255, 250, 234, 0.4);
pointer-events: none;
}
.Footer form {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 12px;
}
.NameInput {
position: relative;
font-size: 18px;
font-family: var(--system-ui);
font-weight: normal;
font-style: italic;
line-height: 1;
}
.NameInput input {
display: block;
min-width: 240px;
max-width: 100%;
min-height: 46px;
background: #fff;
color: #333;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
font-style: inherit;
line-height: inherit;
padding: 2px 50px 2px 10px;
}
.NameInput::after {
display: grid;
place-content: center;
content: ".vl2";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 50px;
color: #999;
}
.DownloadButton {
border: 0;
margin: 0;
padding: 0 0 1px 0;
min-width: 128px;
min-height: 46px;
font-family: var(--font-departure), monospace;
font-size: 18px;
font-style: inherit;
font-weight: inherit;
line-height: 1;
border-radius: 2px;
background: url("../public/button.png") transparent;
background-repeat: no-repeat;
background-size: 150% 190%;
background-position: 50% 50%;
border-top: 3px solid rgba(255, 145, 105, 0.5);
border-left: 3px solid rgba(255, 178, 150, 0.5);
border-right: 3px solid rgba(36, 14, 14, 0.7);
border-bottom: 3px solid rgba(36, 14, 14, 0.7);
color: #ffffff;
text-shadow: 0 0 5px rgba(255, 226, 82, 0.358), 2px 3px 0 rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.ListArea {
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
background: rgba(2, 2, 84, 0.252);
border-radius: 12px;
}
.FileList {
width: 100%;
display: flex;
flex-direction: column;
gap: 6px;
margin: 0;
padding: 0;
list-style: none;
}
.EmptyMessage {
align-self: center;
text-align: center;
}
.AddButton {
border: 0;
margin: 0;
padding: 0 0 2px 0;
min-width: 48px;
min-height: 48px;
font-family: var(--font-departure), monospace;
font-size: 40px;
font-style: inherit;
font-weight: inherit;
line-height: 1;
border-radius: 3px;
background: url("../public/button.png") transparent;
background-repeat: no-repeat;
background-size: 150% 190%;
background-position: 50% 50%;
border-top: 3px solid rgba(255, 145, 105, 0.5);
border-left: 3px solid rgba(255, 178, 150, 0.5);
border-right: 3px solid rgba(36, 14, 14, 0.7);
border-bottom: 3px solid rgba(36, 14, 14, 0.7);
color: #ffffff;
text-shadow: 0 0 5px rgba(255, 226, 82, 0.358), 2px 3px 0 rgba(0, 0, 0, 0.5);
cursor: pointer;
vertical-align: middle;
}
.Header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 40px;
}
.File {
display: flex;
align-items: center;
gap: 12px;
}
.DeleteButton {
flex: 0 0 auto;
border: 0;
background: transparent;
display: grid;
place-content: center;
font-size: 24px;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.4);
cursor: pointer;
}
.DeleteButton:hover {
color: rgb(255, 89, 89);
}
.DeleteButton svg {
pointer-events: none;
}
.IconContainer {
flex: 0 0 auto;
display: grid;
place-content: center;
width: 24px;
}
.PreviewIcon {
width: 100%;
height: auto;
max-height: 24px;
border: 1px solid black;
}
.Path {
background: rgba(0, 0, 0, 0.3);
padding: 3px 8px;
border-radius: 8px;
}
@media (max-width: 767px) {
.Forge {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
padding: 12px;
gap: 12px;
}
.Header {
flex-direction: row;
}
.Header img {
width: auto;
max-height: 96px;
}
}

322
src/Forge.tsx Normal file
View file

@ -0,0 +1,322 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { useDropzone } from "react-dropzone";
import JSZip from "jszip";
import { saveAs } from "file-saver";
import orderBy from "lodash.orderby";
import { FaTrashAlt } from "react-icons/fa";
import styles from "./Forge.module.css";
import { base64ArrayBuffer } from "./utils";
import { hasUncaughtExceptionCaptureCallback } from "process";
function detectFileType(file): FileType | null {
if (file.type) {
if (/^image\//i.test(file.type)) {
return { mimeType: file.type, genericType: "image" };
} else if (/^audio\//i.test(file.type)) {
return { mimeType: file.type, genericType: "audio" };
}
}
if (/\.png$/i.test(file.name)) {
return { mimeType: "image/png", genericType: "image" };
} else if (/\.jpg$/i.test(file.name)) {
return { mimeType: "image/jpeg", genericType: "image" };
} else if (/\.bmp$/i.test(file.name)) {
return { mimeType: "image/bmp", genericType: "image" };
} else if (/\.webp$/i.test(file.name)) {
return { mimeType: "image/webp", genericType: "image" };
} else if (/\.gif$/i.test(file.name)) {
return { mimeType: "image/gif", genericType: "image" };
} else if (/\.tiff$/i.test(file.name)) {
return { mimeType: "image/tiff", genericType: "image" };
} else if (/\.svg$/i.test(file.name)) {
return { mimeType: "image/svg+xml", genericType: "image" };
} else if (/\.wav$/i.test(file.name)) {
return { mimeType: "audio/wav", genericType: "audio" };
} else if (/\.mp3$/i.test(file.name)) {
return { mimeType: "audio/mpeg", genericType: "audio" };
}
if (file.type) {
return {
mimeType: file.type,
genericType: null,
};
}
return null;
}
function FilePreview({ file, onDelete }) {
let icon = null;
if (file.dataUri && file.type?.genericType === "image") {
icon = (
<img
className={styles.PreviewIcon}
src={file.dataUri}
width={24}
alt=""
/>
);
}
return (
<div className={styles.File}>
<span className={styles.IconContainer}>{icon}</span>{" "}
<span className={styles.Path}>{file.path}</span>
<button
className={styles.DeleteButton}
type="button"
aria-label="Delete"
title="Delete"
onClick={(event) => {
onDelete(file.path);
}}
>
<FaTrashAlt />
</button>
</div>
);
}
type FileType = {
mimeType: string;
genericType: string | null;
};
type FileEntry = {
path: string;
buffer: ArrayBuffer;
dataUri: string | null;
date: Date | null;
unixPermissions: string | number | null;
dosPermissions: number | null;
type: FileType | null;
};
async function handleZipFile(file) {
const zip = await JSZip.loadAsync(file);
const map = new Map<string, FileEntry>();
for (const path in zip.files) {
const fileObj = zip.files[path];
if (!fileObj.dir) {
const buffer = await fileObj.async("arraybuffer");
const fileEntry = {
path,
buffer: buffer,
dataUri: null,
date: fileObj.date,
unixPermissions: fileObj.unixPermissions,
dosPermissions: fileObj.dosPermissions,
type: detectFileType(fileObj),
};
if (
fileEntry.type?.genericType === "image" ||
fileEntry.type?.genericType === "audio"
) {
const base64String = await fileObj.async("base64");
fileEntry.dataUri = `data:${fileEntry.type.mimeType};base64,${base64String}`;
}
map.set(path, fileEntry);
}
}
return map;
}
async function handleOtherFile(file) {
const map = new Map();
let path;
if (file.path) {
path = file.path;
if (path.startsWith("/")) {
path = path.slice(1);
}
} else if (file.name) {
path = file.name;
} else {
return map;
}
const buffer = await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", (event) => {
resolve(event.target.result as ArrayBuffer);
});
reader.readAsArrayBuffer(file);
});
const fileEntry = {
path,
buffer,
dataUri: null,
date: null,
unixPermissions: null,
dosPermissions: null,
type: detectFileType(file),
};
if (
fileEntry.type?.genericType === "image" ||
fileEntry.type?.genericType === "audio"
) {
const base64String = base64ArrayBuffer(buffer);
fileEntry.dataUri = `data:${fileEntry.type.mimeType};base64,${base64String}`;
}
map.set(path, fileEntry);
return map;
}
async function handleInputFile(file) {
if (/\.(zip|vl2)$/i.test(file.name)) {
return handleZipFile(file);
} else {
return handleOtherFile(file);
}
}
export function createZipFile(files: Array<FileEntry>) {
const zip = new JSZip();
for (const file of files) {
zip.file(file.path, file.buffer, {
date: file.date,
dosPermissions: file.dosPermissions,
unixPermissions: file.unixPermissions,
});
}
return zip;
}
export async function saveZipFile(zip: JSZip, name: string) {
const blob = await zip.generateAsync({
type: "blob",
mimeType: "application/octet-stream",
});
saveAs(blob, name);
}
export function Forge() {
const [actionLog, setActionLog] = useState(() => []);
const [files, setFiles] = useState(() => new Map());
const onDrop = useCallback(async (acceptedFiles) => {
const actionLog = [];
const finalMap = new Map();
const allFiles: Array<Map<string, any>> = await Promise.all(
acceptedFiles.map((file) => handleInputFile(file))
);
allFiles.forEach((map) => {
map.forEach((file, path) => {
if (finalMap.has(path)) {
actionLog.push({
type: "overwrite",
path,
});
}
finalMap.set(path, file);
});
});
setFiles((prevMap) => {
return new Map([
...Array.from(prevMap.entries()),
...Array.from(finalMap.entries()),
]);
});
setActionLog((prevLog) => [...prevLog, ...actionLog]);
}, []);
const { getRootProps, getInputProps, open, isDragActive } = useDropzone({
noClick: true,
onDrop,
});
const fileList = useMemo(() => {
const paths = orderBy(
Array.from(files.keys()),
[(path) => path.toLowerCase()],
["asc"]
);
return paths.map((path) => files.get(path));
}, [files]);
const addButton = (
<button
type="button"
className={styles.AddButton}
aria-label="Add files"
title="Add files"
onClick={open}
>
+
</button>
);
const handleDelete = useCallback((path) => {
setFiles((files) => {
const newFiles = new Map(files);
newFiles.delete(path);
return newFiles;
});
}, []);
return (
<>
<section className={styles.Forge} {...getRootProps()}>
<header className={styles.Header}>
<img
width={210}
height={188}
src="/vl2-forge/logo-md.png"
alt="VL2 Forge"
/>
{addButton}
</header>
<input {...getInputProps()} />
<div className={styles.ListArea}>
{fileList.length ? (
<ul className={styles.FileList}>
{fileList.map((file) => {
return (
<li key={file.path}>
<FilePreview file={file} onDelete={handleDelete} />
</li>
);
})}
</ul>
) : (
<div className={styles.EmptyMessage}>
Drop files onto the page or press the add button!
</div>
)}
</div>
</section>
<footer className={styles.Footer}>
<form
onSubmit={async (event) => {
event.preventDefault();
const form = event.target as HTMLFormElement;
const fileName = form.elements["fileName"] as HTMLInputElement;
const name = fileName.value.trim();
if (!name) {
window.alert("Name thy file.");
fileName.focus();
} else if (!fileList.length) {
window.alert("Add some files!");
} else {
const zip = createZipFile(fileList);
await saveZipFile(zip, `${name}.vl2`);
}
}}
>
<div className={styles.NameInput}>
<input
name="fileName"
type="text"
placeholder="name thy file"
onChange={(event) => {
if (/\.vl2$/i.test(event.target.value)) {
event.target.value = event.target.value.slice(0, -4);
}
}}
/>
</div>
<button type="submit" className={styles.DownloadButton}>
Download
</button>
</form>
</footer>
</>
);
}

9
src/fonts.ts Normal file
View file

@ -0,0 +1,9 @@
import localFont from "next/font/local";
export const departureMono = localFont({
src: "../public/DepartureMono-Regular.woff2",
weight: "400",
style: "normal",
display: "swap",
variable: "--font-departure",
});

49
src/utils.ts Normal file
View file

@ -0,0 +1,49 @@
// Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then
// use window.btoa' step. According to my tests, this appears to be a faster approach:
// http://jsperf.com/encoding-xhr-image-data/5
/*
MIT LICENSE
Copyright 2011 Jon Leighton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export function base64ArrayBuffer(arrayBuffer) {
var base64 = "";
var encodings =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var bytes = new Uint8Array(arrayBuffer);
var byteLength = bytes.byteLength;
var byteRemainder = byteLength % 3;
var mainLength = byteLength - byteRemainder;
var a, b, c, d;
var chunk;
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + "==";
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
}
return base64;
}

34
tsconfig.json Normal file
View file

@ -0,0 +1,34 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}