О проекте:
Раз уж пошла эпоха вайбкодинга, решил я тоже присоединиться к этому буму и создать свой проект. Всё чего касается этого проекта было либо навайбкожено, либо куплено специально для проекта.
Big Head Engine — RTS/RPG-прототип на Panda3D с редактором карт, data-driven контентом и первой активной картой Angel Arena.
Что уже есть:
- Редактор размещения юнитов, регионов, декораций, terrain
- Runtime scripts через MapScript
- Кастомные unit attributes: love, hate, death, life
- Hero select, leveling, inventory, HUD, multiplayer foundation
- Asset pipeline для GLB/BAM, иконок, VFX particles
Чего планирую, что будет кардинально отличаться от Warcraft 3:
- Тип карт с возможностью присоединиться в любой момент
- Упор на работу с картой преимущественно в IDE
- Хранение состояния карт на сервере. Потенциал для донатов, сейвов персонажей для RPG, активации событий на карте
- Инвентарь 8 слотов, сумка 8 слотов, слоты для зелий
Чего не планирую:
- Редактор объектов отсутствует, но ничего не составит труда навайбкодить его
В качестве полигона для испытаний я создал карту Angel Arena, разработчиком которого я был давным давно (А точнее версии World of Angel Arena)
Самое интересное, скринчики
Редактор:
Игра:
Документация:
Примеры скрипта
Скрипт на логику работы крипов как в Angel Arena
from __future__ import annotations
from big_head_engine.core.scripting import MapScript, ScriptEventRegistry, ScriptGameAPI
NEUTRAL_PLAYER_ID = 6
KILLS_TO_BOSS = 40
MAX_CREEP_LEVEL = 5
NORMAL_DROP_CHANCE_PERCENT = 5
RESPAWN_DELAY_SECONDS = 3.0
TELEPORT_EFFECT = "teleport flash"
GATE_OPEN_ANIMATION = "Open"
CREEP_SPAWN_REGIONS: dict[int, str] = {
1: "region_015",
2: "region_016",
3: "region_017",
4: "region_018",
5: "region_019",
6: "region_020",
7: "region_021",
8: "region_022",
}
CREEP_BOSS_REGIONS: dict[int, str] = {
1: "region_027",
2: "region_041",
3: "region_035",
4: "region_037",
5: "region_040",
6: "region_034",
7: "region_032",
8: "region_029",
}
CREEP_SET_ITEMS: dict[int, tuple[str, ...]] = {
index: (
f"creep_{index}_head",
f"creep_{index}_gloves",
f"creep_{index}_armor",
f"creep_{index}_weapon",
f"creep_{index}_boots",
f"creep_{index}_pants",
)
for index in range(1, 9)
}
class AngelArenaCreeps(MapScript):
id = "angel_arena_creeps"
def __init__(self) -> None:
self.kills_by_spawn: dict[int, int] = {index: 0 for index in range(1, 9)}
self.spawn_level: dict[int, int] = {index: 1 for index in range(1, 9)}
self.boss_active: dict[int, bool] = {index: False for index in range(1, 9)}
self.boss_unit_ids: dict[str, int] = {}
self.events: ScriptEventRegistry | None = None
def register(self, events: ScriptEventRegistry, api: ScriptGameAPI) -> None:
self.events = events
events.on_unit_death(self.on_unit_death)
events.every(0.5, self.refresh_labels)
self.refresh_labels(api)
def on_unit_death(self, unit: object, api: ScriptGameAPI) -> None:
unit_id = self.unit_id(unit)
if unit_id is None:
return
definition = getattr(unit, "definition", None)
tags = list(getattr(definition, "tags", [])) if definition is not None else []
spawn_index = self.creep_index(tags)
if spawn_index is None:
return
killer = getattr(unit, "last_damage_source", None)
killer_id = self.unit_id(killer)
boss_spawn_index = self.boss_unit_ids.pop(unit_id, None)
if boss_spawn_index is not None:
self.on_boss_death(boss_spawn_index, unit, unit_id, killer_id, api)
return
if "normal_creep" not in tags:
return
self.on_normal_creep_death(spawn_index, unit_id, killer_id, api)
def on_normal_creep_death(
self,
spawn_index: int,
unit_id: str,
killer_id: str | None,
api: ScriptGameAPI,
) -> None:
drop_seed = f"normal_drop:{spawn_index}"
item_seed = f"normal_drop_item:{spawn_index}"
drop_roll = api.roll_percent(drop_seed)
did_drop = drop_roll < NORMAL_DROP_CHANCE_PERCENT
if did_drop:
item_id = api.choose(item_seed, CREEP_SET_ITEMS[spawn_index])
if item_id is not None:
self.give_or_drop_item(
item_id,
unit_id,
killer_id,
api,
level=self.spawn_level[spawn_index],
)
if not self.boss_active[spawn_index]:
self.kills_by_spawn[spawn_index] = min(
KILLS_TO_BOSS,
self.kills_by_spawn[spawn_index] + 1,
)
self.refresh_label(spawn_index, api)
if self.kills_by_spawn[spawn_index] >= KILLS_TO_BOSS:
self.open_gate(spawn_index, api)
self.spawn_boss(spawn_index, api)
if self.events is not None:
self.events.after(
RESPAWN_DELAY_SECONDS,
lambda game_api, spawn_index=spawn_index: self.spawn_normal_creep(
spawn_index,
game_api,
),
)
def on_boss_death(
self,
spawn_index: int,
unit: object,
unit_id: str,
killer_id: str | None,
api: ScriptGameAPI,
) -> None:
item_id = api.choose(unit_id, CREEP_SET_ITEMS[spawn_index])
if item_id is not None:
boss_level = min(MAX_CREEP_LEVEL, self.spawn_level[spawn_index] + 1)
self.give_or_drop_item(item_id, unit_id, killer_id, api, level=boss_level)
if killer_id is not None:
source_position = api.unit(killer_id).get_position()
if source_position is not None:
api.play_effect(TELEPORT_EFFECT, source_position, duration=1.2)
api.unit(killer_id).teleport_to_region(CREEP_SPAWN_REGIONS[spawn_index])
api.play_effect_at_region(
TELEPORT_EFFECT,
CREEP_SPAWN_REGIONS[spawn_index],
duration=1.2,
)
self.boss_active[spawn_index] = False
self.kills_by_spawn[spawn_index] = 0
self.spawn_level[spawn_index] = min(
MAX_CREEP_LEVEL,
self.spawn_level[spawn_index] + 1,
)
self.close_gate(spawn_index, api)
self.refresh_label(spawn_index, api)
def spawn_boss(self, spawn_index: int, api: ScriptGameAPI) -> None:
if self.boss_active[spawn_index]:
return
self.boss_active[spawn_index] = True
boss_level = min(MAX_CREEP_LEVEL, self.spawn_level[spawn_index] + 1)
boss_id = api.spawn_unit_in_region(
f"creep_{spawn_index}_level_{boss_level}",
NEUTRAL_PLAYER_ID,
CREEP_BOSS_REGIONS[spawn_index],
)
if boss_id is None:
self.boss_active[spawn_index] = False
return
self.boss_unit_ids[boss_id] = spawn_index
for item_id in CREEP_SET_ITEMS[spawn_index]:
boss_item_id = api.unit(boss_id).add_item(item_id)
if boss_item_id is not None:
api.set_item_level(boss_item_id, boss_level)
def spawn_normal_creep(self, spawn_index: int, api: ScriptGameAPI) -> None:
level = self.spawn_level[spawn_index]
api.spawn_unit_in_region(
f"creep_{spawn_index}_level_{level}",
NEUTRAL_PLAYER_ID,
CREEP_SPAWN_REGIONS[spawn_index],
)
def refresh_labels(self, api: ScriptGameAPI) -> None:
for spawn_index in range(1, 9):
self.refresh_label(spawn_index, api)
def refresh_label(self, spawn_index: int, api: ScriptGameAPI) -> None:
count = self.kills_by_spawn[spawn_index]
level = self.spawn_level[spawn_index]
suffix = f" L{level}"
if self.boss_active[spawn_index]:
suffix += " Boss"
api.set_floating_label_at_region(
f"creep_spawn_counter_{spawn_index}",
f"{count}/{KILLS_TO_BOSS}{suffix}",
CREEP_SPAWN_REGIONS[spawn_index],
z=2.6,
)
def open_gate(self, spawn_index: int, api: ScriptGameAPI) -> None:
gate = api.decoration(f"creep_gate_{spawn_index}")
gate.set_collision_enabled(False)
gate.play_animation(GATE_OPEN_ANIMATION, loop=False)
def close_gate(self, spawn_index: int, api: ScriptGameAPI) -> None:
gate = api.decoration(f"creep_gate_{spawn_index}")
gate.play_animation(GATE_OPEN_ANIMATION, loop=False, reverse=True)
gate.set_collision_enabled(True)
def give_or_drop_item(
self,
item_id: str,
unit_id: str,
killer_id: str | None,
api: ScriptGameAPI,
level: int,
) -> None:
if killer_id is not None:
added_item_id = api.unit(killer_id).add_item(item_id)
if added_item_id is not None:
api.set_item_level(added_item_id, level)
if added_item_id is not None:
return
dropped_item_id = api.drop_item_near_unit(item_id, unit_id)
if dropped_item_id is not None:
api.set_item_level(dropped_item_id, level)
def unit_id(self, unit: object | None) -> str | None:
if unit is None:
return None
instance = getattr(unit, "instance", None)
unit_id = getattr(instance, "id", None)
return unit_id if isinstance(unit_id, str) else None
def creep_index(self, tags: list[str]) -> int | None:
for index in range(1, 9):
if f"creep_type_{index}" in tags:
return index
return None
Заранее отвечу на вопросы:
Нахрена на Python?
- Чисто по фану, подумал что на нём проще всего писать скрипты, а ещё архитектурно я собираю проект так, чтобы в любой момент можно было избавиться от Panda3D, и использовать другой Backend движок
Будет ли Open Source
- Да, планирую выложить в Open Source, но серверную часть (если проект взлетит, то будет моим хлебом) выложу только для LAN
Планируешь заработать на вайбкодинге?
- Не особо, при проектировании движка я закладываю источники для монетизации, но это не самоцель, проект делаю для души

BIG HEAD EN…














П.С. были бы боты, я бы даже потестил...