Important
🚚 Dự án đã chuyển nhà — this project has moved.
LOL2D đã tách thành engine moba2d và content pack moba2d-packs/lol — giờ có thêm cả pack Dota và Naruto đánh chung một sân. Mọi phát triển mới diễn ra ở đó.
👉 Chơi bản mới tại moba2d.pages.dev — các link cũ của trang này tự chuyển hướng sang đó. Repo này được giữ lại làm lịch sử.
Play your favourite League of Legends champions right in the browser — a 2D Summoner's Rift, 58 champions, bot fights, and an installable PWA you can play offline.
A fan-made, indie game based on League of Legends by Riot Games. It runs entirely in the browser: p5.js draws the canvas, Vue 3 drives the HUD, all in TypeScript and bundled with Vite.
What is in it:
- 58 champion kits rebuilt from the real game — skillshots, charged casts, channels, recasts, shields, heals, and a full spread of crowd control.
- A kit builder: mix and match abilities from different champions into a custom loadout, save it, and drop it onto yourself or any bot.
- Blue-vs-Red team fights with bots, neutral jungle camps, allied fountains and turrets, and three lanes of minion waves.
- Fog of war built from a visibility-polygon sweep, with bushes and walls that really do block line of sight.
- Touch controls and a mobile-friendly HUD alongside mouse/keyboard.
- Installable as a PWA — works offline once cached.
| Action | Key |
|---|---|
| Move / attack target | Right click ground / enemy |
| Abilities | A Q W E R |
| Summoner spells | D F |
| Toggle camera follow | Space |
| Zoom | Mouse wheel |
| Nav debug overlay | N |
| Practice panel (pause + live settings) | Esc |
Charged abilities (Varus Q, Pantheon Q) are held down and fire on release. Esc pauses and opens the practice panel rather than leaving the match — exit from the panel's Trận đấu tab.
Requires Node.js 20 or newer.
git clone https://github.com/HoangTran0410/LOL2D.git
cd LOL2D
npm install
npm run devOpen the URL Vite prints (http://localhost:5173 by default).
npm run devrunsassets:generatefirst, so the asset manifest always matches what is on disk inassets/without you having to think about it.
Production build:
npm run build # emits dist/
npm run preview # serve the built outputsrc/
├── main.ts # entry point: boots p5 (global mode) and the SceneManager
├── scenes/ # LoadingScene → MenuScene → GameScene
│ └── setup/ # pregame setup screen (roster, kit builder, rules)
├── game/
│ ├── Game.ts # main loop, owns camera/objectManager/terrainMap/fogOfWar
│ ├── MatchDirector.ts # every mutation of a running match, and the only thing that persists them
│ ├── preset.ts # champion kits, jungle camps, turret and fountain spots
│ ├── gameObject/
│ │ ├── attackableUnits/ # Champion, AIChampion, Minion, Monster, Turret
│ │ ├── spells/ # one file per ability: Ahri_Q.ts, Yasuo_R.ts, ... (58 champions)
│ │ ├── spellObjects/ # base classes: Missile, Area, Beam, HomingMissile
│ │ ├── buffs/ # Stun, Slow, Shield, Invisible, ...
│ │ ├── structures/ # Turret, Fountain
│ │ └── map/ # TerrainMap, FogOfWar, Camera, Obstacle, Minimap
│ ├── combat/ # Vision, MatchTally, ExecuteTargeting
│ ├── nav/ # NavGrid pathfinding
│ ├── managers/ # ObjectManager (quadtree), MinionSpawner, EventManager
│ ├── input/ # keyboard/mouse + TouchControls
│ ├── spell/runtime/ # the spell lifecycle state machine
│ ├── config/ # PregameConfig, savedKits (localStorage)
│ ├── enums/ # TeamId, ActionState, StatusFlags, SpellState, EventType
│ ├── vfx/, debug/ # shared VFX helpers, nav/debug overlays
│ └── hud/ # Vue-based HUD, incl. hud/practice/ (the Esc panel)
├── managers/ # AssetManager, SceneManager
├── pwa/ # service worker registration/update flow
└── generated/ # script-generated asset manifest — do not hand-edit
Spell lifecycle. Every spell declares a castSpec describing how it is cast — press, hold-and-release, channel, or recast — and SpellRuntime runs the READY → CASTING/CHARGING → ACTIVE → COOLDOWN state machine, including resource commit, refund on interrupt, and interrupt sources (death, stun, silence, displacement). Spells only implement the onCastStart / onRelease / onSpellCast hooks.
Spell objects. Projectiles extend MissileSpellObject, area effects extend AreaSpellObject, lines use BeamSpellObject — note that the beam is hit detection only and does not draw itself, so subclass it and write a draw().
Collision and queries. ObjectManager maintains a quadtree rebuilt each frame; all target selection goes through queryObjects({ area, filters }) with the ready-made predicates in PredefinedFilters.
Crowd control. Buffs raise and clear bits in StatusFlags, which the system resolves into ActionState (can move / can cast / targetable).
Teams and lanes. A running match assigns the player to Blue and balances bots across Blue/Red; champions share their side's fountain, turret row, and lane minions. Neutral/standalone objects keep the unique teamId fallback. MinionSpawner runs mirrored waves down the three paths in lanes.ts, including melee, caster, and cannon minions.
The practice panel (Esc) is a superset of the pregame setup screen: three tabs (Đấu thủ, Trận đấu, Gian lận) that reshape a paused, live match through MatchDirector rather than touching localStorage directly.
PWA. The build copies p5 and stats.js into public/vendor/ and loads them locally instead of from a CDN, and a service worker precaches the app shell, so the game can boot fully offline after the first visit.
The full details live in docs/ADDING_SPELLS.md — read it before writing a new spell. It covers the three registration points, the mandatory buff stackId rule, and the engine traps tsc cannot catch.
Images and JSON live under assets/. npm run assets:generate walks that tree and emits src/generated/assetManifest.ts with a typed AssetKey union, so a typo in an asset name is a compile error rather than a broken image at runtime. To add art, drop the file in the right folder and re-run that script.
Ability data (damage, cooldowns, ranges, icons) is imported from the LoL Wiki by scripts/wiki/import-abilities.mjs into docs/abilities/<champion>/<slot>.json, with provenance recorded in assets/source-manifest.json.
npm run ability:import -- --champions Ahri,Zed --slots Q,W,E,R
npm run ability:checktools/ also holds shape-maker, a standalone p5 app for drawing the map's polygon data.
Unit tests run under Vitest with no browser: every p5 drawing global is stubbed with a spy, so a test can prove which primitives a spell asks for and how its logic behaves.
npm test
npx vitest run tests/game/spells/Varus_Q.test.ts # a single fileHouse rule: tuning values are exported as constants from the spell file and imported by its test. Tests assert the wiring, not a copy of the numbers — retuning damage should never mean editing a test.
End-to-end tests drive real Chrome through Playwright, because a unit test cannot prove the game boots and paints. tests/e2e/ has 25+ scripts covering the practice panel, touch controls, minimap, kit builder, PWA offline boot, and more — run the one that touches what you changed rather than the whole folder:
npx vite --port 5199 --strictPort # in another terminal
npm run e2e # or e.g. node tests/e2e/drive-practice-panel.mjsScripts reach into the running game through window.__lol2d, which only exists in dev builds. drive-new-spells.mjs and drive-touch-controls.mjs have known rare flakes unrelated to code correctness — a stray dev server already holding port 5173 makes both more likely.
Contributions are welcome. What you need to know:
- Fork and branch off
main. - Run
npm run verifybefore opening a PR. It runs exactly what CI runs: asset check, ability-data check, both type-check passes, the full test suite, and the build. That is the repository's complete offline check. - Adding a spell? Read
docs/ADDING_SPELLS.mdfirst. There are three registration points, and missing one means the spell never shows up. - Bring tests. Each spell should have a file in
tests/game/spells/. Export the tuning constants from the spell and import them in the test rather than copying numbers. - Look at it. If your change is visual, open the real game — or write a script in
tests/e2e/. A test assertingdraw()was called proves nothing about how it looks. - Formatting follows Prettier (
.prettierrc: 2 spaces, single quotes, trailing commas, 100 columns). - Comments explain why, not what. Prefer recording the reason an approach was chosen, or the trap that forced the code into its current shape.
This is a non-commercial, fan-made project, not affiliated with or endorsed by Riot Games. The game is free and generates no revenue; it exists for entertainment only.
League of Legends and all related trademarks, characters, artwork, and other assets are the property of Riot Games. This project claims no ownership over that intellectual property.



