Initial commit
This commit is contained in:
6
dota-random-builds-front/.dockerignore
Normal file
6
dota-random-builds-front/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
*.log
|
||||
.env*
|
||||
36
dota-random-builds-front/.gitignore
vendored
Normal file
36
dota-random-builds-front/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
37
dota-random-builds-front/AGENTS.md
Normal file
37
dota-random-builds-front/AGENTS.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `src/main.ts` bootstraps the Vue app, mounts `App.vue`, and registers Pinia + router.
|
||||
- `src/App.vue` hosts the layout shell; global theme styles live in `src/assets/main.css`.
|
||||
- `src/pages` holds route-level views (e.g., `HomePage.vue`), wired in `src/router/index.ts`.
|
||||
- `src/components` contains shareable UI (`Randomizer.vue`); aim to keep these presentational.
|
||||
- `src/stores` stores state and side effects (e.g., `randomData.ts` for `/api/randomize` and localStorage prefs); static datasets sit in `src/data`.
|
||||
- `public` serves static assets as-is; `vite.config.ts` configures aliases (`@` → `src`) and plugins.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `npm run dev` — start Vite dev server with hot reload.
|
||||
- `npm run build` — type-check via `vue-tsc`, then produce the production bundle.
|
||||
- `npm run build-only` — production bundle without type-check (useful for quick iteration).
|
||||
- `npm run preview` — serve the built assets locally to verify production output.
|
||||
- `npm run type-check` — standalone TS/SFC type validation.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Vue 3 + TypeScript with `<script setup>`; keep logic typed and colocated with templates/styles.
|
||||
- Use 2-space indentation, single quotes for strings, and omit semicolons to match existing files.
|
||||
- Components/pages use PascalCase filenames; route-level views use a `Page` suffix.
|
||||
- Prefer `@/` alias imports over deep relatives; group imports by library/local.
|
||||
- Keep styles inside SFC blocks or `src/assets/main.css`; reuse existing CSS variables where possible.
|
||||
|
||||
## Testing Guidelines
|
||||
- No automated tests exist yet; before pushing, run `npm run type-check` and `npm run build` to catch regressions.
|
||||
- Manual QA: exercise the randomizer flow (toggle skills/aspects, adjust item count, observe loading/error states).
|
||||
- When adding tests, favor Vitest + Vue Test Utils and mirror component filenames (e.g., `Randomizer.spec.ts`).
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Git history is minimal; use concise, imperative messages (prefer Conventional Commits such as `feat: add randomizer store`) that describe intent.
|
||||
- PRs should include: summary of changes, impacted areas (UI/store/API), steps to reproduce/test, and screenshots for UI-facing updates.
|
||||
- Link to relevant issues, keep PRs small and focused, and call out any API or config assumptions (backend `/api/randomize` endpoint, localStorage usage).
|
||||
|
||||
## Environment & API Notes
|
||||
- The frontend expects an `/api/randomize` POST endpoint; if running locally without the backend, configure a mock or Vite proxy.
|
||||
- Avoid storing secrets in the repo; prefer runtime env vars and document any required values in the PR description.
|
||||
52
dota-random-builds-front/API.md
Normal file
52
dota-random-builds-front/API.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# API Endpoints for Frontend Compatibility
|
||||
|
||||
This document defines the minimal backend surface expected by the Vue frontend. Default base path is `/api`; responses are JSON with `Content-Type: application/json`.
|
||||
|
||||
## POST /api/randomize
|
||||
- **Purpose:** Produce a randomized Dota 2 build and optionally return the full data sets used for rendering.
|
||||
- **Request body:**
|
||||
- `includeSkills` (boolean) — include a skill build suggestion.
|
||||
- `includeAspect` (boolean) — include an aspect suggestion.
|
||||
- `itemsCount` (number) — how many items to return (UI offers 3–6).
|
||||
- **Response 200 body:**
|
||||
- `hero` — object `{ id: string; name: string; primary: string; }`.
|
||||
- `items` — array of `{ id: string; name: string; }` with length `itemsCount` (also used to hydrate cached items if you return the full pool).
|
||||
- `skillBuild` (optional) — `{ id: string; name: string; description?: string; }` when `includeSkills` is true.
|
||||
- `aspect` (optional) — string when `includeAspect` is true.
|
||||
- `heroes` (optional) — full hero list to cache client-side; same shape as `hero`.
|
||||
- `skillBuilds` (optional) — full skill build list; same shape as `skillBuild`.
|
||||
- `aspects` (optional) — array of strings.
|
||||
- **Example request:**
|
||||
```json
|
||||
{
|
||||
"includeSkills": true,
|
||||
"includeAspect": false,
|
||||
"itemsCount": 6
|
||||
}
|
||||
```
|
||||
- **Example response:**
|
||||
```json
|
||||
{
|
||||
"hero": { "id": "pudge", "name": "Pudge", "primary": "strength" },
|
||||
"items": [
|
||||
{ "id": "blink", "name": "Blink Dagger" },
|
||||
{ "id": "bkb", "name": "Black King Bar" }
|
||||
],
|
||||
"skillBuild": { "id": "hookdom", "name": "Hook/Rot Max", "description": "Max hook, then rot; early Flesh Heap." },
|
||||
"heroes": [{ "id": "axe", "name": "Axe", "primary": "strength" }],
|
||||
"skillBuilds": [{ "id": "support", "name": "Support" }],
|
||||
"aspects": ["Heroic Might"]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
- Use standard HTTP status codes (400 invalid input, 500 server error).
|
||||
- Error payload shape:
|
||||
```json
|
||||
{ "message": "Failed to load random build" }
|
||||
```
|
||||
- The UI surfaces `message` directly; keep it user-friendly.
|
||||
|
||||
## Implementation Notes
|
||||
- Ensure deterministic constraints: unique `id` per entity and avoid empty arrays when `includeSkills`/`includeAspect` are true.
|
||||
- If backing data is static, you may return `heroes/items/skillBuilds/aspects` once per request; the frontend caches them after the first successful call.
|
||||
21
dota-random-builds-front/Dockerfile
Normal file
21
dota-random-builds-front/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source and build
|
||||
COPY . .
|
||||
RUN npm run build-only
|
||||
|
||||
# Production stage with nginx
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
42
dota-random-builds-front/README.md
Normal file
42
dota-random-builds-front/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# dota-random-builds-front
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
1
dota-random-builds-front/env.d.ts
vendored
Normal file
1
dota-random-builds-front/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
dota-random-builds-front/index.html
Normal file
13
dota-random-builds-front/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
dota-random-builds-front/nginx.conf
Normal file
23
dota-random-builds-front/nginx.conf
Normal file
@@ -0,0 +1,23 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
3088
dota-random-builds-front/package-lock.json
generated
Normal file
3088
dota-random-builds-front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
dota-random-builds-front/package.json
Normal file
33
dota-random-builds-front/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "dota-random-builds-front",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.3",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"typescript": "~5.9.0",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vue-tsc": "^3.1.5"
|
||||
}
|
||||
}
|
||||
BIN
dota-random-builds-front/public/favicon.ico
Normal file
BIN
dota-random-builds-front/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
127
dota-random-builds-front/src/App.vue
Normal file
127
dota-random-builds-front/src/App.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView, RouterLink } from 'vue-router';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container">
|
||||
<nav class="nav">
|
||||
<RouterLink to="/" class="nav-link">Randomizer</RouterLink>
|
||||
<RouterLink to="/build-of-day" class="nav-link">Build of the Day</RouterLink>
|
||||
</nav>
|
||||
<RouterView />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
|
||||
#app{
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
/* --- Многослойное Dota/Arcane освещение --- */
|
||||
background:
|
||||
/* Бирюзовый левый свет */
|
||||
radial-gradient(900px 700px at 16% 22%, var(--glow-1), transparent 70%),
|
||||
|
||||
/* Синий правый объемный свет */
|
||||
radial-gradient(800px 600px at 86% 78%, var(--glow-2), transparent 72%),
|
||||
|
||||
/* Верхнее золото (очень тонкое, “блеск”) */
|
||||
radial-gradient(1000px 800px at 50% -20%, var(--gold-glow), transparent 85%),
|
||||
|
||||
/* Нижнее мягкое свечение */
|
||||
radial-gradient(900px 900px at 50% 120%, var(--blue-glow), transparent 75%),
|
||||
|
||||
/* Вертикальный общий градиент */
|
||||
linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%);
|
||||
|
||||
color: #e6f0fa;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial;
|
||||
}
|
||||
|
||||
/* --- Лёгкий аркановый блик сверху --- */
|
||||
#app::before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.06), transparent 45%);
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
|
||||
/* --- Premium noise — чуть заметный, как в Dota UI --- */
|
||||
#app::after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: var(--noise-opacity);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Контейнер контента */
|
||||
.container{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Мягкое появление */
|
||||
animation: fadeIn 0.6s ease forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* mobile */
|
||||
@media (max-width: 720px){
|
||||
.container{ padding: 20px; }
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
color: #e6f0fa;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 0 12px rgba(79, 195, 247, 0.2);
|
||||
}
|
||||
|
||||
.nav-link.router-link-exact-active {
|
||||
background: linear-gradient(90deg, #b63a2b, #8b241b);
|
||||
color: #f8e7c6;
|
||||
border-color: rgba(182, 58, 43, 0.5);
|
||||
box-shadow: 0 0 12px rgba(182, 58, 43, 0.45);
|
||||
}
|
||||
</style>
|
||||
31
dota-random-builds-front/src/assets/main.css
Normal file
31
dota-random-builds-front/src/assets/main.css
Normal file
@@ -0,0 +1,31 @@
|
||||
:root{
|
||||
--bg-1: #050a19;
|
||||
--bg-2: #0b1f3a;
|
||||
|
||||
/* Бирюзово-синие glows */
|
||||
--glow-1: rgba(79, 209, 197, 0.16);
|
||||
--glow-2: rgba(56, 189, 248, 0.12);
|
||||
--glow-3: rgba(96, 165, 250, 0.10);
|
||||
|
||||
/* Акценты и текст */
|
||||
--accent: #4fd1c5;
|
||||
--muted: #9fb3cc;
|
||||
|
||||
--noise-opacity: 0.04;
|
||||
|
||||
/* Dota-стильные подсветки */
|
||||
--gold-glow: rgba(200, 168, 106, 0.12);
|
||||
--blue-glow: rgba(79,195,247,0.12);
|
||||
}
|
||||
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
/* width: 100%;
|
||||
height: 100%; */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
410
dota-random-builds-front/src/components/Randomizer.vue
Normal file
410
dota-random-builds-front/src/components/Randomizer.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div class="randomizer">
|
||||
<h2>Random Dota 2 Build</h2>
|
||||
|
||||
<div class="controls">
|
||||
<label>
|
||||
Hero:
|
||||
<select v-model="selectedHeroId">
|
||||
<option :value="null">Random</option>
|
||||
<option v-for="hero in heroes" :key="hero.id" :value="hero.id">
|
||||
{{ hero.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><input type="checkbox" v-model="includeSkills" /> Include skill build</label>
|
||||
<label><input type="checkbox" v-model="includeAspect" /> Include aspect</label>
|
||||
<label>
|
||||
Items to pick:
|
||||
<select v-model.number="itemsCount">
|
||||
<option :value="3">3</option>
|
||||
<option :value="4">4</option>
|
||||
<option :value="5">5</option>
|
||||
<option :value="6">6</option>
|
||||
</select>
|
||||
</label>
|
||||
<button @click="requestRandomize" :disabled="!canRandomize">
|
||||
{{ loading ? 'Loading...' : 'Randomize' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">Не удалось загрузить данные: {{ error }}</p>
|
||||
|
||||
<div v-if="result" class="result">
|
||||
<div>
|
||||
<h3>Hero</h3>
|
||||
<div class="hero">{{ result.hero.name }}</div>
|
||||
<div class="hero-badge">{{ result.hero.primary }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Items</h3>
|
||||
<ul>
|
||||
<li v-for="it in result.items" :key="it.id">{{ it.name }}</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="includeSkills">
|
||||
<h3>Skill Build</h3>
|
||||
<div v-if="result?.skillBuild" class="skill-build">
|
||||
<div v-for="level in skillLevels" :key="level" class="skill-level">
|
||||
<span class="level-num">{{ level }}</span>
|
||||
<span class="skill-key" :class="getSkillClass(result.skillBuild[String(level)])">
|
||||
{{ formatSkill(result.skillBuild[String(level)]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="warning">Скилл билд не был выбран, сгенерируйте челлендж заново.</div>
|
||||
</div>
|
||||
|
||||
<div v-if="includeAspect">
|
||||
<h3>Aspect</h3>
|
||||
<div v-if="result.aspect">
|
||||
{{ result.aspect }}
|
||||
</div>
|
||||
<div v-else class="warning">Аспект не был выбран, сгенерируйте челлендж заново.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRandomDataStore } from '@/stores/randomData'
|
||||
|
||||
const randomDataStore = useRandomDataStore()
|
||||
const { prefs, result, loading, error, heroes } = storeToRefs(randomDataStore)
|
||||
|
||||
const skillLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 25]
|
||||
|
||||
function formatSkill(skill: string | undefined): string {
|
||||
if (!skill) return '-'
|
||||
const map: Record<string, string> = {
|
||||
q: 'Q',
|
||||
w: 'W',
|
||||
e: 'E',
|
||||
r: 'R',
|
||||
left_talent: 'L',
|
||||
right_talent: 'R'
|
||||
}
|
||||
return map[skill] ?? skill
|
||||
}
|
||||
|
||||
function getSkillClass(skill: string | undefined): string {
|
||||
if (!skill) return ''
|
||||
if (skill === 'r') return 'skill-ult'
|
||||
if (skill === 'left_talent' || skill === 'right_talent') return 'skill-talent'
|
||||
return 'skill-basic'
|
||||
}
|
||||
|
||||
const includeSkills = computed({
|
||||
get: () => prefs.value.includeSkills,
|
||||
set: (val: boolean) => randomDataStore.setPrefs({ includeSkills: val })
|
||||
})
|
||||
|
||||
const includeAspect = computed({
|
||||
get: () => prefs.value.includeAspect,
|
||||
set: (val: boolean) => randomDataStore.setPrefs({ includeAspect: val })
|
||||
})
|
||||
|
||||
const itemsCount = computed({
|
||||
get: () => prefs.value.itemsCount,
|
||||
set: (val: number) => randomDataStore.setPrefs({ itemsCount: val })
|
||||
})
|
||||
|
||||
const selectedHeroId = computed({
|
||||
get: () => prefs.value.heroId ?? null,
|
||||
set: (val: number | null) => randomDataStore.setPrefs({ heroId: val })
|
||||
})
|
||||
|
||||
const canRandomize = computed(() => !loading.value)
|
||||
|
||||
function requestRandomize() {
|
||||
randomDataStore.randomize({
|
||||
includeSkills: includeSkills.value,
|
||||
includeAspect: includeAspect.value,
|
||||
itemsCount: itemsCount.value,
|
||||
heroId: selectedHeroId.value
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
randomDataStore.loadPrefs()
|
||||
randomDataStore.loadHeroes()
|
||||
randomDataStore.randomize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:root {
|
||||
--dota-red: #b63a2b;
|
||||
--dota-red-glow: rgba(182, 58, 43, 0.45);
|
||||
--dota-gold: #c8a86a;
|
||||
--dota-gold-glow: rgba(200, 168, 106, 0.3);
|
||||
--dota-blue: #4fc3f7;
|
||||
--dota-blue-glow: rgba(79, 195, 247, 0.35);
|
||||
--panel: rgba(10, 14, 20, 0.6);
|
||||
}
|
||||
|
||||
/* MAIN BLOCK -------------------------------------------------- */
|
||||
.randomizer {
|
||||
padding: 26px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(20, 25, 40, 0.9), rgba(8, 10, 16, 0.92)),
|
||||
radial-gradient(120% 120% at 0% 0%, rgba(255, 255, 255, 0.05), transparent);
|
||||
color: #e6f0fa;
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow:
|
||||
0 0 18px rgba(0, 0, 0, 0.6),
|
||||
0 0 32px rgba(0, 0, 0, 0.4),
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.02);
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Decorative glow line */
|
||||
.randomizer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top left,
|
||||
rgba(255, 255, 255, 0.10),
|
||||
transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* TITLE -------------------------------------------------- */
|
||||
.randomizer>h2 {
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
font-size: 1.7rem;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 18px;
|
||||
text-shadow: 0 0 6px var(--dota-red-glow);
|
||||
color: var(--dota-gold);
|
||||
}
|
||||
|
||||
/* CONTROLS -------------------------------------------------- */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.95rem;
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.controls label:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 0 14px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.controls select {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: inherit;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* BUTTON -------------------------------------------------- */
|
||||
.controls button {
|
||||
background: linear-gradient(90deg, var(--dota-red), #8b241b);
|
||||
color: #f8e7c6;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
|
||||
box-shadow:
|
||||
0 0 12px var(--dota-red-glow),
|
||||
inset 0 0 6px rgba(0, 0, 0, 0.4);
|
||||
|
||||
transition: transform .12s ease, box-shadow .12s ease, opacity .12s ease;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 0 18px var(--dota-red-glow),
|
||||
inset 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.controls button:active {
|
||||
transform: translateY(1px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.controls button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f87171;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #fbbf24;
|
||||
font-weight: 600;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* RESULT PANEL -------------------------------------------------- */
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 22px;
|
||||
|
||||
background: linear-gradient(180deg, rgba(15, 17, 23, 0.9), rgba(6, 8, 13, 0.9));
|
||||
padding: 18px;
|
||||
border-radius: 14px;
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 18px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* HERO -------------------------------------------------- */
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--dota-blue);
|
||||
text-shadow: 0 0 6px var(--dota-blue-glow);
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0.07),
|
||||
rgba(255, 255, 255, 0.02));
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: var(--dota-gold);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ITEMS -------------------------------------------------- */
|
||||
.result ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result li {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
color: #f2f7ff;
|
||||
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
box-shadow:
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.3),
|
||||
0 0 10px rgba(40, 150, 255, 0.15);
|
||||
|
||||
transition: 0.15s ease;
|
||||
}
|
||||
|
||||
.result li:hover {
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
box-shadow:
|
||||
inset 0 -3px 0 rgba(0, 0, 0, 0.4),
|
||||
0 0 14px rgba(40, 150, 255, 0.22);
|
||||
}
|
||||
|
||||
/* SKILL + ASPECT -------------------------------------------------- */
|
||||
h3 {
|
||||
margin: 8px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--dota-gold);
|
||||
text-shadow: 0 0 4px var(--dota-gold-glow);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* SKILL BUILD -------------------------------------------------- */
|
||||
.skill-build {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.skill-level {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.level-num {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.skill-key {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.skill-basic {
|
||||
color: #8bc34a;
|
||||
}
|
||||
|
||||
.skill-ult {
|
||||
color: #ffd54f;
|
||||
background: rgba(255, 213, 79, 0.15);
|
||||
border-color: rgba(255, 213, 79, 0.3);
|
||||
}
|
||||
|
||||
.skill-talent {
|
||||
color: #4fc3f7;
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
border-color: rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
/* MOBILE -------------------------------------------------- */
|
||||
@media (max-width: 720px) {
|
||||
.result {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
dota-random-builds-front/src/data/aspects.ts
Normal file
7
dota-random-builds-front/src/data/aspects.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const aspects = [
|
||||
'Carry',
|
||||
'Support',
|
||||
'Roamer',
|
||||
'Jungler',
|
||||
'Pusher'
|
||||
]
|
||||
134
dota-random-builds-front/src/data/heroes.ts
Normal file
134
dota-random-builds-front/src/data/heroes.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
export interface Hero {
|
||||
id: string
|
||||
name: string
|
||||
primary: 'Strength' | 'Agility' | 'Intelligence'
|
||||
}
|
||||
|
||||
export const heroes: Hero[] = [
|
||||
{ id: 'abaddon', name: 'Abaddon', primary: 'Strength' },
|
||||
{ id: 'alchemist', name: 'Alchemist', primary: 'Strength' },
|
||||
{ id: 'axe', name: 'Axe', primary: 'Strength' },
|
||||
{ id: 'beastmaster', name: 'Beastmaster', primary: 'Strength' },
|
||||
{ id: 'brewmaster', name: 'Brewmaster', primary: 'Strength' },
|
||||
{ id: 'bristleback', name: 'Bristleback', primary: 'Strength' },
|
||||
{ id: 'centaur', name: 'Centaur Warrunner', primary: 'Strength' },
|
||||
{ id: 'chaos_knight', name: 'Chaos Knight', primary: 'Strength' },
|
||||
{ id: 'clockwerk', name: 'Clockwerk', primary: 'Strength' },
|
||||
{ id: 'dawnbreaker', name: 'Dawnbreaker', primary: 'Strength' },
|
||||
{ id: 'doom', name: 'Doom', primary: 'Strength' },
|
||||
{ id: 'dragon_knight', name: 'Dragon Knight', primary: 'Strength' },
|
||||
{ id: 'earth_spirit', name: 'Earth Spirit', primary: 'Strength' },
|
||||
{ id: 'earthshaker', name: 'Earthshaker', primary: 'Strength' },
|
||||
{ id: 'elder_titan', name: 'Elder Titan', primary: 'Strength' },
|
||||
{ id: 'huskar', name: 'Huskar', primary: 'Strength' },
|
||||
{ id: 'kunkka', name: 'Kunkka', primary: 'Strength' },
|
||||
{ id: 'legion_commander', name: 'Legion Commander', primary: 'Strength' },
|
||||
{ id: 'lifestealer', name: 'Lifestealer', primary: 'Strength' },
|
||||
{ id: 'lycan', name: 'Lycan', primary: 'Strength' },
|
||||
{ id: 'magnus', name: 'Magnus', primary: 'Strength' },
|
||||
{ id: 'mars', name: 'Mars', primary: 'Strength' },
|
||||
{ id: 'night_stalker', name: 'Night Stalker', primary: 'Strength' },
|
||||
{ id: 'ogre_magi', name: 'Ogre Magi', primary: 'Strength' },
|
||||
{ id: 'omniknight', name: 'Omniknight', primary: 'Strength' },
|
||||
{ id: 'phoenix', name: 'Phoenix', primary: 'Strength' },
|
||||
{ id: 'primal_beast', name: 'Primal Beast', primary: 'Strength' },
|
||||
{ id: 'pudge', name: 'Pudge', primary: 'Strength' },
|
||||
{ id: 'slardar', name: 'Slardar', primary: 'Strength' },
|
||||
{ id: 'snapfire', name: 'Snapfire', primary: 'Strength' },
|
||||
{ id: 'spirit_breaker', name: 'Spirit Breaker', primary: 'Strength' },
|
||||
{ id: 'sven', name: 'Sven', primary: 'Strength' },
|
||||
{ id: 'tidehunter', name: 'Tidehunter', primary: 'Strength' },
|
||||
{ id: 'timbersaw', name: 'Timbersaw', primary: 'Strength' },
|
||||
{ id: 'tiny', name: 'Tiny', primary: 'Strength' },
|
||||
{ id: 'treant', name: 'Treant Protector', primary: 'Strength' },
|
||||
{ id: 'tusk', name: 'Tusk', primary: 'Strength' },
|
||||
{ id: 'underlord', name: 'Underlord', primary: 'Strength' },
|
||||
{ id: 'undying', name: 'Undying', primary: 'Strength' },
|
||||
{ id: 'wraith_king', name: 'Wraith King', primary: 'Strength' },
|
||||
|
||||
/* -------------------- Agility -------------------- */
|
||||
|
||||
{ id: 'anti_mage', name: 'Anti-Mage', primary: 'Agility' },
|
||||
{ id: 'arc_warden', name: 'Arc Warden', primary: 'Agility' },
|
||||
{ id: 'bloodseeker', name: 'Bloodseeker', primary: 'Agility' },
|
||||
{ id: 'bounty_hunter', name: 'Bounty Hunter', primary: 'Agility' },
|
||||
{ id: 'broodmother', name: 'Broodmother', primary: 'Agility' },
|
||||
{ id: 'clinkz', name: 'Clinkz', primary: 'Agility' },
|
||||
{ id: 'drow_ranger', name: 'Drow Ranger', primary: 'Agility' },
|
||||
{ id: 'ember_spirit', name: 'Ember Spirit', primary: 'Agility' },
|
||||
{ id: 'faceless_void', name: 'Faceless Void', primary: 'Agility' },
|
||||
{ id: 'gyrocopter', name: 'Gyrocopter', primary: 'Agility' },
|
||||
{ id: 'hoodwink', name: 'Hoodwink', primary: 'Agility' },
|
||||
{ id: 'juggernaut', name: 'Juggernaut', primary: 'Agility' },
|
||||
{ id: 'lone_druid', name: 'Lone Druid', primary: 'Agility' },
|
||||
{ id: 'luna', name: 'Luna', primary: 'Agility' },
|
||||
{ id: 'medusa', name: 'Medusa', primary: 'Agility' },
|
||||
{ id: 'meepo', name: 'Meepo', primary: 'Agility' },
|
||||
{ id: 'mirana', name: 'Mirana', primary: 'Agility' },
|
||||
{ id: 'monkey_king', name: 'Monkey King', primary: 'Agility' },
|
||||
{ id: 'muerta', name: 'Muerta', primary: 'Agility' },
|
||||
{ id: 'naga_siren', name: 'Naga Siren', primary: 'Agility' },
|
||||
{ id: 'nyx_assassin', name: 'Nyx Assassin', primary: 'Agility' },
|
||||
{ id: 'pangolier', name: 'Pangolier', primary: 'Agility' },
|
||||
{ id: 'phantom_assassin', name: 'Phantom Assassin', primary: 'Agility' },
|
||||
{ id: 'phantom_lancer', name: 'Phantom Lancer', primary: 'Agility' },
|
||||
{ id: 'razor', name: 'Razor', primary: 'Agility' },
|
||||
{ id: 'riki', name: 'Riki', primary: 'Agility' },
|
||||
{ id: 'shadow_fiend', name: 'Shadow Fiend', primary: 'Agility' },
|
||||
{ id: 'slark', name: 'Slark', primary: 'Agility' },
|
||||
{ id: 'sniper', name: 'Sniper', primary: 'Agility' },
|
||||
{ id: 'spectre', name: 'Spectre', primary: 'Agility' },
|
||||
{ id: 'templar_assassin', name: 'Templar Assassin', primary: 'Agility' },
|
||||
{ id: 'terrorblade', name: 'Terrorblade', primary: 'Agility' },
|
||||
{ id: 'troll_warlord', name: 'Troll Warlord', primary: 'Agility' },
|
||||
{ id: 'ursa', name: 'Ursa', primary: 'Agility' },
|
||||
{ id: 'viper', name: 'Viper', primary: 'Agility' },
|
||||
{ id: 'weaver', name: 'Weaver', primary: 'Agility' },
|
||||
|
||||
/* -------------------- Intelligence -------------------- */
|
||||
|
||||
{ id: 'ancient_apparition', name: 'Ancient Apparition', primary: 'Intelligence' },
|
||||
{ id: 'bane', name: 'Bane', primary: 'Intelligence' },
|
||||
{ id: 'batrider', name: 'Batrider', primary: 'Intelligence' },
|
||||
{ id: 'chen', name: 'Chen', primary: 'Intelligence' },
|
||||
{ id: 'crystal_maiden', name: 'Crystal Maiden', primary: 'Intelligence' },
|
||||
{ id: 'dark_seer', name: 'Dark Seer', primary: 'Intelligence' },
|
||||
{ id: 'dark_willow', name: 'Dark Willow', primary: 'Intelligence' },
|
||||
{ id: 'dazzle', name: 'Dazzle', primary: 'Intelligence' },
|
||||
{ id: 'death_prophet', name: 'Death Prophet', primary: 'Intelligence' },
|
||||
{ id: 'disruptor', name: 'Disruptor', primary: 'Intelligence' },
|
||||
{ id: 'enchantress', name: 'Enchantress', primary: 'Intelligence' },
|
||||
{ id: 'enigma', name: 'Enigma', primary: 'Intelligence' },
|
||||
{ id: 'grimstroke', name: 'Grimstroke', primary: 'Intelligence' },
|
||||
{ id: 'invoker', name: 'Invoker', primary: 'Intelligence' },
|
||||
{ id: 'jakiro', name: 'Jakiro', primary: 'Intelligence' },
|
||||
{ id: 'keeper_of_the_light', name: 'Keeper of the Light', primary: 'Intelligence' },
|
||||
{ id: 'leshrac', name: 'Leshrac', primary: 'Intelligence' },
|
||||
{ id: 'lich', name: 'Lich', primary: 'Intelligence' },
|
||||
{ id: 'lina', name: 'Lina', primary: 'Intelligence' },
|
||||
{ id: 'lion', name: 'Lion', primary: 'Intelligence' },
|
||||
{ id: 'muerta', name: 'Muerta', primary: 'Intelligence' },
|
||||
{ id: 'nature_prophet', name: "Nature's Prophet", primary: 'Intelligence' },
|
||||
{ id: 'necrophos', name: 'Necrophos', primary: 'Intelligence' },
|
||||
{ id: 'ogre_magi_int', name: 'Ogre Magi (Int)', primary: 'Intelligence' },
|
||||
{ id: 'oracle', name: 'Oracle', primary: 'Intelligence' },
|
||||
{ id: 'outworld_destroyer', name: 'Outworld Destroyer', primary: 'Intelligence' },
|
||||
{ id: 'puck', name: 'Puck', primary: 'Intelligence' },
|
||||
{ id: 'pugna', name: 'Pugna', primary: 'Intelligence' },
|
||||
{ id: 'queen_of_pain', name: 'Queen of Pain', primary: 'Intelligence' },
|
||||
{ id: 'rubick', name: 'Rubick', primary: 'Intelligence' },
|
||||
{ id: 'shadow_demon', name: 'Shadow Demon', primary: 'Intelligence' },
|
||||
{ id: 'shadow_shaman', name: 'Shadow Shaman', primary: 'Intelligence' },
|
||||
{ id: 'silencer', name: 'Silencer', primary: 'Intelligence' },
|
||||
{ id: 'skywrath_mage', name: 'Skywrath Mage', primary: 'Intelligence' },
|
||||
{ id: 'storm_spirit', name: 'Storm Spirit', primary: 'Intelligence' },
|
||||
{ id: 'techies', name: 'Techies', primary: 'Intelligence' },
|
||||
{ id: 'tinker', name: 'Tinker', primary: 'Intelligence' },
|
||||
{ id: 'visage', name: 'Visage', primary: 'Intelligence' },
|
||||
{ id: 'void_spirit', name: 'Void Spirit', primary: 'Intelligence' },
|
||||
{ id: 'warlock', name: 'Warlock', primary: 'Intelligence' },
|
||||
{ id: 'windranger', name: 'Windranger', primary: 'Intelligence' },
|
||||
{ id: 'winter_wyvern', name: 'Winter Wyvern', primary: 'Intelligence' },
|
||||
{ id: 'witch_doctor', name: 'Witch Doctor', primary: 'Intelligence' },
|
||||
{ id: 'zeus', name: 'Zeus', primary: 'Intelligence' }
|
||||
];
|
||||
887
dota-random-builds-front/src/data/items.ts
Normal file
887
dota-random-builds-front/src/data/items.ts
Normal file
@@ -0,0 +1,887 @@
|
||||
export interface Item {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
|
||||
export const items: Item[] = [
|
||||
{
|
||||
"id": "item_town_portal_scroll",
|
||||
"name": "Town Portal Scroll"
|
||||
},
|
||||
{
|
||||
"id": "item_clarity",
|
||||
"name": "Clarity"
|
||||
},
|
||||
{
|
||||
"id": "item_faerie_fire",
|
||||
"name": "Faerie Fire"
|
||||
},
|
||||
{
|
||||
"id": "item_smoke_of_deceit",
|
||||
"name": "Smoke of Deceit"
|
||||
},
|
||||
{
|
||||
"id": "item_observer_ward",
|
||||
"name": "Observer Ward"
|
||||
},
|
||||
{
|
||||
"id": "item_sentry_ward",
|
||||
"name": "Sentry Ward"
|
||||
},
|
||||
{
|
||||
"id": "item_enchanted_mango",
|
||||
"name": "Enchanted Mango"
|
||||
},
|
||||
{
|
||||
"id": "item_healing_salve",
|
||||
"name": "Healing Salve"
|
||||
},
|
||||
{
|
||||
"id": "item_tango",
|
||||
"name": "Tango"
|
||||
},
|
||||
{
|
||||
"id": "item_blood_grenade",
|
||||
"name": "Blood Grenade"
|
||||
},
|
||||
{
|
||||
"id": "item_dust_of_appearance",
|
||||
"name": "Dust of Appearance"
|
||||
},
|
||||
{
|
||||
"id": "item_bottle",
|
||||
"name": "Bottle"
|
||||
},
|
||||
{
|
||||
"id": "item_aghanim_s_shard",
|
||||
"name": "Aghanim's Shard"
|
||||
},
|
||||
{
|
||||
"id": "item_aghanim_s_blessing",
|
||||
"name": "Aghanim's Blessing"
|
||||
},
|
||||
{
|
||||
"id": "item_observer_and_sentry_wards",
|
||||
"name": "Observer and Sentry Wards"
|
||||
},
|
||||
{
|
||||
"id": "item_iron_branch",
|
||||
"name": "Iron Branch"
|
||||
},
|
||||
{
|
||||
"id": "item_gauntlets_of_strength",
|
||||
"name": "Gauntlets of Strength"
|
||||
},
|
||||
{
|
||||
"id": "item_slippers_of_agility",
|
||||
"name": "Slippers of Agility"
|
||||
},
|
||||
{
|
||||
"id": "item_mantle_of_intelligence",
|
||||
"name": "Mantle of Intelligence"
|
||||
},
|
||||
{
|
||||
"id": "item_circlet",
|
||||
"name": "Circlet"
|
||||
},
|
||||
{
|
||||
"id": "item_belt_of_strength",
|
||||
"name": "Belt of Strength"
|
||||
},
|
||||
{
|
||||
"id": "item_band_of_elvenskin",
|
||||
"name": "Band of Elvenskin"
|
||||
},
|
||||
{
|
||||
"id": "item_robe_of_the_magi",
|
||||
"name": "Robe of the Magi"
|
||||
},
|
||||
{
|
||||
"id": "item_crown",
|
||||
"name": "Crown"
|
||||
},
|
||||
{
|
||||
"id": "item_ogre_axe",
|
||||
"name": "Ogre Axe"
|
||||
},
|
||||
{
|
||||
"id": "item_blade_of_alacrity",
|
||||
"name": "Blade of Alacrity"
|
||||
},
|
||||
{
|
||||
"id": "item_staff_of_wizardry",
|
||||
"name": "Staff of Wizardry"
|
||||
},
|
||||
{
|
||||
"id": "item_diadem",
|
||||
"name": "Diadem"
|
||||
},
|
||||
{
|
||||
"id": "item_quelling_blade",
|
||||
"name": "Quelling Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_ring_of_protection",
|
||||
"name": "Ring of Protection"
|
||||
},
|
||||
{
|
||||
"id": "item_infused_raindrops",
|
||||
"name": "Infused Raindrops"
|
||||
},
|
||||
{
|
||||
"id": "item_orb_of_venom",
|
||||
"name": "Orb of Venom"
|
||||
},
|
||||
{
|
||||
"id": "item_orb_of_blight",
|
||||
"name": "Orb of Blight"
|
||||
},
|
||||
{
|
||||
"id": "item_blades_of_attack",
|
||||
"name": "Blades of Attack"
|
||||
},
|
||||
{
|
||||
"id": "item_orb_of_frost",
|
||||
"name": "Orb of Frost"
|
||||
},
|
||||
{
|
||||
"id": "item_gloves_of_haste",
|
||||
"name": "Gloves of Haste"
|
||||
},
|
||||
{
|
||||
"id": "item_chainmail",
|
||||
"name": "Chainmail"
|
||||
},
|
||||
{
|
||||
"id": "item_helm_of_iron_will",
|
||||
"name": "Helm of Iron Will"
|
||||
},
|
||||
{
|
||||
"id": "item_broadsword",
|
||||
"name": "Broadsword"
|
||||
},
|
||||
{
|
||||
"id": "item_blitz_knuckles",
|
||||
"name": "Blitz Knuckles"
|
||||
},
|
||||
{
|
||||
"id": "item_javelin",
|
||||
"name": "Javelin"
|
||||
},
|
||||
{
|
||||
"id": "item_claymore",
|
||||
"name": "Claymore"
|
||||
},
|
||||
{
|
||||
"id": "item_mithril_hammer",
|
||||
"name": "Mithril Hammer"
|
||||
},
|
||||
{
|
||||
"id": "item_ring_of_regen",
|
||||
"name": "Ring of Regen"
|
||||
},
|
||||
{
|
||||
"id": "item_sage_s_mask",
|
||||
"name": "Sage's Mask"
|
||||
},
|
||||
{
|
||||
"id": "item_magic_stick",
|
||||
"name": "Magic Stick"
|
||||
},
|
||||
{
|
||||
"id": "item_fluffy_hat",
|
||||
"name": "Fluffy Hat"
|
||||
},
|
||||
{
|
||||
"id": "item_wind_lace",
|
||||
"name": "Wind Lace"
|
||||
},
|
||||
{
|
||||
"id": "item_cloak",
|
||||
"name": "Cloak"
|
||||
},
|
||||
{
|
||||
"id": "item_boots_of_speed",
|
||||
"name": "Boots of Speed"
|
||||
},
|
||||
{
|
||||
"id": "item_gem_of_true_sight",
|
||||
"name": "Gem of True Sight"
|
||||
},
|
||||
{
|
||||
"id": "item_morbid_mask",
|
||||
"name": "Morbid Mask"
|
||||
},
|
||||
{
|
||||
"id": "item_voodoo_mask",
|
||||
"name": "Voodoo Mask"
|
||||
},
|
||||
{
|
||||
"id": "item_shadow_amulet",
|
||||
"name": "Shadow Amulet"
|
||||
},
|
||||
{
|
||||
"id": "item_ghost_scepter",
|
||||
"name": "Ghost Scepter"
|
||||
},
|
||||
{
|
||||
"id": "item_blink_dagger",
|
||||
"name": "Blink Dagger"
|
||||
},
|
||||
{
|
||||
"id": "item_ring_of_health",
|
||||
"name": "Ring of Health"
|
||||
},
|
||||
{
|
||||
"id": "item_void_stone",
|
||||
"name": "Void Stone"
|
||||
},
|
||||
{
|
||||
"id": "item_magic_wand",
|
||||
"name": "Magic Wand"
|
||||
},
|
||||
{
|
||||
"id": "item_null_talisman",
|
||||
"name": "Null Talisman"
|
||||
},
|
||||
{
|
||||
"id": "item_wraith_band",
|
||||
"name": "Wraith Band"
|
||||
},
|
||||
{
|
||||
"id": "item_bracer",
|
||||
"name": "Bracer"
|
||||
},
|
||||
{
|
||||
"id": "item_soul_ring",
|
||||
"name": "Soul Ring"
|
||||
},
|
||||
{
|
||||
"id": "item_orb_of_corrosion",
|
||||
"name": "Orb of Corrosion"
|
||||
},
|
||||
{
|
||||
"id": "item_falcon_blade",
|
||||
"name": "Falcon Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_power_treads",
|
||||
"name": "Power Treads"
|
||||
},
|
||||
{
|
||||
"id": "item_phase_boots",
|
||||
"name": "Phase Boots"
|
||||
},
|
||||
{
|
||||
"id": "item_oblivion_staff",
|
||||
"name": "Oblivion Staff"
|
||||
},
|
||||
{
|
||||
"id": "item_perseverance",
|
||||
"name": "Perseverance"
|
||||
},
|
||||
{
|
||||
"id": "item_mask_of_madness",
|
||||
"name": "Mask of Madness"
|
||||
},
|
||||
{
|
||||
"id": "item_hand_of_midas",
|
||||
"name": "Hand of Midas"
|
||||
},
|
||||
{
|
||||
"id": "item_helm_of_the_dominator",
|
||||
"name": "Helm of the Dominator"
|
||||
},
|
||||
{
|
||||
"id": "item_boots_of_travel",
|
||||
"name": "Boots of Travel"
|
||||
},
|
||||
{
|
||||
"id": "item_moon_shard",
|
||||
"name": "Moon Shard"
|
||||
},
|
||||
{
|
||||
"id": "item_boots_of_travel_2",
|
||||
"name": "Boots of Travel 2"
|
||||
},
|
||||
{
|
||||
"id": "item_buckler",
|
||||
"name": "Buckler"
|
||||
},
|
||||
{
|
||||
"id": "item_ring_of_basilius",
|
||||
"name": "Ring of Basilius"
|
||||
},
|
||||
{
|
||||
"id": "item_headdress",
|
||||
"name": "Headdress"
|
||||
},
|
||||
{
|
||||
"id": "item_urn_of_shadows",
|
||||
"name": "Urn of Shadows"
|
||||
},
|
||||
{
|
||||
"id": "item_tranquil_boots",
|
||||
"name": "Tranquil Boots"
|
||||
},
|
||||
{
|
||||
"id": "item_pavise",
|
||||
"name": "Pavise"
|
||||
},
|
||||
{
|
||||
"id": "item_arcane_boots",
|
||||
"name": "Arcane Boots"
|
||||
},
|
||||
{
|
||||
"id": "item_drum_of_endurance",
|
||||
"name": "Drum of Endurance"
|
||||
},
|
||||
{
|
||||
"id": "item_mekansm",
|
||||
"name": "Mekansm"
|
||||
},
|
||||
{
|
||||
"id": "item_holy_locket",
|
||||
"name": "Holy Locket"
|
||||
},
|
||||
{
|
||||
"id": "item_vladmir_s_offering",
|
||||
"name": "Vladmir's Offering"
|
||||
},
|
||||
{
|
||||
"id": "item_spirit_vessel",
|
||||
"name": "Spirit Vessel"
|
||||
},
|
||||
{
|
||||
"id": "item_pipe_of_insight",
|
||||
"name": "Pipe of Insight"
|
||||
},
|
||||
{
|
||||
"id": "item_guardian_greaves",
|
||||
"name": "Guardian Greaves"
|
||||
},
|
||||
{
|
||||
"id": "item_boots_of_bearing",
|
||||
"name": "Boots of Bearing"
|
||||
},
|
||||
{
|
||||
"id": "item_parasma",
|
||||
"name": "Parasma"
|
||||
},
|
||||
{
|
||||
"id": "item_veil_of_discord",
|
||||
"name": "Veil of Discord"
|
||||
},
|
||||
{
|
||||
"id": "item_glimmer_cape",
|
||||
"name": "Glimmer Cape"
|
||||
},
|
||||
{
|
||||
"id": "item_force_staff",
|
||||
"name": "Force Staff"
|
||||
},
|
||||
{
|
||||
"id": "item_aether_lens",
|
||||
"name": "Aether Lens"
|
||||
},
|
||||
{
|
||||
"id": "item_witch_blade",
|
||||
"name": "Witch Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_eul_s_scepter_of_divinity",
|
||||
"name": "Eul's Scepter of Divinity"
|
||||
},
|
||||
{
|
||||
"id": "item_rod_of_atos",
|
||||
"name": "Rod of Atos"
|
||||
},
|
||||
{
|
||||
"id": "item_dagon",
|
||||
"name": "Dagon"
|
||||
},
|
||||
{
|
||||
"id": "item_orchid_malevolence",
|
||||
"name": "Orchid Malevolence"
|
||||
},
|
||||
{
|
||||
"id": "item_solar_crest",
|
||||
"name": "Solar Crest"
|
||||
},
|
||||
{
|
||||
"id": "item_aghanim_s_scepter",
|
||||
"name": "Aghanim's Scepter"
|
||||
},
|
||||
{
|
||||
"id": "item_refresher_orb",
|
||||
"name": "Refresher Orb"
|
||||
},
|
||||
{
|
||||
"id": "item_octarine_core",
|
||||
"name": "Octarine Core"
|
||||
},
|
||||
{
|
||||
"id": "item_scythe_of_vyse",
|
||||
"name": "Scythe of Vyse"
|
||||
},
|
||||
{
|
||||
"id": "item_gleipnir",
|
||||
"name": "Gleipnir"
|
||||
},
|
||||
{
|
||||
"id": "item_wind_waker",
|
||||
"name": "Wind Waker"
|
||||
},
|
||||
{
|
||||
"id": "item_crystalys",
|
||||
"name": "Crystalys"
|
||||
},
|
||||
{
|
||||
"id": "item_meteor_hammer",
|
||||
"name": "Meteor Hammer"
|
||||
},
|
||||
{
|
||||
"id": "item_armlet_of_mordiggian",
|
||||
"name": "Armlet of Mordiggian"
|
||||
},
|
||||
{
|
||||
"id": "item_skull_basher",
|
||||
"name": "Skull Basher"
|
||||
},
|
||||
{
|
||||
"id": "item_shadow_blade",
|
||||
"name": "Shadow Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_desolator",
|
||||
"name": "Desolator"
|
||||
},
|
||||
{
|
||||
"id": "item_battle_fury",
|
||||
"name": "Battle Fury"
|
||||
},
|
||||
{
|
||||
"id": "item_ethereal_blade",
|
||||
"name": "Ethereal Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_nullifier",
|
||||
"name": "Nullifier"
|
||||
},
|
||||
{
|
||||
"id": "item_monkey_king_bar",
|
||||
"name": "Monkey King Bar"
|
||||
},
|
||||
{
|
||||
"id": "item_butterfly",
|
||||
"name": "Butterfly"
|
||||
},
|
||||
{
|
||||
"id": "item_radiance",
|
||||
"name": "Radiance"
|
||||
},
|
||||
{
|
||||
"id": "item_daedalus",
|
||||
"name": "Daedalus"
|
||||
},
|
||||
{
|
||||
"id": "item_silver_edge",
|
||||
"name": "Silver Edge"
|
||||
},
|
||||
{
|
||||
"id": "item_divine_rapier",
|
||||
"name": "Divine Rapier"
|
||||
},
|
||||
{
|
||||
"id": "item_bloodthorn",
|
||||
"name": "Bloodthorn"
|
||||
},
|
||||
{
|
||||
"id": "item_abyssal_blade",
|
||||
"name": "Abyssal Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_revenant_s_brooch",
|
||||
"name": "Revenant's Brooch"
|
||||
},
|
||||
{
|
||||
"id": "item_disperser",
|
||||
"name": "Disperser"
|
||||
},
|
||||
{
|
||||
"id": "item_khanda",
|
||||
"name": "Khanda"
|
||||
},
|
||||
{
|
||||
"id": "item_vanguard",
|
||||
"name": "Vanguard"
|
||||
},
|
||||
{
|
||||
"id": "item_blade_mail",
|
||||
"name": "Blade Mail"
|
||||
},
|
||||
{
|
||||
"id": "item_aeon_disk",
|
||||
"name": "Aeon Disk"
|
||||
},
|
||||
{
|
||||
"id": "item_soul_booster",
|
||||
"name": "Soul Booster"
|
||||
},
|
||||
{
|
||||
"id": "item_crimson_guard",
|
||||
"name": "Crimson Guard"
|
||||
},
|
||||
{
|
||||
"id": "item_lotus_orb",
|
||||
"name": "Lotus Orb"
|
||||
},
|
||||
{
|
||||
"id": "item_black_king_bar",
|
||||
"name": "Black King Bar"
|
||||
},
|
||||
{
|
||||
"id": "item_hurricane_pike",
|
||||
"name": "Hurricane Pike"
|
||||
},
|
||||
{
|
||||
"id": "item_manta_style",
|
||||
"name": "Manta Style"
|
||||
},
|
||||
{
|
||||
"id": "item_linken_s_sphere",
|
||||
"name": "Linken's Sphere"
|
||||
},
|
||||
{
|
||||
"id": "item_shiva_s_guard",
|
||||
"name": "Shiva's Guard"
|
||||
},
|
||||
{
|
||||
"id": "item_heart_of_tarrasque",
|
||||
"name": "Heart of Tarrasque"
|
||||
},
|
||||
{
|
||||
"id": "item_assault_cuirass",
|
||||
"name": "Assault Cuirass"
|
||||
},
|
||||
{
|
||||
"id": "item_bloodstone",
|
||||
"name": "Bloodstone"
|
||||
},
|
||||
{
|
||||
"id": "item_helm_of_the_overlord",
|
||||
"name": "Helm of the Overlord"
|
||||
},
|
||||
{
|
||||
"id": "item_eternal_shroud",
|
||||
"name": "Eternal Shroud"
|
||||
},
|
||||
{
|
||||
"id": "item_dragon_lance",
|
||||
"name": "Dragon Lance"
|
||||
},
|
||||
{
|
||||
"id": "item_sange",
|
||||
"name": "Sange"
|
||||
},
|
||||
{
|
||||
"id": "item_yasha",
|
||||
"name": "Yasha"
|
||||
},
|
||||
{
|
||||
"id": "item_kaya",
|
||||
"name": "Kaya"
|
||||
},
|
||||
{
|
||||
"id": "item_echo_sabre",
|
||||
"name": "Echo Sabre"
|
||||
},
|
||||
{
|
||||
"id": "item_maelstrom",
|
||||
"name": "Maelstrom"
|
||||
},
|
||||
{
|
||||
"id": "item_diffusal_blade",
|
||||
"name": "Diffusal Blade"
|
||||
},
|
||||
{
|
||||
"id": "item_mage_slayer",
|
||||
"name": "Mage Slayer"
|
||||
},
|
||||
{
|
||||
"id": "item_phylactery",
|
||||
"name": "Phylactery"
|
||||
},
|
||||
{
|
||||
"id": "item_heaven_s_halberd",
|
||||
"name": "Heaven's Halberd"
|
||||
},
|
||||
{
|
||||
"id": "item_kaya_and_sange",
|
||||
"name": "Kaya and Sange"
|
||||
},
|
||||
{
|
||||
"id": "item_sange_and_yasha",
|
||||
"name": "Sange and Yasha"
|
||||
},
|
||||
{
|
||||
"id": "item_yasha_and_kaya",
|
||||
"name": "Yasha and Kaya"
|
||||
},
|
||||
{
|
||||
"id": "item_satanic",
|
||||
"name": "Satanic"
|
||||
},
|
||||
{
|
||||
"id": "item_eye_of_skadi",
|
||||
"name": "Eye of Skadi"
|
||||
},
|
||||
{
|
||||
"id": "item_mjollnir",
|
||||
"name": "Mjollnir"
|
||||
},
|
||||
{
|
||||
"id": "item_overwhelming_blink",
|
||||
"name": "Overwhelming Blink"
|
||||
},
|
||||
{
|
||||
"id": "item_swift_blink",
|
||||
"name": "Swift Blink"
|
||||
},
|
||||
{
|
||||
"id": "item_arcane_blink",
|
||||
"name": "Arcane Blink"
|
||||
},
|
||||
{
|
||||
"id": "item_harpoon",
|
||||
"name": "Harpoon"
|
||||
},
|
||||
{
|
||||
"id": "item_ring_of_tarrasque",
|
||||
"name": "Ring of Tarrasque"
|
||||
},
|
||||
{
|
||||
"id": "item_tiara_of_selemene",
|
||||
"name": "Tiara of Selemene"
|
||||
},
|
||||
{
|
||||
"id": "item_cornucopia",
|
||||
"name": "Cornucopia"
|
||||
},
|
||||
{
|
||||
"id": "item_energy_booster",
|
||||
"name": "Energy Booster"
|
||||
},
|
||||
{
|
||||
"id": "item_vitality_booster",
|
||||
"name": "Vitality Booster"
|
||||
},
|
||||
{
|
||||
"id": "item_point_booster",
|
||||
"name": "Point Booster"
|
||||
},
|
||||
{
|
||||
"id": "item_talisman_of_evasion",
|
||||
"name": "Talisman of Evasion"
|
||||
},
|
||||
{
|
||||
"id": "item_platemail",
|
||||
"name": "Platemail"
|
||||
},
|
||||
{
|
||||
"id": "item_hyperstone",
|
||||
"name": "Hyperstone"
|
||||
},
|
||||
{
|
||||
"id": "item_ultimate_orb",
|
||||
"name": "Ultimate Orb"
|
||||
},
|
||||
{
|
||||
"id": "item_demon_edge",
|
||||
"name": "Demon Edge"
|
||||
},
|
||||
{
|
||||
"id": "item_mystic_staff",
|
||||
"name": "Mystic Staff"
|
||||
},
|
||||
{
|
||||
"id": "item_reaver",
|
||||
"name": "Reaver"
|
||||
},
|
||||
{
|
||||
"id": "item_eaglesong",
|
||||
"name": "Eaglesong"
|
||||
},
|
||||
{
|
||||
"id": "item_sacred_relic",
|
||||
"name": "Sacred Relic"
|
||||
}
|
||||
]
|
||||
|
||||
// export const items: Item[] = [
|
||||
// { id: 'item_magic_wand', name: 'Magic Wand' },
|
||||
// { id: 'item_bracer', name: 'Bracer' },
|
||||
// { id: 'item_wraith_band', name: 'Wraith Band' },
|
||||
// { id: 'item_null_talisman', name: 'Null Talisman' },
|
||||
// { id: 'item_soul_ring', name: 'Soul Ring' },
|
||||
// { id: 'item_falcon_blade', name: 'Falcon Blade' },
|
||||
|
||||
// // Boots
|
||||
// { id: 'item_phase_boots', name: 'Phase Boots' },
|
||||
// { id: 'item_power_treads', name: 'Power Treads' },
|
||||
// { id: 'item_arcane_boots', name: 'Arcane Boots' },
|
||||
// { id: 'item_tranquil_boots', name: 'Tranquil Boots' },
|
||||
// { id: 'item_boots_of_bearing', name: 'Boots of Bearing' },
|
||||
// { id: 'item_guardian_greaves', name: 'Guardian Greaves' },
|
||||
|
||||
// // Mobility
|
||||
// { id: 'item_blink', name: 'Blink Dagger' },
|
||||
// { id: 'item_force_staff', name: 'Force Staff' },
|
||||
// { id: 'item_hurricane_pike', name: 'Hurricane Pike' },
|
||||
// { id: 'item_overwhelming_blink', name: 'Overwhelming Blink' },
|
||||
// { id: 'item_swift_blink', name: 'Swift Blink' },
|
||||
// { id: 'item_arcane_blink', name: 'Arcane Blink' },
|
||||
|
||||
// // Armor / regen aura
|
||||
// { id: 'item_mekansm', name: 'Mekansm' },
|
||||
// { id: 'item_pipe', name: 'Pipe of Insight' },
|
||||
// { id: 'item_vladmir', name: 'Vladmir’s Offering' },
|
||||
// { id: 'item_solar_crest', name: 'Solar Crest' },
|
||||
// { id: 'item_spirit_vessel', name: 'Spirit Vessel' },
|
||||
|
||||
// // Simple DPS items
|
||||
// { id: 'item_mask_of_madness', name: 'Mask of Madness' },
|
||||
// { id: 'item_armlet', name: 'Armlet of Mordiggian' },
|
||||
// { id: 'item_echo_sabre', name: 'Echo Sabre' },
|
||||
// { id: 'item_skull_basher', name: 'Skull Basher' },
|
||||
// { id: 'item_desolator', name: 'Desolator' },
|
||||
// { id: 'item_silver_edge', name: 'Silver Edge' },
|
||||
|
||||
// // Magic items
|
||||
// { id: 'item_veil_of_discord', name: 'Veil of Discord' },
|
||||
// { id: 'item_aether_lens', name: 'Aether Lens' },
|
||||
// { id: 'item_kaya', name: 'Kaya' },
|
||||
// { id: 'item_rod_of_atos', name: 'Rod of Atos' },
|
||||
// { id: 'item_dagon', name: 'Dagon' },
|
||||
|
||||
// // HP items
|
||||
// { id: 'item_hood_of_defiance', name: 'Hood of Defiance' },
|
||||
// { id: 'item_heart', name: 'Heart of Tarrasque' },
|
||||
// // Strong DPS items
|
||||
// { id: 'item_monkey_king_bar', name: 'Monkey King Bar' },
|
||||
// { id: 'item_butterfly', name: 'Butterfly' },
|
||||
// { id: 'item_daedalus', name: 'Daedalus' },
|
||||
// { id: 'item_greater_crit', name: 'Greater Crit' },
|
||||
// { id: 'item_bfury', name: 'Battle Fury' },
|
||||
// { id: 'item_satanic', name: 'Satanic' },
|
||||
// { id: 'item_mjollnir', name: 'Mjollnir' },
|
||||
// { id: 'item_radiance', name: 'Radiance' },
|
||||
// { id: 'item_diffusal_blade', name: 'Diffusal Blade' },
|
||||
// { id: 'item_abissal_blade', name: 'Abyssal Blade' },
|
||||
// { id: 'item_maelstrom', name: 'Maelstrom' },
|
||||
// { id: 'item_mage_slayer', name: 'Mage Slayer' },
|
||||
|
||||
// // Tank / defensive items
|
||||
// { id: 'item_black_king_bar', name: 'Black King Bar' },
|
||||
// { id: 'item_shivas_guard', name: 'Shiva’s Guard' },
|
||||
// { id: 'item_blade_mail', name: 'Blade Mail' },
|
||||
// { id: 'item_lotus_orb', name: 'Lotus Orb' },
|
||||
// { id: 'item_vanguard', name: 'Vanguard' },
|
||||
// { id: 'item_pipe', name: 'Pipe of Insight' },
|
||||
// { id: 'item_eternal_shroud', name: 'Eternal Shroud' },
|
||||
// { id: 'item_crimson_guard', name: 'Crimson Guard' },
|
||||
// { id: 'item_heavens_halberd', name: 'Heaven’s Halberd' },
|
||||
|
||||
// // Mobility / utility
|
||||
// { id: 'item_cyclone', name: 'Eul’s Scepter of Divinity' },
|
||||
// { id: 'item_glimmer_cape', name: 'Glimmer Cape' },
|
||||
// { id: 'item_ghost', name: 'Ghost Scepter' },
|
||||
// { id: 'item_ethereal_blade', name: 'Ethereal Blade' },
|
||||
// { id: 'item_aeon_disk', name: 'Aeon Disk' },
|
||||
// { id: 'item_boots_of_bearing', name: 'Boots of Bearing' },
|
||||
// { id: 'item_rod_of_atos', name: 'Rod of Atos' },
|
||||
// { id: 'item_solar_crest', name: 'Solar Crest' },
|
||||
|
||||
// // Healing / support items
|
||||
// { id: 'item_guardian_greaves', name: 'Guardian Greaves' },
|
||||
// { id: 'item_holy_locket', name: 'Holy Locket' },
|
||||
// { id: 'item_arcane_boots', name: 'Arcane Boots' },
|
||||
// { id: 'item_spirit_vessel', name: 'Spirit Vessel' },
|
||||
// { id: 'item_medallion_of_courage', name: 'Medallion of Courage' },
|
||||
|
||||
// // Magical burst / caster items
|
||||
// { id: 'item_kaya', name: 'Kaya' },
|
||||
// { id: 'item_kaya_and_sange', name: 'Kaya and Sange' },
|
||||
// { id: 'item_yasha_and_kaya', name: 'Yasha and Kaya' },
|
||||
// { id: 'item_sange_and_yasha', name: 'Sange and Yasha' },
|
||||
// { id: 'item_octarine_core', name: 'Octarine Core' },
|
||||
// { id: 'item_scythe_of_vyse', name: 'Scythe of Vyse' },
|
||||
// { id: 'item_dagon', name: 'Dagon' },
|
||||
|
||||
// { id: 'item_rapier', name: 'Divine Rapier' },
|
||||
// { id: 'item_moon_shard', name: 'Moon Shard' },
|
||||
// { id: 'item_silver_edge', name: 'Silver Edge' },
|
||||
// { id: 'item_bloodthorn', name: 'Bloodthorn' },
|
||||
// { id: 'item_nullifier', name: 'Nullifier' },
|
||||
|
||||
// // Intelligence & spellpower
|
||||
// { id: 'item_refresher', name: 'Refresher Orb' },
|
||||
// { id: 'item_aghanims_scepter', name: 'Aghanim’s Scepter' },
|
||||
// { id: 'item_aghanims_shard', name: 'Aghanim’s Shard' },
|
||||
// { id: 'item_witch_blade', name: 'Witch Blade' },
|
||||
// { id: 'item_gungir', name: 'Gleipnir' },
|
||||
|
||||
// // Tank / sustain late game
|
||||
// { id: 'item_heart', name: 'Heart of Tarrasque' },
|
||||
// { id: 'item_assault', name: 'Assault Cuirass' },
|
||||
// { id: 'item_satanic', name: 'Satanic' },
|
||||
// { id: 'item_harpoon', name: 'Harpoon' },
|
||||
|
||||
// // Universal top-tier
|
||||
// { id: 'item_sphere', name: 'Linken’s Sphere' },
|
||||
// { id: 'item_skadi', name: 'Eye of Skadi' },
|
||||
// { id: 'item_manta', name: 'Manta Style' },
|
||||
// { id: 'item_overwhelming_blink', name: 'Overwhelming Blink' },
|
||||
// { id: 'item_swift_blink', name: 'Swift Blink' },
|
||||
|
||||
// // Summon & utility
|
||||
// { id: 'item_necronomicon', name: 'Necronomicon' },
|
||||
// { id: 'item_drum', name: 'Drum of Endurance' },
|
||||
// { id: 'item_helm_of_the_overlord', name: 'Helm of the Overlord' },
|
||||
|
||||
// // Roshan rewards
|
||||
// { id: 'item_aegis', name: 'Aegis of the Immortal' },
|
||||
// { id: 'item_cheese', name: 'Cheese' },
|
||||
// { id: 'item_refresher_shard', name: 'Refresher Shard' },
|
||||
// { id: 'item_aghanims_blessing', name: 'Aghanim’s Blessing' },
|
||||
|
||||
// // Neutral items — Tier 1
|
||||
// { id: 'item_arcane_ring', name: 'Arcane Ring' },
|
||||
// { id: 'item_faded_broach', name: 'Faded Broach' },
|
||||
// { id: 'item_keen_optic', name: 'Keen Optic' },
|
||||
|
||||
// // Tier 2
|
||||
// { id: 'item_grove_bow', name: 'Grove Bow' },
|
||||
// { id: 'item_pupils_gift', name: 'Pupil’s Gift' },
|
||||
// { id: 'item_philosophers_stone', name: 'Philosopher’s Stone' },
|
||||
|
||||
// // Tier 3
|
||||
// { id: 'item_paladin_sword', name: 'Paladin Sword' },
|
||||
// { id: 'item_quickening_charm', name: 'Quickening Charm' },
|
||||
// { id: 'item_spider_legs', name: 'Spider Legs' },
|
||||
|
||||
// // Tier 4
|
||||
// { id: 'item_spell_prism', name: 'Spell Prism' },
|
||||
// { id: 'item_timeless_relic', name: 'Timeless Relic' },
|
||||
// { id: 'item_mind_breaker', name: 'Mind Breaker' },
|
||||
|
||||
// // Tier 5
|
||||
// { id: 'item_apex', name: 'Apex' },
|
||||
// { id: 'item_ex_machina', name: 'Ex Machina' },
|
||||
// { id: 'item_mirror_shield', name: 'Mirror Shield' },
|
||||
// { id: 'item_book_of_shadows', name: 'Book of Shadows' },
|
||||
// { id: 'item_seer_stone', name: 'Seer Stone' }
|
||||
// ];
|
||||
|
||||
|
||||
12
dota-random-builds-front/src/data/skills.ts
Normal file
12
dota-random-builds-front/src/data/skills.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface SkillBuildOption {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const skillBuilds: SkillBuildOption[] = [
|
||||
{ id: 'q_first', name: 'Max Q first', description: 'Prioritize first ability' },
|
||||
{ id: 'w_first', name: 'Max W first', description: 'Prioritize second ability' },
|
||||
{ id: 'e_first', name: 'Max E first', description: 'Prioritize third ability' },
|
||||
{ id: 'balanced', name: 'Balanced', description: 'Evenly distribute points' }
|
||||
]
|
||||
13
dota-random-builds-front/src/main.ts
Normal file
13
dota-random-builds-front/src/main.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import "@/assets/main.css"
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
306
dota-random-builds-front/src/pages/BuildOfTheDayPage.vue
Normal file
306
dota-random-builds-front/src/pages/BuildOfTheDayPage.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div class="build-of-day">
|
||||
<h2>Build of the Day</h2>
|
||||
|
||||
<p v-if="loading" class="loading">Loading...</p>
|
||||
<p v-if="error" class="error">Failed to load: {{ error }}</p>
|
||||
|
||||
<div v-if="build" class="result">
|
||||
<div class="date-badge">{{ formattedDate }}</div>
|
||||
|
||||
<div class="hero-section">
|
||||
<h3>Hero</h3>
|
||||
<div class="hero">{{ build.hero.name }}</div>
|
||||
<div class="hero-badge">{{ build.hero.primary }}</div>
|
||||
</div>
|
||||
|
||||
<div class="build-section">
|
||||
<h3>Items</h3>
|
||||
<ul>
|
||||
<li v-for="it in build.items" :key="it.id">{{ it.name }}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Skill Build</h3>
|
||||
<div class="skill-build">
|
||||
<div v-for="level in skillLevels" :key="level" class="skill-level">
|
||||
<span class="level-num">{{ level }}</span>
|
||||
<span class="skill-key" :class="getSkillClass(build.skillBuild[String(level)])">
|
||||
{{ formatSkill(build.skillBuild[String(level)]) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="build.aspect">
|
||||
<h3>Aspect</h3>
|
||||
<div class="aspect">{{ build.aspect }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRandomDataStore } from '@/stores/randomData'
|
||||
|
||||
const randomDataStore = useRandomDataStore()
|
||||
const { buildOfDay: build, buildOfDayLoading: loading, buildOfDayError: error } = storeToRefs(randomDataStore)
|
||||
|
||||
const skillLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 25]
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
if (!build.value?.date) return ''
|
||||
const d = new Date(build.value.date)
|
||||
return d.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
function formatSkill(skill: string | undefined): string {
|
||||
if (!skill) return '-'
|
||||
const map: Record<string, string> = {
|
||||
q: 'Q',
|
||||
w: 'W',
|
||||
e: 'E',
|
||||
r: 'R',
|
||||
left_talent: 'L',
|
||||
right_talent: 'R'
|
||||
}
|
||||
return map[skill] ?? skill
|
||||
}
|
||||
|
||||
function getSkillClass(skill: string | undefined): string {
|
||||
if (!skill) return ''
|
||||
if (skill === 'r') return 'skill-ult'
|
||||
if (skill === 'left_talent' || skill === 'right_talent') return 'skill-talent'
|
||||
return 'skill-basic'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
randomDataStore.fetchBuildOfDay()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:root {
|
||||
--dota-red: #b63a2b;
|
||||
--dota-red-glow: rgba(182, 58, 43, 0.45);
|
||||
--dota-gold: #c8a86a;
|
||||
--dota-gold-glow: rgba(200, 168, 106, 0.3);
|
||||
--dota-blue: #4fc3f7;
|
||||
--dota-blue-glow: rgba(79, 195, 247, 0.35);
|
||||
--panel: rgba(10, 14, 20, 0.6);
|
||||
}
|
||||
|
||||
/* MAIN BLOCK -------------------------------------------------- */
|
||||
.build-of-day {
|
||||
padding: 26px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(20, 25, 40, 0.9), rgba(8, 10, 16, 0.92)),
|
||||
radial-gradient(120% 120% at 0% 0%, rgba(255, 255, 255, 0.05), transparent);
|
||||
color: #e6f0fa;
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow:
|
||||
0 0 18px rgba(0, 0, 0, 0.6),
|
||||
0 0 32px rgba(0, 0, 0, 0.4),
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.02);
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Decorative glow line */
|
||||
.build-of-day::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top left,
|
||||
rgba(255, 255, 255, 0.10),
|
||||
transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* TITLE -------------------------------------------------- */
|
||||
.build-of-day > h2 {
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
font-size: 1.7rem;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 18px;
|
||||
text-shadow: 0 0 6px var(--dota-red-glow);
|
||||
color: var(--dota-gold);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: var(--dota-blue);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f87171;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* DATE BADGE -------------------------------------------------- */
|
||||
.date-badge {
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--dota-gold);
|
||||
margin-bottom: 18px;
|
||||
padding: 10px 20px;
|
||||
background: rgba(200, 168, 106, 0.1);
|
||||
border: 1px solid rgba(200, 168, 106, 0.3);
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* RESULT PANEL -------------------------------------------------- */
|
||||
.result {
|
||||
margin-top: 12px;
|
||||
background: linear-gradient(180deg, rgba(15, 17, 23, 0.9), rgba(6, 8, 13, 0.9));
|
||||
padding: 18px;
|
||||
border-radius: 14px;
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 18px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* HERO -------------------------------------------------- */
|
||||
.hero {
|
||||
font-weight: 900;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 8px;
|
||||
color: var(--dota-blue);
|
||||
text-shadow: 0 0 6px var(--dota-blue-glow);
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0.07),
|
||||
rgba(255, 255, 255, 0.02));
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: var(--dota-gold);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ITEMS -------------------------------------------------- */
|
||||
.result ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result li {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
color: #f2f7ff;
|
||||
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
box-shadow:
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.3),
|
||||
0 0 10px rgba(40, 150, 255, 0.15);
|
||||
|
||||
transition: 0.15s ease;
|
||||
}
|
||||
|
||||
.result li:hover {
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
box-shadow:
|
||||
inset 0 -3px 0 rgba(0, 0, 0, 0.4),
|
||||
0 0 14px rgba(40, 150, 255, 0.22);
|
||||
}
|
||||
|
||||
/* SKILL + ASPECT -------------------------------------------------- */
|
||||
h3 {
|
||||
margin: 8px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--dota-gold);
|
||||
text-shadow: 0 0 4px var(--dota-gold-glow);
|
||||
}
|
||||
|
||||
.aspect {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
color: #f2f7ff;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* SKILL BUILD -------------------------------------------------- */
|
||||
.skill-build {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.skill-level {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.level-num {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.skill-key {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.skill-basic {
|
||||
color: #8bc34a;
|
||||
}
|
||||
|
||||
.skill-ult {
|
||||
color: #ffd54f;
|
||||
background: rgba(255, 213, 79, 0.15);
|
||||
border-color: rgba(255, 213, 79, 0.3);
|
||||
}
|
||||
|
||||
.skill-talent {
|
||||
color: #4fc3f7;
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
border-color: rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
/* MOBILE -------------------------------------------------- */
|
||||
@media (max-width: 720px) {
|
||||
.build-of-day {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
dota-random-builds-front/src/pages/HomePage.vue
Normal file
24
dota-random-builds-front/src/pages/HomePage.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<h1>Dota Random Builds</h1>
|
||||
<Randomizer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Randomizer from '@/components/Randomizer.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
padding: 20px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px
|
||||
}
|
||||
</style>
|
||||
21
dota-random-builds-front/src/router/index.ts
Normal file
21
dota-random-builds-front/src/router/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import HomePage from '@/pages/HomePage.vue'
|
||||
import BuildOfTheDayPage from '@/pages/BuildOfTheDayPage.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'HomePage',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: '/build-of-day',
|
||||
name: 'BuildOfTheDayPage',
|
||||
component: BuildOfTheDayPage
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default router
|
||||
12
dota-random-builds-front/src/stores/counter.ts
Normal file
12
dota-random-builds-front/src/stores/counter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
139
dota-random-builds-front/src/stores/randomData.ts
Normal file
139
dota-random-builds-front/src/stores/randomData.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface Hero {
|
||||
id: number
|
||||
name: string
|
||||
primary: string
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type SkillBuild = Record<string, string>
|
||||
|
||||
export interface RandomizeResult {
|
||||
hero: Hero
|
||||
items: Item[]
|
||||
skillBuild?: SkillBuild
|
||||
aspect?: string
|
||||
}
|
||||
|
||||
export interface BuildOfDayResult {
|
||||
date: string
|
||||
hero: Hero
|
||||
items: Item[]
|
||||
skillBuild: SkillBuild
|
||||
aspect?: string
|
||||
}
|
||||
|
||||
type RandomizePayload = {
|
||||
includeSkills: boolean
|
||||
includeAspect: boolean
|
||||
itemsCount: number
|
||||
heroId?: number | null
|
||||
}
|
||||
|
||||
const PREFS_KEY = 'randomizer:prefs'
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
export const useRandomDataStore = defineStore('randomData', {
|
||||
state: () => ({
|
||||
heroes: [] as Hero[],
|
||||
result: null as RandomizeResult | null,
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
prefs: {
|
||||
includeSkills: false,
|
||||
includeAspect: false,
|
||||
itemsCount: 6,
|
||||
heroId: null as number | null
|
||||
} as RandomizePayload,
|
||||
buildOfDay: null as BuildOfDayResult | null,
|
||||
buildOfDayLoading: false,
|
||||
buildOfDayError: null as string | null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
loadPrefs() {
|
||||
try {
|
||||
const raw = localStorage.getItem(PREFS_KEY)
|
||||
if (!raw) return
|
||||
const parsed = JSON.parse(raw)
|
||||
this.prefs = {
|
||||
includeSkills: Boolean(parsed.includeSkills),
|
||||
includeAspect: Boolean(parsed.includeAspect),
|
||||
itemsCount: Number(parsed.itemsCount) || 6,
|
||||
heroId: parsed.heroId ?? null
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load prefs', err)
|
||||
}
|
||||
},
|
||||
|
||||
async loadHeroes() {
|
||||
try {
|
||||
const { data } = await axios.get<Hero[]>(`${BASE_URL}/api/heroes`)
|
||||
this.heroes = data
|
||||
} catch (err) {
|
||||
console.warn('Failed to load heroes', err)
|
||||
}
|
||||
},
|
||||
|
||||
savePrefs() {
|
||||
try {
|
||||
localStorage.setItem(PREFS_KEY, JSON.stringify(this.prefs))
|
||||
} catch (err) {
|
||||
console.warn('Failed to save prefs', err)
|
||||
}
|
||||
},
|
||||
|
||||
setPrefs(patch: Partial<RandomizePayload>) {
|
||||
this.prefs = { ...this.prefs, ...patch }
|
||||
this.savePrefs()
|
||||
},
|
||||
|
||||
async randomize(overrides?: Partial<RandomizePayload>) {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
const payload: RandomizePayload = {
|
||||
...this.prefs,
|
||||
...overrides
|
||||
}
|
||||
|
||||
// sync prefs with overrides so UI reflects latest choice
|
||||
this.prefs = payload
|
||||
this.savePrefs()
|
||||
|
||||
try {
|
||||
const { data } = await axios.post<RandomizeResult>(`${BASE_URL}/api/randomize`, { ...payload })
|
||||
this.result = data
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load random build'
|
||||
this.error = message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async fetchBuildOfDay() {
|
||||
if (this.buildOfDayLoading) return
|
||||
this.buildOfDayLoading = true
|
||||
this.buildOfDayError = null
|
||||
|
||||
try {
|
||||
const { data } = await axios.get<BuildOfDayResult>(`${BASE_URL}/api/build-of-day`)
|
||||
this.buildOfDay = data
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load build of the day'
|
||||
this.buildOfDayError = message
|
||||
} finally {
|
||||
this.buildOfDayLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
12
dota-random-builds-front/tsconfig.app.json
Normal file
12
dota-random-builds-front/tsconfig.app.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
dota-random-builds-front/tsconfig.json
Normal file
11
dota-random-builds-front/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
dota-random-builds-front/tsconfig.node.json
Normal file
19
dota-random-builds-front/tsconfig.node.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node24/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
18
dota-random-builds-front/vite.config.ts
Normal file
18
dota-random-builds-front/vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user