Initial commit

This commit is contained in:
Maxim
2025-12-16 22:16:45 +03:00
commit bf03750e76
64 changed files with 7619 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Python
__pycache__/
*.py[cod]
.venv/
*.sqlite3
# Node
node_modules/
dist/
# IDE
.idea/
.vscode/
*.swp
# Env
.env
.env.*
# Logs
*.log

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
services:
backend:
build: ./dota-random-builds-back
volumes:
- backend-data:/app/data
expose:
- "8000"
frontend:
build: ./dota-random-builds-front
ports:
- "80:80"
depends_on:
- backend
volumes:
backend-data:

View File

@@ -0,0 +1,8 @@
.venv
__pycache__
*.pyc
*.pyo
.git
.gitignore
*.sqlite3
.env

View 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 36).
- **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.

View File

@@ -0,0 +1,13 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Run migrations, load data and start server
CMD ["sh", "-c", "python manage.py migrate && python manage.py load_static_data && python manage.py runserver 0.0.0.0:8000"]

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

View File

@@ -0,0 +1,51 @@
import random
from typing import Dict
def generate_skill_build() -> Dict[int, str]:
"""
Generate a random skill build for levels 1-25 following Dota 2 rules:
- Ultimate (r) can only be skilled at levels 6, 12, 18
- Talents can only be skilled at levels 10, 15, 20, 25
- Basic abilities (q, w, e) can be skilled at other levels
- Each basic ability maxes at 4 points
- Ultimate maxes at 3 points
- One talent per tier (left OR right)
"""
skill_build: Dict[int, str] = {}
# Track ability points spent
ability_points = {"q": 0, "w": 0, "e": 0, "r": 0}
max_points = {"q": 4, "w": 4, "e": 4, "r": 3}
# Talent levels and which side to pick
talent_levels = [10, 15, 20, 25]
# Ultimate levels
ult_levels = [6, 12, 18]
for level in range(1, 26):
if level in talent_levels:
# Pick random talent side
skill_build[level] = random.choice(["left_talent", "right_talent"])
elif level in ult_levels:
# Must skill ultimate if not maxed
if ability_points["r"] < max_points["r"]:
skill_build[level] = "r"
ability_points["r"] += 1
else:
# Ultimate maxed, pick a basic ability
available = [a for a in ["q", "w", "e"] if ability_points[a] < max_points[a]]
if available:
choice = random.choice(available)
skill_build[level] = choice
ability_points[choice] += 1
else:
# Regular level - pick a basic ability
available = [a for a in ["q", "w", "e"] if ability_points[a] < max_points[a]]
if available:
choice = random.choice(available)
skill_build[level] = choice
ability_points[choice] += 1
return skill_build

View File

@@ -0,0 +1 @@
"" # Package marker

View File

@@ -0,0 +1 @@
"" # Package marker

View File

@@ -0,0 +1,104 @@
import re
import sys
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from api.models import Aspect, Hero, Item
# Add data directory to path for imports
sys.path.insert(0, str(Path(settings.BASE_DIR) / "data"))
from facets import HERO_FACETS
def _strip_comments(text: str) -> str:
"""Remove single-line and block comments from TypeScript data."""
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
lines = []
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("//"):
continue
lines.append(line)
return "\n".join(lines)
def _extract_objects(text: str, require_primary: bool = False):
cleaned = _strip_comments(text)
objects = []
for match in re.finditer(r"\{[^}]*\}", cleaned):
chunk = match.group(0)
entry = {}
id_match = re.search(r"['\"]?id['\"]?\s*:\s*(['\"])(.*?)\1", chunk)
name_match = re.search(r"['\"]?name['\"]?\s*:\s*(['\"])(.*?)\1", chunk)
if not (id_match and name_match):
continue
entry["slug"] = id_match.group(2)
entry["name"] = name_match.group(2)
if require_primary:
primary_match = re.search(r"['\"]?primary['\"]?\s*:\s*(['\"])(.*?)\1", chunk)
if not primary_match:
continue
entry["primary"] = primary_match.group(2)
if len(entry) == (3 if require_primary else 2):
objects.append(entry)
return objects
class Command(BaseCommand):
help = "Load heroes, items and facets from data files into the database."
def handle(self, *args, **options):
base_dir = Path(settings.BASE_DIR)
heroes_path = base_dir / "data" / "heroes.ts"
items_path = base_dir / "data" / "items.ts"
if not heroes_path.exists() or not items_path.exists():
raise CommandError("Missing data files in the ./data directory.")
heroes = _extract_objects(heroes_path.read_text(encoding="utf-8"), require_primary=True)
items = _extract_objects(items_path.read_text(encoding="utf-8"), require_primary=False)
if not heroes:
raise CommandError("Failed to parse heroes.ts")
if not items:
raise CommandError("Failed to parse items.ts")
hero_created = item_created = aspect_created = 0
for hero in heroes:
hero_obj, created = Hero.objects.update_or_create(
name=hero["name"],
defaults={
"primary": hero["primary"],
},
)
hero_created += int(created)
# Load facets for this hero
facets = HERO_FACETS.get(hero["name"], [])
for facet_name in facets:
_, facet_created = Aspect.objects.update_or_create(
hero=hero_obj,
name=facet_name,
)
aspect_created += int(facet_created)
for item in items:
_, created = Item.objects.update_or_create(
name=item["name"],
)
item_created += int(created)
self.stdout.write(
self.style.SUCCESS(
f"Loaded {len(heroes)} heroes ({hero_created} new), "
f"{len(items)} items ({item_created} new), "
f"and {aspect_created} new facets."
)
)

View File

@@ -0,0 +1,31 @@
# Generated by Django 6.0 on 2025-12-09 03:20
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Hero',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('external_id', models.CharField(max_length=100, unique=True)),
('name', models.CharField(max_length=200)),
('primary', models.CharField(choices=[('Strength', 'Strength'), ('Agility', 'Agility'), ('Intelligence', 'Intelligence')], max_length=20)),
],
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('external_id', models.CharField(max_length=100, unique=True)),
('name', models.CharField(max_length=200)),
],
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0 on 2025-12-09 03:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='hero',
name='external_id',
),
migrations.RemoveField(
model_name='item',
name='external_id',
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 6.0 on 2025-12-09 18:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_remove_hero_external_id_remove_item_external_id'),
]
operations = [
migrations.CreateModel(
name='Aspect',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('hero', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aspects', to='api.hero')),
],
options={
'unique_together': {('hero', 'name')},
},
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 6.0 on 2025-12-16 18:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0003_aspect'),
]
operations = [
migrations.CreateModel(
name='BuildOfDay',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(unique=True)),
('skill_build', models.JSONField(default=dict)),
('aspect', models.CharField(blank=True, max_length=200, null=True)),
('hero', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.hero')),
('items', models.ManyToManyField(to='api.item')),
],
),
]

View File

@@ -0,0 +1,48 @@
from django.db import models
class Hero(models.Model):
PRIMARY_ATTR_STRENGTH = "Strength"
PRIMARY_ATTR_AGILITY = "Agility"
PRIMARY_ATTR_INTELLIGENCE = "Intelligence"
PRIMARY_CHOICES = (
(PRIMARY_ATTR_STRENGTH, "Strength"),
(PRIMARY_ATTR_AGILITY, "Agility"),
(PRIMARY_ATTR_INTELLIGENCE, "Intelligence"),
)
name = models.CharField(max_length=200)
primary = models.CharField(max_length=20, choices=PRIMARY_CHOICES)
def __str__(self) -> str: # pragma: no cover - convenience only
return f"{self.name} ({self.primary})"
class Item(models.Model):
name = models.CharField(max_length=200)
def __str__(self) -> str: # pragma: no cover - convenience only
return self.name
class Aspect(models.Model):
hero = models.ForeignKey(Hero, on_delete=models.CASCADE, related_name="aspects")
name = models.CharField(max_length=200)
class Meta:
unique_together = ["hero", "name"]
def __str__(self) -> str: # pragma: no cover - convenience only
return f"{self.hero.name} - {self.name}"
class BuildOfDay(models.Model):
date = models.DateField(unique=True)
hero = models.ForeignKey(Hero, on_delete=models.CASCADE)
items = models.ManyToManyField(Item)
skill_build = models.JSONField(default=dict)
aspect = models.CharField(max_length=200, blank=True, null=True)
def __str__(self) -> str: # pragma: no cover - convenience only
return f"Build of {self.date} - {self.hero.name}"

View File

@@ -0,0 +1,38 @@
from rest_framework import serializers
from api.models import Hero, Item
class HeroSerializer(serializers.ModelSerializer):
primary = serializers.SerializerMethodField()
class Meta:
model = Hero
fields = ["id", "name", "primary"]
def get_primary(self, obj):
return obj.primary.lower()
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ["id", "name"]
class RandomizeBuildRequestSerializer(serializers.Serializer):
includeSkills = serializers.BooleanField()
includeAspect = serializers.BooleanField()
itemsCount = serializers.IntegerField(min_value=1)
heroId = serializers.IntegerField(required=False, allow_null=True)
class RandomizeBuildResponseSerializer(serializers.Serializer):
hero = HeroSerializer()
items = ItemSerializer(many=True)
skillBuild = serializers.DictField(
child=serializers.CharField(),
required=False,
help_text="Map of level (1-25) to skill (q/w/e/r/left_talent/right_talent)"
)
aspect = serializers.CharField(required=False)

View File

@@ -0,0 +1,69 @@
from django.core.management import call_command
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from api.models import Hero, Item
class RandomizeBuildAPITest(TestCase):
def setUp(self):
self.client = APIClient()
self.hero = Hero.objects.create(name="Axe", primary="Strength")
Item.objects.bulk_create(
[
Item(name="Blink Dagger"),
Item(name="Black King Bar"),
Item(name="Boots of Travel"),
Item(name="Force Staff"),
]
)
def test_randomize_with_skills(self):
response = self.client.post(
"/api/randomize",
{"includeSkills": True, "includeAspect": False, "itemsCount": 3},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertIn("hero", data)
self.assertEqual(len(data.get("items", [])), 3)
self.assertIn("skillBuild", data)
self.assertTrue(data.get("skillBuilds"))
self.assertNotIn("aspect", data)
def test_randomize_with_aspect_only(self):
response = self.client.post(
"/api/randomize",
{"includeSkills": False, "includeAspect": True, "itemsCount": 2},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertIn("aspect", data)
self.assertTrue(data.get("aspects"))
self.assertNotIn("skillBuild", data)
self.assertEqual(len(data.get("items", [])), 2)
def test_rejects_invalid_payload(self):
response = self.client.post(
"/api/randomize",
{"includeSkills": "yes", "includeAspect": True, "itemsCount": 0},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("message", response.json())
class LoadStaticDataCommandTest(TestCase):
def test_loads_heroes_and_items(self):
call_command("load_static_data")
self.assertGreater(Hero.objects.count(), 0)
self.assertGreater(Item.objects.count(), 0)

View File

@@ -0,0 +1,9 @@
from django.urls import path
from .views import BuildOfDayView, HeroesListView, RandomizeBuildView
urlpatterns = [
path("randomize", RandomizeBuildView.as_view(), name="randomize-build"),
path("heroes", HeroesListView.as_view(), name="heroes-list"),
path("build-of-day", BuildOfDayView.as_view(), name="build-of-day"),
]

View File

@@ -0,0 +1,164 @@
from datetime import date
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import Aspect, BuildOfDay, Hero, Item
from api.serializers import (
HeroSerializer,
ItemSerializer,
RandomizeBuildRequestSerializer,
)
from .data import generate_skill_build
class HeroesListView(APIView):
"""
GET: Return all available heroes for selection.
"""
def get(self, request):
heroes = Hero.objects.all().order_by("name")
return Response(HeroSerializer(heroes, many=True).data, status=status.HTTP_200_OK)
class RandomizeBuildView(APIView):
"""
POST: Generate a random Dota 2 build with hero, items, and optionally skills/aspects.
"""
def get_serializer(self, *args, **kwargs):
return RandomizeBuildRequestSerializer(*args, **kwargs)
def post(self, request):
serializer = RandomizeBuildRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"message": self._format_errors(serializer.errors)},
status=status.HTTP_400_BAD_REQUEST,
)
validated_data = serializer.validated_data
include_skills = validated_data["includeSkills"]
include_aspect = validated_data["includeAspect"]
items_count = validated_data["itemsCount"]
hero_id = validated_data.get("heroId")
hero_count = Hero.objects.count()
item_count = Item.objects.count()
if hero_count == 0:
return Response(
{"message": "No heroes available. Load data first."},
status=status.HTTP_400_BAD_REQUEST,
)
if item_count < items_count:
return Response(
{"message": f"Not enough items available. Requested {items_count}, found {item_count}."},
status=status.HTTP_400_BAD_REQUEST,
)
if hero_id:
hero_obj = Hero.objects.filter(id=hero_id).first()
if not hero_obj:
return Response(
{"message": f"Hero with id {hero_id} not found."},
status=status.HTTP_400_BAD_REQUEST,
)
else:
hero_obj = Hero.objects.order_by("?").first()
item_objs = Item.objects.order_by("?")[:items_count]
response_data = {
"hero": HeroSerializer(hero_obj).data,
"items": ItemSerializer(item_objs, many=True).data,
}
if include_skills:
response_data["skillBuild"] = generate_skill_build()
if include_aspect:
hero_aspects = Aspect.objects.filter(hero=hero_obj)
if hero_aspects.exists():
aspect_obj = hero_aspects.order_by("?").first()
response_data["aspect"] = aspect_obj.name
return Response(response_data, status=status.HTTP_200_OK)
def _format_errors(self, errors: dict) -> str:
"""Format serializer errors into a readable message."""
messages = []
for field, field_errors in errors.items():
for error in field_errors:
if field == "itemsCount" and "greater than or equal to" in str(error):
messages.append("itemsCount must be at least 1.")
elif "valid" in str(error).lower() or "required" in str(error).lower():
if field in ("includeSkills", "includeAspect"):
messages.append("includeSkills and includeAspect must be boolean.")
elif field == "itemsCount":
messages.append("itemsCount must be a number.")
else:
messages.append(f"{field}: {error}")
return messages[0] if messages else "Invalid request data."
class BuildOfDayView(APIView):
"""
GET: Return the build of the day. Generates one if it doesn't exist for today.
"""
ITEMS_COUNT = 6
def get(self, request):
today = date.today()
build = BuildOfDay.objects.filter(date=today).first()
if not build:
build = self._generate_build_of_day(today)
if build is None:
return Response(
{"message": "Unable to generate build. Load data first."},
status=status.HTTP_400_BAD_REQUEST,
)
response_data = {
"date": build.date.isoformat(),
"hero": HeroSerializer(build.hero).data,
"items": ItemSerializer(build.items.all(), many=True).data,
"skillBuild": build.skill_build,
}
if build.aspect:
response_data["aspect"] = build.aspect
return Response(response_data, status=status.HTTP_200_OK)
def _generate_build_of_day(self, today: date) -> BuildOfDay | None:
"""Generate and save a new build of the day."""
hero_count = Hero.objects.count()
item_count = Item.objects.count()
if hero_count == 0 or item_count < self.ITEMS_COUNT:
return None
hero_obj = Hero.objects.order_by("?").first()
item_objs = list(Item.objects.order_by("?")[: self.ITEMS_COUNT])
aspect_name = None
hero_aspects = Aspect.objects.filter(hero=hero_obj)
if hero_aspects.exists():
aspect_obj = hero_aspects.order_by("?").first()
aspect_name = aspect_obj.name
build = BuildOfDay.objects.create(
date=today,
hero=hero_obj,
skill_build=generate_skill_build(),
aspect=aspect_name,
)
build.items.set(item_objs)
return build

View File

@@ -0,0 +1,16 @@
"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,129 @@
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 6.0.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-a#7v1um&b88$k)2nm7hvxe__o2c(gz=t5%)1)*oaij6u%+i((='
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"corsheaders",
"rest_framework",
"api",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
import os
DATA_DIR = BASE_DIR / 'data'
DATA_DIR.mkdir(exist_ok=True)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': DATA_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
# CORS settings
CORS_ALLOW_ALL_ORIGINS = True

View File

@@ -0,0 +1,23 @@
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path("api/", include("api.urls")),
]

View File

@@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()

View File

@@ -0,0 +1,7 @@
export const aspects = [
'Carry',
'Support',
'Roamer',
'Jungler',
'Pusher'
]

View File

@@ -0,0 +1,128 @@
# Hero Facets data from Dota 2 patch 7.36+
# Each hero has 2 facets that modify their abilities
HERO_FACETS = {
# Strength Heroes
"Abaddon": ["Mephitic Shroud", "The Quickening"],
"Alchemist": ["Seed Money", "Mixologist"],
"Axe": ["One Man Army", "Call Out"],
"Beastmaster": ["Wild Hunt", "Beast Mode"],
"Brewmaster": ["Roll Out the Barrel", "Drunken Master"],
"Bristleback": ["Seeing Red", "Prickly"],
"Centaur Warrunner": ["Horsepower", "Rawhide"],
"Chaos Knight": ["Irrationality", "Schism"],
"Clockwerk": ["Expanded Armature", "Overclock"],
"Dawnbreaker": ["Solar Guardian", "Gleaming Hammer"],
"Doom": ["Impending Doom", "Gluttony"],
"Dragon Knight": ["Wyrm's Wrath", "Corrosive Dragon"],
"Earth Spirit": ["Ready to Roll", "Stepping Stone"],
"Earthshaker": ["Tectonic Buildup", "Slugger"],
"Elder Titan": ["Deconstruction", "Momentum"],
"Huskar": ["Bloodbath", "Incendiary"],
"Kunkka": ["Grog Blossom", "Tideskipper"],
"Legion Commander": ["Stonehall Plate", "Spoils of War"],
"Lifestealer": ["Corpse Eater", "Rage Demon"],
"Lycan": ["Spirit Wolves", "Alpha Wolves"],
"Magnus": ["Reverse Polarity Field", "Hot-Blooded"],
"Mars": ["Shock Troops", "Arena of Blood"],
"Night Stalker": ["Blinding Void", "Heart of Darkness"],
"Ogre Magi": ["Fat Chances", "Learning Curve"],
"Omniknight": ["Degen Aura", "Ardent Revelation"],
"Phoenix": ["Dying Light", "Spread the Fire"],
"Primal Beast": ["Ferocity", "Irascible"],
"Pudge": ["Flayers Hook", "Fresh Meat"],
"Slardar": ["Brineguard", "Leg Day"],
"Snapfire": ["Ricochet II", "Full Bore"],
"Spirit Breaker": ["Imbalanced", "Bulldoze"],
"Sven": ["Heavy Plate", "Warcry Aura"],
"Tidehunter": ["Blubber", "Kraken Swell"],
"Timbersaw": ["Flamethrower", "Shredder"],
"Tiny": ["Crash Landing", "Insurmountable"],
"Treant Protector": ["Sapling", "Primeval Power"],
"Tusk": ["Drinking Buddies", "Overpowering Chill"],
"Underlord": ["Abyssal Horde", "Fiend's Gate"],
"Undying": ["Rotting Mitts", "Ripped"],
"Wraith King": ["Bone Guard", "Death's Hallow"],
# Agility Heroes
"Anti-Mage": ["Magebane", "Mana Thirst"],
"Arc Warden": ["Order", "Disorder"],
"Bloodseeker": ["Arterial Spray", "Sanguivore"],
"Bounty Hunter": ["Through and Through", "Big Game Hunter"],
"Broodmother": ["Necrotic Webs", "Feeding Frenzy"],
"Clinkz": ["Bone and Arrow", "Engulfing Step"],
"Drow Ranger": ["Sidestep", "Vantage Point"],
"Ember Spirit": ["Double Impact", "Chain Gang"],
"Faceless Void": ["Chronosphere Teamwork", "Distortion Field"],
"Gyrocopter": ["Afterburner", "Chop Shop"],
"Hoodwink": ["Treebounce Trickshot", "Go Nuts"],
"Juggernaut": ["Bladestorm", "Agigain"],
"Lone Druid": ["Bear Necessities", "Unbearable"],
"Luna": ["Lunar Orbit", "Moonshield"],
"Medusa": ["Engorged", "Monstrous"],
"Meepo": ["More Meepo", "Pack Rat"],
"Mirana": ["Moonlight", "Shooting Star"],
"Monkey King": ["Simian Stride", "Wukong's Faithful"],
"Muerta": ["Ofrenda", "Dance of the Dead"],
"Naga Siren": ["Aquatic Dominance", "Deluge"],
"Nyx Assassin": ["Scuttle", "Spelunker"],
"Pangolier": ["Double Jump", "Thunderbolt"],
"Phantom Assassin": ["Methodical", "Immaterial"],
"Phantom Lancer": ["Convergence", "Phantom Edge"],
"Razor": ["Dynamo", "Shocking"],
"Riki": ["Exterminator", "Infiltrator"],
"Shadow Fiend": ["Lasting Presence", "Shadowmire"],
"Slark": ["Leeching Leash", "Dark Reef Renegade"],
"Sniper": ["Ghillie Suit", "Scattershot"],
"Spectre": ["Forsaken", "Twist the Knife"],
"Templar Assassin": ["Refractor", "Third Strike"],
"Terrorblade": ["Soul Fragment", "Condemned"],
"Troll Warlord": ["Bad Influence", "Insensitive"],
"Ursa": ["Bear Down", "Grudge Bearer"],
"Viper": ["Predator", "Poison Burst"],
"Weaver": ["Skitterstep", "Hivemind"],
# Intelligence Heroes
"Ancient Apparition": ["Bone Chill", "Exposure"],
"Bane": ["Dream Haunter", "Sleepwalk"],
"Batrider": ["Arsonist", "Stoked"],
"Chen": ["Centaur Convert", "Totemic"],
"Crystal Maiden": ["Cold Comfort", "Frozen Expanse"],
"Dark Seer": ["Heart of Battle", "Quick Wit"],
"Dark Willow": ["Thorny Thicket", "Wilding"],
"Dazzle": ["Nothl Boon", "Poison Bloom"],
"Death Prophet": ["Spirit Collector", "Mourning Ritual"],
"Disruptor": ["Thunderstorm", "Kinetic Fence"],
"Enchantress": ["Overprotective Wisps", "Little Friends"],
"Enigma": ["Event Horizon", "Gravity Well"],
"Grimstroke": ["Fine Art", "Inkstigate"],
"Invoker": ["Elitist", "Magus"],
"Jakiro": ["Liquid Fire", "Liquid Frost"],
"Keeper of the Light": ["Solar Bind", "Recall"],
"Leshrac": ["Diabolic Edict Charges", "Misery"],
"Lich": ["Growing Cold", "Ice Spire"],
"Lina": ["Slow Burn", "Thermal Runaway"],
"Lion": ["Fist of Death", "Gobble Up"],
"Nature's Prophet": ["Ironwood Treant", "Curse of the Forest"],
"Necrophos": ["Rapid Decay", "Profane Potency"],
"Oracle": ["Clairvoyant Curse", "Prognosticate"],
"Outworld Destroyer": ["Obsidian Decimator", "Ominous Discernment"],
"Puck": ["Jostling Rift", "Curveball"],
"Pugna": ["Siphoning Ward", "Rewards of Ruin"],
"Queen of Pain": ["Succubus", "Selfish Scream"],
"Rubick": ["Frugal Filch", "Arcane Supremacy"],
"Shadow Demon": ["Promulgate", "Menace"],
"Shadow Shaman": ["Cluster Cluck", "Massive Serpent Wards"],
"Silencer": ["Irrepressible", "Reverberating Silence"],
"Skywrath Mage": ["Staff of the Scion", "Shield of the Scion"],
"Storm Spirit": ["Shock Collar", "Static Slide"],
"Techies": ["Squee", "Spleen"],
"Tinker": ["Repair Bots", "Translocator"],
"Visage": ["Sepulchre", "Death Toll"],
"Void Spirit": ["Sanctuary", "Aether Remnant"],
"Warlock": ["Black Grimoire", "Champion of Gorroth"],
"Windranger": ["Journeyman", "Whirlwind"],
"Winter Wyvern": ["Heal Wyvern", "Essence Devourer"],
"Witch Doctor": ["Cleft Death", "Voodoo Festeration"],
"Zeus": ["Livewire", "Lightning Hands"],
}

View 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' }
];

View 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: 'Vladmirs 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: 'Shivas 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: 'Heavens Halberd' },
// // Mobility / utility
// { id: 'item_cyclone', name: 'Euls 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: 'Aghanims Scepter' },
// { id: 'item_aghanims_shard', name: 'Aghanims 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: 'Linkens 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: 'Aghanims 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: 'Pupils Gift' },
// { id: 'item_philosophers_stone', name: 'Philosophers 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' }
// ];

View 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' }
]

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1 @@
создай django + drf приложение щас в среде используется python 3.12.3

View File

@@ -0,0 +1,3 @@
django>=6.0,<7.0
djangorestframework>=3.16,<4.0
django-cors-headers>=4.9,<5.0

View File

@@ -0,0 +1,6 @@
node_modules
dist
.git
.gitignore
*.log
.env*

36
dota-random-builds-front/.gitignore vendored Normal file
View 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__/

View 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.

View 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 36).
- **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.

View 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;"]

View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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>

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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;
}

View 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>

View File

@@ -0,0 +1,7 @@
export const aspects = [
'Carry',
'Support',
'Roamer',
'Jungler',
'Pusher'
]

View 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' }
];

View 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: 'Vladmirs 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: 'Shivas 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: 'Heavens Halberd' },
// // Mobility / utility
// { id: 'item_cyclone', name: 'Euls 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: 'Aghanims Scepter' },
// { id: 'item_aghanims_shard', name: 'Aghanims 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: 'Linkens 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: 'Aghanims 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: 'Pupils Gift' },
// { id: 'item_philosophers_stone', name: 'Philosophers 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' }
// ];

View 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' }
]

View 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')

View 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>

View 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>

View 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

View 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 }
})

View 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
}
}
}
})

View 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/*"]
}
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View 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"]
}
}

View 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))
},
},
})