logo
LIS PUBLICA
☰
  • Новое
  • Горячее
  • Сокровищница
  • Лучшее
  • Сообщества
  • Видео
  • Обсуждаемое

VariusSoft
VariusSoft Серия: Пишем танчики на JavaScript Сообщество: GameDev Опубликовано 1 неделю назад
  • [моё]
  • Battle City
  • GameDev
  • Длиннопост
  • Программирование

«Танчики» на HTML5

Разные виды врагов и бонусы

Игра почти готова, но не хватает ещё парочки нюансов)

Сразу смотрим результат, а я пока распишу, что же тут происходит

1. Я добавил разные типы врагов. Как и в оригинале, это базовые, быстроходные и бронированные. Последние плюсом ко всему имеют бронебойные патроны.

2. Я добавил новый вид бонуса: «Строитель». Поднимая мастерок игрок может построить несколько бетонных и несколько кирпичных блоков. Именно для баланса этого бонуса и введены вражеские танки, которые могут уничтожать в том числе и бетонные стены.

Давайте смотреть

    export class Bonus {
    /**
     * По всему коду натыкал как можно больше комментов, чтоб понятно было куда собака зарыта
     *
     * @param {number} x - Логическая координата X
     * @param {number} y - Логическая координата Y
     * @param {string} type - Тип бонуса (BonusType.STAR, BonusType.GRENADE, etc.)
     */
    constructor(x, y, type) {
        this.x = x;
        this.y = y;
        this.type = type;

        // Время жизни бонуса
        this.lifetime = Duration.BONUS_LIFETIME;
        this.blinkTime = Duration.BONUS_BLINK_TIME;

        // Состояние
        this.collected = false;
        this.expired = false;

        // Анимация
        this.animationFrame = 0;
        this.animationTimer = 0;
        this.animationSpeed = 6; // Кадров игры между сменой кадра анимации

        // Мигание перед исчезновением
        this.blinkTimer = 0;
        this.blinkSpeed = 4; // Быстрое мигание
        this.visible = true;
    }

    /**
     * Обновление состояния бонуса
     */
    update() {
        if (this.collected || this.expired) return;

        // Уменьшаем время жизни
        this.lifetime--;

        // Проверка истечения времени
        if (this.lifetime <= 0) {
            this.expired = true;
            return;
        }

        // Анимация спрайта
        this.animationTimer++;
        if (this.animationTimer >= this.animationSpeed) {
            this.animationTimer = 0;
            this.animationFrame = (this.animationFrame + 1) % BONUS_SPRITE.frameCount;
        }

        // Мигание перед исчезновением
        if (this.lifetime <= (Duration.BONUS_LIFETIME - this.blinkTime)) {
            this.blinkTimer++;
            if (this.blinkTimer >= this.blinkSpeed) {
                this.blinkTimer = 0;
                this.visible = !this.visible;
            }
        }
    }

    /**
     * Сбор бонуса игроком
     */
    collect() {
        this.collected = true;
    }

    /**
     * Проверка, истёк ли бонус
     */
    isExpired() {
        return this.expired || this.collected;
    }

    /**
     * Получить bounding box в логических координатах
     */
    getBounds() {
        return {
            x: this.x,
            y: this.y,
            width: BONUS_SIZE,
            height: BONUS_SIZE
        };
    }

    /**
     * Отрисовка бонуса
     */
    render() {
        if (this.collected || this.expired) return;
        if (!this.visible) return; // Мигание

        const ctx = foregroundCtx;

        // Конвертация логических координат в физические
        const px = GAME_FIELD_X + this.x * GAME_SCALE;
        const py = GAME_FIELD_Y + this.y * GAME_SCALE;
        const size = BONUS_SIZE * GAME_SCALE;

        const sprite = getBonusSprite(this.type);

        if (sprite) {
            const frameX = this.animationFrame * BONUS_SPRITE.frameWidth;

            ctx.drawImage(
                sprite,
                frameX, 0,
                BONUS_SPRITE.frameWidth, BONUS_SPRITE.frameHeight,
                px, py,
                size, size
            );
        } else {
            // Fallback: цветной квадрат
            this.renderFallback(ctx, px, py, size);
        }
    }

    /**
     * Fallback отрисовка (цветной квадрат с буквой)
     */
    renderFallback(ctx, px, py, size) {
        // Цвета для разных типов бонусов
        const colors = {
            [BonusType.STAR]: '#FFFF00',
            [BonusType.GRENADE]: '#FF4500',
            [BonusType.TIMER]: '#00BFFF',
            [BonusType.SHOVEL]: '#8B4513',
            [BonusType.TANK]: '#00FF00',
            [BonusType.HELMET]: '#C0C0C0',
            [BonusType.GUN]: '#FF00FF',
            [BonusType.BUILDER]: '#334455'
        };

        const letters = {
            [BonusType.STAR]: 'S',
            [BonusType.GRENADE]: 'G',
            [BonusType.TIMER]: 'T',
            [BonusType.SHOVEL]: 'L',
            [BonusType.TANK]: '+',
            [BonusType.HELMET]: 'H',
            [BonusType.GUN]: 'P',
            [BonusType.BUILDER]: 'B'
        };

        ctx.fillStyle = colors[this.type] || '#FFFFFF';
        ctx.fillRect(px + 2, py + 2, size - 4, size - 4);

        ctx.fillStyle = '#000000';
        ctx.font = `bold ${size * 0.6}px Arial`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(letters[this.type] || '?', px + size / 2, py + size / 2);
    }
}

Где и как учитываются бонусы?

Во-первый в main.js я сразу создаю бонус для строительства

    // DEBUG: Тестовый бонус Builder рядом с игроком
    const testBonus = new Bonus(PLAYER_SPAWN_POINT.x + 32, PLAYER_SPAWN_POINT.y - 32, BonusType.BUILDER);
    bonuses.push(testBonus);

Это сделано для тестов, но, возможно, я даже оставлю в конечном варианте

Дальше в кор геймплее

    const collectedBonus = checkTankBonusCollision(player, bonuses);
        if (collectedBonus) {
            applyBonusEffect(collectedBonus);
        }
// И дальше по коду сам метод
function applyBonusEffect(bonus) {
    switch (bonus.type) {
        case BonusType.STAR:
            // Апгрейд оружия (до уровня 3 максимум)
            if (player && player.bulletLevel < 3) {
                player.bulletLevel++;
                // Уровень 1+ даёт 2 пули
                if (player.bulletLevel >= 1) {
                    player.bulletCount = 2;
                }
            }
            break;

        case BonusType.GUN:
            // Мгновенный максимальный апгрейд
            if (player) {
                player.bulletLevel = 3;
                player.bulletCount = 2;
            }
            break;

        case BonusType.TANK:
            // Дополнительная жизнь
            playerLives++;
            break;

        case BonusType.HELMET:
            // Временная неуязвимость
            if (player) {
                player.hasShield = true;
                player.shieldDuration = Duration.EFFECT_SHIELD;
            }
            break;

        case BonusType.TIMER:
            // Заморозка врагов
            freezeTimer = Duration.EFFECT_TIMER;
            break;

        case BonusType.GRENADE:
            // Уничтожение всех врагов на экране
            for (const enemy of enemies) {
                if (!enemy.destroyed) {
                    enemy.destroyed = true;
                    // Создаём взрывы
                    const centerX = enemy.x + TANK_SIZE / 2;
                    const centerY = enemy.y + TANK_SIZE / 2;
                    effects.push(createBigExplosion(centerX, centerY));
                }
            }
            break;

        case BonusType.SHOVEL:
            // Укрепление базы
            shovelTimer = Duration.EFFECT_SHOVEL;
            fortifyBase();
            break;

        case BonusType.BUILDER:
            // Режим строителя: даёт блоки для размещения
            if (player) {
                player.builderBricks += 8;
                player.builderSteel += 4;
            }
            break;
    }

    bonus.collect();
}

Далее все таймеры эффекты глобальные сбрасываются вот тут

    function updateGlobalEffects() {
    // Таймер заморозки
    if (freezeTimer > 0) {
        freezeTimer--;
    }

    // Таймер укрепления базы
    if (shovelTimer > 0) {
        shovelTimer--;
        if (shovelTimer <= 0 && baseFortified) {
            restoreBase();
        }
    }
}

Вообще в данном случае было бы логично вынести логику сброса эффекта бонуса в сам бонус. Чтоб он там в фоне считался, а ядру просто отдавал событие или менял внутри себя флаг. Как будто там будет почище. Подумал об этом только сейчас, отрефакторю к следующему выпуску.

Следующий этап уже не про новые фичи, а про полировку, оптимизацию и доработку.

Писал я план ещё в самом начале, так что часть из этих пунктов уже сделана, но всё равно нужно будет пройтись по всем.


Всем хороших игр!

Читать дальше...
9
+9 / -0
1
34
ТГ ВК
Aid314
Aid314 Опубликовано 1 неделю назад
5
+5 / -0
Войти

Вход

Регистрация

Я не помню пароль

Войти через Google
Порог горячего 15
  • etoshtrudel
    etoshtrudel

    Нам ещё и ехать в Рязань за чем-то другим)) так что, вокруг меня прям почти один набор

    +1
  • Porked
    Porked

    не на вес, на плотность в банке. немного прижал и всё. после первого раза понятно будет. Капуста после заливаяния кипящим маринадом очень сильно оседает, если совсем уже не прессовать. я её ещё и пер...

    +1
  • Porked
    Porked

    на одну 3х-литровую банку:

    капусту, морковь (по вкусу, она там по сути для косметики), чеснок 3-4 зубчика(но тут от размера зубчика зависит, я головку целую кромсал или давил в банку, перемешивая с кап...

    +1
Правила сайта
Пользовательское соглашение
О ПД
Принципы самоуправления
Нашёл ошибку?
©2026 Varius Soft