Working on a new potion

Heya,

during Christmas holidays I’ve ported a lot of stuff to the new engine. I’ve also had a quite funny first multiplayer match with two friends and the three of us made it until wave 50 (then the simulation crashed :-)).

Besides porting stuff, I’ve also implemented a new legendary potion, that should enable some interesting strategies.

Update: I’ve adjusted the lore of the card. Thanks Jhoi Ashleigh for your suggestions!

All towers ported!

Yay! I’ve ported all towers to the new engine.

Now I’m starting with items, which hopefully should be much faster.

For a change, I’ve added a new item this evening instead of porting an existing one 🙂

Artwork is not final yet, but the stats will be somewhat like that:

Unit testing game logic

I’m currently rewriting Mazebert in Java with the goal to add multiplayer. In order to do this, I need to decouple all game logic code from client code. The game logic is then a separate module and runs a deterministic simulation in its own thread.

While being quite the opposite of what game programming was for me all these years – stuffing everything into the scene graph of the used rendering framework – it brings a lot of opportunities in terms of programming. One of them is the ability to easily unit test all game logic. When porting the nature towers of the game, I’ve written unit tests for almost every tower, and it’s been super fun. I was even able to understand and fix some problems of the old game:

  • Frog poison was doing less than the promised 100% DoT
  • Kills from frog DoT did not drop cards
  • Huli’s aura says all femal towers in range, but did not scale with range changing items
  • Balu’s cuddle did not scale with attack speed modifier

For example, here’s the unit test code for Holgar the Horrible:

class VikingTest extends SimTest {
    RandomPluginTrainer randomPluginTrainer = new RandomPluginTrainer();
    DamageSystemTrainer damageSystemTrainer;

    Wizard wizard;
    Viking viking;
    Creep creep;

    @BeforeEach
    void setUp() {
        simulationListeners = new SimulationListeners();
        randomPlugin = randomPluginTrainer;
        unitGateway = new UnitGateway();
        projectileGateway = new ProjectileGateway();
        damageSystem = damageSystemTrainer = new DamageSystemTrainer(simulationListeners);
        lootSystem = new LootSystemTrainer();

        commandExecutor = new CommandExecutor();
        commandExecutor.init();

        wizard = new Wizard();
        unitGateway.addUnit(wizard);

        viking = new Viking();
        viking.setWizard(wizard);
        unitGateway.addUnit(viking);

        creep = new Creep();
        unitGateway.addUnit(creep);
    }

    @Test
    void attack_canHitSameCreepWithTwoAxesAtOnce() {
        whenVikingAttacks();

        assertThat(creep.getHealth()).isEqualTo(80);
    }

    @Test
    void attack_canHitTwoCreeps() {
        Creep anotherCreep = new Creep();
        unitGateway.addUnit(anotherCreep);

        whenVikingAttacks();

        assertThat(creep.getHealth()).isEqualTo(90);
        assertThat(anotherCreep.getHealth()).isEqualTo(90);
    }

    @Test
    void kill_canDropMead_drop() {
        randomPluginTrainer.givenFloatAbs(0.0f);
        damageSystemTrainer.givenConstantDamage(1000);

        whenVikingAttacks();

        assertThat(wizard.potionStash.get(PotionType.Mead).getAmount()).isEqualTo(1);
    }

    @Test
    void kill_canDropMead_noDrop() {
        randomPluginTrainer.givenFloatAbs(0.9f);
        damageSystemTrainer.givenConstantDamage(1000);

        whenVikingAttacks();

        assertThat(wizard.potionStash.get(PotionType.Mead)).isNull();
    }

    @Test
    void kill_canDropMead_badDropChanceOfCreep_noDrop() {
        randomPluginTrainer.givenFloatAbs(0.0f);
        damageSystemTrainer.givenConstantDamage(1000);
        creep.setDropChance(0);

        whenVikingAttacks();

        assertThat(wizard.potionStash.get(PotionType.Mead)).isNull();
    }

    @Test
    void drinksMead() {
        givenMeadPotionIsDrank();
        givenMeadPotionIsDrank();
        givenMeadPotionIsDrank();

        CustomTowerBonus bonus = new CustomTowerBonus();
        viking.populateCustomTowerBonus(bonus);
        assertThat(bonus.title).isEqualTo("Mead:");
        assertThat(bonus.value).isEqualTo("3");
    }

    @Test
    void drinksNoMead() {
        CustomTowerBonus bonus = new CustomTowerBonus();
        viking.populateCustomTowerBonus(bonus);
        assertThat(bonus.title).isEqualTo("Mead:");
        assertThat(bonus.value).isEqualTo("0");
    }

    @Test
    void drinksMead_hasEffects() {
        givenMeadPotionIsDrank();
        assertThat(viking.getChanceToMiss()).isEqualTo(0.01f);
        givenMeadPotionIsDrank();
        assertThat(viking.getChanceToMiss()).isEqualTo(0.02f);
    }

    private void givenMeadPotionIsDrank() {
        wizard.potionStash.add(PotionType.Mead);
        DrinkPotionCommand command = new DrinkPotionCommand();
        command.potionType = PotionType.Mead;
        command.playerId = wizard.getPlayerId();
        commandExecutor.executeVoid(command);
    }

    private void whenVikingAttacks() {
        viking.simulate(3.0f); // attack
        projectileGateway.simulate(1.0f); // projectile spawning
        projectileGateway.simulate(1.0f); // projectile hitting
    }
}

And here is the production code of the tower:

public strictfp class Viking extends Tower {

    public Viking() {
        setBaseCooldown(3.0f);
        setBaseRange(2.0f);
        setAttackType(AttackType.Fal);
        setStrength(0.89f);
        setDamageSpread(0.16f);
        setGender(Gender.Male);
        setElement(Element.Nature);

        addAbility(new ProjectileDamageAbility(ProjectileViewType.Axe, 6));
        addAbility(new VikingMultishot());
        addAbility(new VikingMead());
    }

    @Override
    protected float getGoldCostFactor() {
        return 1.1f;
    }

    @Override
    public String getName() {
        return "Holgar the Horrible";
    }

    @Override
    public String getDescription() {
        return "Holgar is the terror of the seven seas with a slight alcohol problem.";
    }

    @Override
    public String getAuthor() {
        return "Holger Herbricht";
    }

    @Override
    public Rarity getRarity() {
        return Rarity.Rare;
    }

    @Override
    public int getItemLevel() {
        return 50;
    }

    @Override
    public String getSinceVersion() {
        return "0.3";
    }

    @Override
    public String getModelId() {
        return "viking";
    }

    @Override
    public int getImageOffsetOnCardY() {
        return 8;
    }

    @Override
    public void populateCustomTowerBonus(CustomTowerBonus bonus) {
        bonus.title = "Mead:";
        bonus.value = "" + getConsumedMeadCount();
    }

    private int getConsumedMeadCount() {
        MeadAbility meadPotionAbility = getAbility(MeadAbility.class);
        if (meadPotionAbility == null) {
           return 0;
        } else {
            return meadPotionAbility.getStackCount();
        }
    }
}

With the ability to attack two targets:

public strictfp class VikingMultishot extends AttackAbility {

    public VikingMultishot() {
        super(2, true);
    }

    @Override
    public boolean isVisibleToUser() {
        return true;
    }

    @Override
    public String getTitle() {
        return "Double Throw";
    }

    @Override
    public String getDescription() {
        return "Holgar throws two axes at once. He likes the sound of two cracking skulls.";
    }

    @Override
    public String getIconFile() {
        return "0026_axeattack2_512";
    }
}

And the ability to find mead:

public strictfp class VikingMead extends Ability<Tower> implements OnKillListener {

    private static final float CHANCE = 0.01f;

    private final LootSystem lootSystem = Sim.context().lootSystem;

    @Override
    protected void initialize(Tower unit) {
        super.initialize(unit);
        unit.onKill.add(this);
    }

    @Override
    protected void dispose(Tower unit) {
        unit.onKill.remove(this);
        super.dispose(unit);
    }

    @Override
    public void onKill(Creep target) {
        if (getUnit().isAbilityTriggered(CHANCE * StrictMath.min(1, target.getDropChance()))) {
            Wizard wizard = getUnit().getWizard();
            lootSystem.dropCard(wizard, target, wizard.potionStash, PotionType.Mead);
        }
    }

    @Override
    public boolean isVisibleToUser() {
        return true;
    }

    @Override
    public String getTitle() {
        return "Drink for Victory";
    }

    @Override
    public String getDescription() {
        return "Whenever Holgar kills a creep, there is a " + format.percent(CHANCE) + "% chance he finds a bottle of mead.";
    }

    @Override
    public String getIconFile() {
        return "9005_MeadPotion";
    }
}

New forum ✌️

Heya!

it’s been a while, but I’ve recently found the time to write a new forum. It’s no longer bbpress, but a self written solution. Downside: no deep integration into wordpress. Upside: no more spammers and deep integration into the game!

What does this mean? Everybody is allowed to read. Only players of the game are allowed to write! You can login with your IGN and savecode.

Hope to see you at the new forum!

PS: This game is not dead yet. I’m currently working on a complete rewrite with proper multiplayer support.

Cheers
Andy

Forum closed to readonly mode

Due to heavy spammer attacks the forum is currently in readonly mode.

I’ve cleaned the database from all 30.000 spam posts (why do people post this disgusting stuff?).

Currently I’m looking for ways to prevent further spam in the forum. Akismet Anti-Spam doesn’t seem to be enough anymore.

Version 1.4.1

A small maintenance release was pushed to Google Play and the iOS App Store:

  • Upgrade to the latest SDKs
  • Fix a bonus round exploit
  • Fix crash in certain situations, by preventing Knusperhexe from eating revive mass creeps

Christmas Present

This weekend, a special present will wait for you at the black market! I’ve heard it’s a pretty bad-ass sword.

What might it be?

What might it be?

Wish you all a happy and peaceful Christmas time!

PS: Just noticed this morning that 59 players already purchased a black market item for 250 this night. You’ve got your 250 relics back and will get a present, too.