Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A talent-calculator-style planner for EverQuest Legends Alternate Advancement (AA) builds. Unofficial fan-made tool, not affiliated with the game.README 전문 보기 ↗A talent-calculator-style planner for EverQuest Legends Alternate Advancement (AA) builds. Unofficial fan-made tool, not affiliated with the game.
Live: https://aacalc.abnotus.com
?, color-coded by confidence. Purely a display hint — never counted in real point totals, and automatically replaced the moment the wiki confirms the real valuePlayer-facing version history is in the app itself — click the version tag in the bottom-right corner. For everything else, git log is the changelog.
Each version in that history has a matching annotated git tag (vX.Y.Z), so a reported issue can be pinned to a specific version.
All AA data (costs, effects, ranks, prerequisites) lives in data.src.js, sourced from eqlwiki.com/Alternate_Advancement and cross-checked against in-game logs/screenshots where the wiki is silent or wrong. Values marked ? are undocumented anywhere and treated as 0 until confirmed.
Most descriptions state one effect that scales, written as a slash progression ("by 2/4/6%") so the value for the rank you hold can be picked out. An AA whose ranks each do something different is written as consecutive "Rank N: ..." clauses instead, and renders one line per rank with your current one marked (splitPerRankLines in logic.js). A progression can't express that shape: its slots are positional, so one covering only ranks 2-4 would line up against ranks 1-3, and a rank doing something unrelated has no slot at all. Splitting only starts at the very beginning of a description, which is what keeps a passing mention like "Rank 2 requires level 30" inline.
python wiki-sync/scrape_wiki.py
Fetches the AA page's current wikitext from eqlwiki's MediaWiki API and compares it against wiki-sync/snapshot.json (the state as of the last run). Prints what's new, gone, or changed, then overwrites the snapshot.
It's a diagnostic, not an auto-updater — it never touches data.src.js. Run it, review what changed, cross-check those entries against data.src.js by hand, apply any confirmed fixes, then rebuild. Run manually whenever we want to check in on the wiki; never on a schedule.
python wiki-sync/verify_from_log.py path/to/eqlog_CharName_Zone.txt
A second, independent check on the costs data.src.js marks as confirmed. It reads the "gained the ability ... at a cost of N ability points" and "improved ... at a cost of N" lines from a player's own EverQuest Legends log and compares each against the data. Like the scrape, it's a diagnostic: it never reads the wiki and never touches data.src.js. Matches are recorded in wiki-sync/log_verified.json; a mismatch, a name the data doesn't have, or a rank the current data lacks is reported for you to look at. A log only covers what that character trained while logging, so it confirms a subset. wiki-sync/total_verified.json lists the owned ranks the log doesn't cover, taken from a build export whose total matched the in-game total; those are verified by that total match alone, one tier below a log match.
python wiki-sync/guess_costs.py
Regenerates src/costGuesses.js: estimates for per-rank costs the wiki hasn't documented (? in data.src.js). It cross-references other fully known AAs with the same rank count and matching known costs rather than trusting one AA's own progression (2/4/6/? looks like doubling, but a sibling known at 2/4/6/9 proves otherwise). Confidence scales with how many siblings agree (tiers in the script's docstring); a gap between two of the AA's own known costs can still get a lower-confidence interpolated guess.
The file is rewritten from scratch each run, so a confirmed guess simply disappears. Run it after any data.src.js change. A small hand-maintained MANUAL_GUESSES dict covers slots nothing else can reach, tagged with its own "very-low" tier; a real cross-AA match always wins over it.
python wiki-sync/guess_effects.py
The same for numeric values inside effect descriptions, such as the ? in "Increases your critical hit chance by 1/?/5/10%.". Regenerates src/effectGuesses.js, keyed one level deeper than costs because one description can hold several progressions (Adamant Will has two). Siblings are compared only within hand-declared groups (EFFECT_SIBLING_GROUPS): cost curves recur across AAs because the game reuses templates, but a recurring effect magnitude is coincidence. Interpolation and the manual fallback work as for costs.
python wiki-sync/assign_aa_ids.py
Maintains src/aaIds.js, the append-only numeric id table the share/export format addresses AAs by (keys.js's idForKey/entryForId). Existing AAs keep their id and new ones get the next unused integer. An id is never reused, even for a removed AA, so an old link resolves to "gone" rather than to a different AA.
Run it after any data.src.js change that adds, removes or renames an AA. Identity comes from the slugified name, so a rename looks like a removal plus an addition, and existing links would silently drop that AA's picks. The script warns whenever an id vanishes in the same run new ones are assigned. A vanished id isn't automatically a rename: an AA can be genuinely removed. Once it's confirmed the AA itself is unchanged under a new name, make two fixes in the same commit: hand-edit aaIds.js to give the new key the old id (share links), and add an old-key → new-key entry to RENAMED_KEYS in keys.js (local saves store the key directly, and pre-v4 index-based saves resolve to a key through LEGACY_AA_ORDER first, so without it a saved rank silently drops on the next load). tests/test_renamed_aa_key_migration.py covers the second, for both save formats.
No build tools, no server — just open index.html in a browser.
The app logic is authored as real ES modules under src/ (aaIds.js, costGuesses.js, effectGuesses.js, keys.js, changelogData.js, state.js, logic.js, builds.js, dom.js, render.js, exportImport.js, events.js, main.js). Native ES modules don't work over file:// in Chrome, and this app is deliberately built to run by just double-clicking index.html with no local server — so build_minify.py bundles src/main.js and its imports into a single classic script and minifies it (via esbuild, a build-time-only dependency — see Prerequisites below), which is what index.html actually loads. It runs esbuild through its JS API, so the build works the same on Windows, Linux and macOS, and it writes nothing until every output has been generated.
build_minify.py also runs data-integrity checks before building and fails with an explanation if one is violated, rather than shipping something broken: wiki-sync/common.py's data_integrity_problems (node evaluates data.src.js and every AA it finds must also be seen by the line parser the build and the wiki-sync scripts share, and no two AAs in one list may slugify to the same key), check_prereq_disambiguation_invariant (a repeated AA name needs exactly one non-auto occurrence for prereq resolution to stay deterministic) and check_aa_ids_current (every AA in data.src.js needs a matching entry in src/aaIds.js, or it silently drops out of every share link/export that includes it — run wiki-sync/assign_aa_ids.py to fix), and that src/costGuesses.js/src/effectGuesses.js match what wiki-sync/guess_costs.py/guess_effects.py --check would generate now (effect guesses are keyed by progression position, so an edited description can otherwise attach a stale guess to the wrong slot). If a build fails on one, the error message says what to do.
Prerequisites: npm install once, to pull in esbuild (the only dependency, and build-time only — the shipped app itself still has none, no server, works from file://).
To make a change:
src/ (app logic), data.src.js (AA data), or styles.src.css.python build_minify.py. This regenerates app.src.js (the unminified bundle, readable — generated, don't edit directly), app.js/data.js/styles.css (minified, what ships), and re-stamps index.html with a cache-busting version hash.index.html to test.At runtime, state.ranks and purchaseOrder address AAs by index into AA_DATA — simple, and every render/logic function already works that way. But that index is not what gets persisted to localStorage, exported text, or share links: src/keys.js derives a stable key from each AA's name instead, so a save survives data.src.js being reordered or regenerated by a wiki scrape. Without this, reordering an AA would silently shift every index-based save onto the wrong ability.
keys.js also carries a frozen snapshot of AA_DATA's ordering as of 2026-07-09 (LEGACY_AA_ORDER), used only to migrate saves made before this existed. Never update it — it's a historical record of what old saves meant, not current data.
A share link/export's BUILD_CODE is a compact, numeric-id-keyed payload, base64-encoded. As of BUILD_CODE_VERSION 5 it's packed bits rather than JSON (src/exportImport.js's packV5/expandBinaryPayload): every field is sized to its own real enforced ceiling — a rank needs 5 bits, an AA id 9, a purchase-order entry only 6 (it indexes into this build's own AA list, not the global id space) — and the AA set rides as a bitmap when that beats listing ids outright, which the encoder decides per build by packing both and keeping the smaller. Owned progress is stored as one bit per planned AA meaning "owned at the planned rank", plus an explicit delta only where the two disagree (1 entry in 43 on a real build) and, since v7, an (id, rank) pair for each AA that's owned but no longer planned. Measured 41-67% smaller than the JSON format across build sizes (~45% on a 180-pick build: 604 characters down to 332), with nothing dropped or approximated.
v5 is deliberately not compressed: packed bits are near-maximum entropy, so DEFLATE made them bigger. It carries a CRC-16 instead of DEFLATE's incidental integrity check, so a mangled or truncated link reports "looks invalid" rather than decoding into a plausible but wrong build.
decodeBuildCode recognizes every format the app has emitted, sniffed from the bytes: v5+'s magic byte first (binary can be valid deflate-raw input that inflates into garbage), falling through to the older formats if that decode fails, since about one older deflate-raw code in 40 starts with the same byte; then gzip's 2-byte magic, then a deflate-raw attempt, then uncompressed JSON. On the JSON side expandCompactPayload accepts the v3+ positional array or the v2 keyed object (a keyed object counts as compact only at v 2 and without the verbose save's fields, since v1.0.0's links were the verbose save itself, carrying its own v: 4), and expandCompactRanks the columnar r/o shape (v4+) or the older pair array, so every era of link keeps working. A decoded value that isn't a recognizable build is rejected. decompress pipes through a Blob's stream so a rejection surfaces in the awaited call a try/catch guards.
Rank values are clamped per entry on load (clampRankValue), and numeric fields from untrusted input go through safeParseInt, which accepts only numbers and strings. purchaseOrder gets its own ceiling too: every load walks it (reconcilePurchaseOrderCounts in logic.js is linear, but rendering a step per entry isn't free), and a share link inflated to tens of thousands of entries is cheap to make, since it compresses well. MAX_PURCHASE_ORDER (2000, in state.js's deserializePurchaseOrder) truncates it first; a fully maxed roster is a few hundred entries. MAX_WAYPOINTS bounds waypoints the same way.
state.js's STORAGE_KEY is whatever build you're currently looking at — autosaved on every change, loaded unconditionally on boot. src/builds.js adds named snapshots on top as a separate concern: saving copies the current state into its own key, loading overwrites the current state with a saved copy. Which slot a loaded/saved build is "active" is tracked per tab (sessionStorage, seeded from the last one in localStorage for a new tab), so a Load in one tab can't change which slot another tab's Link or Split repoints. It's cleared on Reset/Import/a share link so a later save can't mistake unrelated content for an update to a slot it no longer matches.
state.owned (the Progression tab's "actually trained in-game" watermark) persists to a localStorage key scoped to an owned profile id (ownedStorageKeyFor, state.js), not a single fixed key. state.ownedProfileId says which profile the current session is showing; each saved Build slot carries its own ownedProfileId field pointing at one too. Two builds pointing at the same profile id read and write the same storage entry; that's what "linked" tracking means. A change re-reads the stored profile before writing it, and a write from another tab arrives through the storage event, so two open tabs on one profile keep each other's marks.
Saving a build makes it the active slot and moves the session onto that slot's profile, so marks made afterwards land in the build just saved. A new slot, or an existing one that wasn't already sharing the session's profile, gets a fresh profile seeded with a copy of whatever's showing at save time; re-saving the build that's loaded keeps its profile and any links. The Progression tab's Manage tracking… control (render.js's owned-tracking modal, backed by linkOwnedToBuild/mergeOwnedFromBuild/splitOwnedFromCurrent in builds.js) can Link two builds onto the same profile, Merge another build's marks in as a one-directional non-destructive union, or Split a linked build back onto its own copy, at any time. Owned data rides every export and share link, since it belongs to one build just like the plan does. Every import, a share link or pasted text, gets a fresh profile holding the owned progress it carries (empty if it carries none), with no confirmation, because nothing existing can be overwritten.
Every existing user's builds are backfilled to a well-known shared LEGACY_OWNED_PROFILE_ID profile the first time they're seen post-upgrade (migrateLegacyOwnedProfile/migrateStaleBuildSlots) — a self-healing sweep that re-checks on every boot rather than a one-time flag, so a build that reaches a given browser later (a restored backup, a synced device) still gets caught. The original eql_aa_owned_v1 key is left in place, never deleted.
Minting a profile has no matching cleanup on its own — deleting a build only removes its index entry and slot key, not whatever profile it pointed at, since another build might still be linked to it. builds.js's cleanupOrphanedOwnedProfiles(), called once per boot, sweeps any profile with no build slot's ownedProfileId, neither the live session nor the autosaved one pointing at it, and no write in the last 7 days (it may be another open tab's unsaved session), via state.js's listOwnedProfileIds/removeOwnedProfile, leaving the legacy profile untouched regardless.
src/costGuesses.js is only ever consulted through keys.js's costGuessFor, and only when the real costs[rankIdx] is exactly "?" — logic.js's costNum()/spentPoints() never look at it, so an estimate can't affect real point totals. The moment a real number replaces "?" in data.src.js, that slot's guess (if one still exists) is simply never read again.
src/effectGuesses.js has the same guarantee: logic.js's highlightRankValue only substitutes a guess where the description text is literally "?" — search, export text, and everywhere else a description is read still see the real, unmodified string.
tests/ has data-independent Python unit tests for wiki-sync/guess_costs.py's, wiki-sync/guess_effects.py's, wiki-sync/assign_aa_ids.py's, wiki-sync/common.py's, wiki-sync/scrape_wiki.py's and wiki-sync/verify_from_log.py's core logic, plus 54 Playwright browser tests that drive the actual app — cost/effect estimates, class-based rank caps, Archetype class-eligibility gating, expansion-era badges, hiding AAs, Progression's drag/auto-scroll/reorder, cross-class prereq dependencies, per-build owned-tracking profiles (Link/Merge/Split, migration, orphaned-profile cleanup), share-code encoding backward-compatibility, and the Other Classes tab, among others. See tests/README.md for the full list, prerequisites, and how to run them (python tests/run_all.py runs everything in one pass). Every push to main deploys the site, so the build and tests are run by hand before pushing; GitHub Actions (.github/workflows/checks.yml) runs them again after each push.
Hosted on GitHub Pages, served from main on every push.
PolyForm Noncommercial 1.0.0 — free to use, share, and modify for any noncommercial purpose.