Перейти к содержанию

VIP-система#

Плагин-пакет: VIP-статус из базы, бонусы на спавне и в уроне, команда выдачи, натив и форвард для Pawn-плагинов и переводы.

examples/plugins/vip/
├─ __init__.py     Plugin и подключение модулей
├─ config.py       cvar'ы
├─ models.py       dataclass Vip
├─ repo.py         хранилище SQLite
├─ state.py        PlayerState(keep=True)
├─ perks.py        загрузка VIP и бонусы
├─ commands.py     /vip и amx_vip_add
├─ api.py          натив и форвард для Pawn
└─ lang/           ru.toml, en.toml
"""VIP-система: бонусы на спавне и в уроне, натив и форвард для Pawn, хранение в SQLite."""

from amxpy import Plugin, events

from . import api, commands, perks, repo
from .config import cfg

plugin = Plugin("VIP", version="1.2.0", author="amxpy")
plugin.include(cfg, api.router, commands.router, perks.router)


@plugin.on(events.Init)
async def init(e: events.Init) -> None:
    hot = e.hot_reload
    await repo.migrate()
    if hot:
        plugin.log.info("reloaded on the fly, player state kept")
from amxpy import Router, events

from . import repo
from .api import vip_granted
from .config import cfg
from .state import vips

router = Router()


@router.on(events.Authorized)  # SteamID уже известен
async def load(e: events.Authorized) -> None:
    player = e.player
    vip = await repo.find(player.steam_id)  # кадр не ждёт; игрок ушёл — задача отменится
    if vip is not None and vip.active:
        vips[player] = vip
        vip_granted.emit(player, vip.days_left)


@router.on(events.PlayerSpawn, post=True)
def spawn_bonus(e: events.PlayerSpawn) -> None:
    if e.player in vips:
        e.player.health += cfg.hp_bonus


@router.on(events.TakeDamage)
def more_damage(e: events.TakeDamage) -> None:
    if e.attacker in vips:
        e.damage *= cfg.damage_mult
from datetime import UTC, datetime, timedelta

from amxpy import Access, Player, Router

from . import repo
from .api import vip_granted
from .models import Vip
from .state import vips

router = Router()


@router.command("/vip", help="Статус VIP")
def status(player: Player) -> None:
    match vips.get(player):
        case None:
            player.print(player.tr("VIP_NONE"))
        case Vip(until=None):
            player.print(player.tr("VIP_FOREVER"))
        case vip:
            player.print(player.tr("VIP_DAYS", days=vip.days_left))


@router.command("amx_vip_add", access=Access.RCON, help="Выдать VIP")
async def add(admin: Player, target: Player, days: int = 30) -> None:
    vip = Vip(target.steam_id, datetime.now(UTC) + timedelta(days=days))
    await repo.save(vip)
    vips[target] = vip
    vip_granted.emit(target, vip.days_left)
    admin.console(f"VIP: {target.name}, {days} d.")
"""То, что видят Pawn-плагины: amxpyc генерирует из этого vip.inc."""

from amxpy import Player, Router

from .state import vips

router = Router()
vip_granted = router.forward("vip_granted", player=Player, days=int)


@router.native
def vip_is_user_vip(player: Player) -> bool:
    """Является ли игрок VIP."""
    return player in vips


@router.native
def vip_days_left(player: Player) -> int:
    """Сколько дней VIP осталось: -1 — навсегда, 0 — не VIP."""
    vip = vips.get(player)
    return vip.days_left if vip else 0
from amxpy import Config, cvar


class Settings(Config, prefix="vip_"):
    hp_bonus: int = cvar(25, "Доп. HP на спавне", min=0, max=155)
    damage_mult: float = cvar(1.1, "Множитель урона VIP", min=1.0, max=2.0)


cfg = Settings()
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from math import ceil


@dataclass
class Vip:
    steam_id: str
    until: datetime | None = None  # None — навсегда

    @property
    def active(self) -> bool:
        return self.until is None or self.until > datetime.now(UTC)

    @property
    def days_left(self) -> int:
        """Сколько дней осталось; -1 — навсегда."""
        if self.until is None:
            return -1
        return max(0, ceil((self.until - datetime.now(UTC)) / timedelta(days=1)))
from amxpy.storage import Table

from .models import Vip

table = Table("vip", Vip, key="steam_id")  # addons/amxpy/data/vip.db, запросы вне игрового потока


async def migrate() -> None:
    await table.create()


async def find(steam_id: str) -> Vip | None:
    return await table.get(steam_id)


async def save(vip: Vip) -> None:
    await table.put(vip)
from amxpy import PlayerState

from .models import Vip

# vips[player] — очищается при выходе игрока и переживает горячую перезагрузку
vips: PlayerState[Vip] = PlayerState(keep=True)

Как это работает#

  • __init__.py создаёт единственный Plugin и подключает роутеры модулей и конфиг через plugin.include(...); в Init готовит базу.
  • state.py — vips: PlayerState[Vip] с keep=True: статус игроков переживает горячую перезагрузку.
  • perks.py загружает VIP в Authorized (SteamID уже известен), даёт HP на спавне (post) и усиливает урон (pre).
  • commands.py — /vip отвечает на языке игрока через player.tr(...), а amx_vip_add <target> [days] выдаёт VIP и рассылает форвард vip_granted.
  • api.py — натив vip_is_user_vip, vip_days_left и форвард vip_granted: из них amxpyc генерирует vip.inc для Pawn-плагинов.