abstract class Enemy { float lissa_x; float lissa_y; float lissa_z; float radius; color fill_color; color stroke_color; String name; Enemy allegiance; int threat_level; boolean is_boss; int map_x; int map_y; boolean is_dead; float wander_speed = 1.0; float wander_timer = 0; PVector pos; float heading; float target_heading; PVector recoil; float base_speed = 1; float base_turn = 0.015; float recoil_mult = 1; float recoil_recover = 0.02; float aggro_radius = 200; float attack_radius = 30; int max_life = 4; int life; int defense; ArrayList current_attack; ArrayList basic_attack; int attack_timer; int stun_time; void tickTime(float time_passed) { wander_timer += wander_speed * time_passed * MOVEMENT_SCALING; if(kobolds[map_x][map_y] != 0) kobold_peril[map_x][map_y] += threat_level * time_passed * THREAT_SCALING; } boolean mapWander() { wander_timer--; int old_x = map_x; int old_y = map_y; int randomX = 0; int randomY = 0; int new_x; int new_y; int randomDir = (int) random(5); if(randomDir == 0) randomX = 1; if(randomDir == 1) randomX = -1; if(randomDir == 2) randomY = 1; if(randomDir == 3) randomY = -1; new_x = map_x + randomX; new_y = map_y + randomY; if(new_x < 0) new_x = 1; if(new_y < 0) new_y = 1; if(new_x >= WORLD_W) new_x = WORLD_W - 2; if(new_y >= WORLD_H) new_y = WORLD_H - 2; //bounce off sides if(map_threat[new_x][new_y] < map_threat[map_x][map_y]) { map_x = new_x; map_y = new_y; } calculateThreat(); if(old_x != map_x || old_y != map_y) { if(!muted) marchSound.trigger(); return true; } return false; } void skyDraw() { if(stun_time <= 0 || (draw_frame / 1) % 2 == 0) { attackDraw(); fill(fill_color); stroke(stroke_color); strokeWeight(2); ellipse(pos.x, pos.y, radius, radius); } } void mapDrawShadow() { noStroke(); strokeWeight(0); fill(blendColors(color(0), GROUND_COLOR, 0.5)); float orbitphase = 100 + draw_frame * 0.05; ellipse(xcen(map_x) + 0.9* TILE_W / 2 * sin(lissa_x * orbitphase), ycen(map_y) + 0.9*TILE_H / 3 * sin(lissa_y * orbitphase), radius/4, radius/4); } void mapDraw() { stroke(stroke_color); strokeWeight(1); fill(fill_color); float orbitphase = 100 + draw_frame * 0.05; ellipse(xcen(map_x) + 0.9 *TILE_W / 2 * sin(lissa_x * orbitphase), ycen(map_y) + 0.9* TILE_H / 3 * sin(lissa_y * orbitphase) - 32 + 16*sin(lissa_z * orbitphase), radius/4, radius/4); } int smack(int strength, PVector direction) { int dam = strength - defense; if(!current_attack.isEmpty()) { AttackElement ae = (AttackElement) current_attack.get(0); dam -= ae.defense_bonus; } if(dam < 0) dam = 0; if(stun_time <= 0) { life -= dam; if(life < 0) life = 0; if(direction != null) { recoil.add(PVector.mult(direction, recoil_mult)); } stun_time = STUN_DURATION; if(life == 0) { stun_time = THROES_DURATION; if(is_boss) music.pause(); } ///TODO being-hit effects; showDamage(pos, dam); if(dam > 0) if(!muted) hitSound.trigger(); } else dam = -1; last_hit_enemy = this; return dam; } void skyTick() { steerThink(); if(life > 0) attackThink(); else current_attack.clear(); if(stun_time > 0) stun_time --; if(stun_time <= 0 && life == 0) is_dead = true; } void skyEntry() { current_attack.clear(); heading = PI; life = max_life; stun_time = 0; } void steerThink() { float speed = base_speed; float turning = base_turn; if(!current_attack.isEmpty()) { //attack adjustments for speed and turning here AttackElement front = (AttackElement) current_attack.get(0); speed *= front.speed_mult; turning *= front.turning_mult; } if(cos(heading) > 0) target_heading = 0; else target_heading = PI; if(player_pos.x - pos.x > WIDTH*3) target_heading = 0; if(player_pos.x - pos.x < -WIDTH*3) target_heading = PI; if(inRadius(player_pos, pos, aggro_radius)) { PVector toPlayer = PVector.sub(player_pos, pos); target_heading = atan2(toPlayer.y, toPlayer.x); } if(pos.y < 0) target_heading = HALF_PI; if(pos.y > HEIGHT) target_heading = -HALF_PI; heading = angleTowards(heading, target_heading, turning); if(life > 0) //no voluntary movement if we're dead pos.add(new PVector(cos(heading)*speed, sin(heading)*speed)); pos.add(recoil); if(recoil.mag() < recoil_recover) recoil.x = recoil.y = 0; else { PVector recoil_cancel = new PVector(recoil.x, recoil.y); recoil_cancel.normalize(); recoil_cancel.mult(-recoil_recover); recoil.add(recoil_cancel); } } void attackThink() { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); if(attack_timer == 0) { if(front.announcement != null) callAttack(pos, front.announcement); if(front.sound != null) if(!muted) front.sound.trigger(); } PVector attack_pos = new PVector(pos.x + cos(heading)*front.hitbox_dist,pos.y + sin(heading)*front.hitbox_dist); if(inRadius(attack_pos, player_pos, front.hitbox_size + DRAGONDOT_RADIUS)) { smackPlayer(front.damage, new PVector(cos(heading)*front.knockback, sin(heading)*front.knockback)); } //friendly fire Iterator ie = battle_enemies.iterator(); while(ie.hasNext()) { Enemy e = (Enemy) ie.next(); if(e != this) { if(inRadius(attack_pos, e.pos, front.hitbox_size + e.radius)) { e.smack(front.damage, new PVector(cos(heading)*front.knockback, sin(heading)*front.knockback)); } } } attack_timer ++; if(attack_timer > front.duration){ current_attack.remove(0); attack_timer = 0; } } else if(inRadius(pos, player_pos, attack_radius)) { attack_timer = 0; current_attack.addAll(basic_attack); } ///TODO attack collision processing } void attackDraw() { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); pushMatrix(); translate(pos.x, pos.y); translate(cos(heading)*front.hitbox_dist,sin(heading)*front.hitbox_dist); front.draw(attack_timer / (float)front.duration); popMatrix(); } } Enemy(int mapx, int mapy) { map_x = mapx; map_y = mapy; lissa_x = random(0.5)+0.5; lissa_y = random(0.5)+0.5; lissa_z = random(0.5)+0.5; heading = PI; recoil = new PVector(0,0); current_attack = new ArrayList(); basic_attack = new ArrayList(); String name; if(map_x == 0 && map_y == 0) allegiance = bossNW; if(map_x == 0 && map_y == WORLD_H-1) allegiance = bossSW; if(map_x == WORLD_W-1 && map_y == 0) allegiance = bossNE; if(map_x == WORLD_W-1 && map_y == WORLD_H-1) allegiance = bossSE; } } class Buzzard extends Enemy { Buzzard(int mapx, int mapy) { super(mapx, mapy); name = "Buzzard"; radius = 12; fill_color = color(46, 96, 43); stroke_color = color(7, 175, 87); threat_level = 5; is_boss = false; aggro_radius = 300; attack_radius = 70; max_life = 2; //allegiance = theTestBoss; basic_attack.add(new Idle(60)); basic_attack.add(new Peck(30, 1, radius)); basic_attack.add(new Idle(60)); } } class DartHawk extends Enemy { DartHawk(int mapx, int mapy) { super(mapx, mapy); name = "Dart Hawk"; radius = 9; fill_color = color(96, 96, 43); stroke_color = color(50, 20, 0); threat_level = 3; is_boss = false; aggro_radius = WIDTH*2; attack_radius = 45; base_speed = 2; base_turn *= 1.7; max_life = 1; wander_speed = 1.0; //allegiance = theTestBoss; basic_attack.add(new Idle(15)); basic_attack.add(new Peck(30, 1, radius)); basic_attack.add(new Idle(90)); } } class MirrorBird extends Enemy { MirrorBird(int mapx, int mapy) { super(mapx, mapy); name = "Mirror Bird"; radius = 16; fill_color = color(230, 230, 120); stroke_color = color(160, 160, 255); threat_level = 10; is_boss = false; aggro_radius = 300; attack_radius = 70; max_life = 2; //allegiance = theTestBoss; basic_attack.add(new Idle(60)); basic_attack.add(new Peck(30, 2, radius)); basic_attack.add(new Idle(60)); } int smack(int strength, PVector direction) { if(fireball_pos != null && fireball_life > 0 && inRadius(fireball_pos, pos, FIREBALL_RADIUS + radius)) { if(!fireball_hostile) { fireball_hostile = true; fireball_vel.mult(-1); fireball_life = FIREBALL_MAX_LIFE; if(!muted) reflectSound.trigger(); } return super.smack(0, new PVector(0, 0)); } else return super.smack(strength, direction); } } class AirMine extends Enemy { AirMine(int mapx, int mapy) { super(mapx, mapy); name = "Airmine"; radius = 12; fill_color = color(128, 128, 128); stroke_color = color(192, 0, 0); threat_level = 5; is_boss = false; aggro_radius = 1000; attack_radius = radius + DRAGONDOT_RADIUS + 5; base_speed *= 0.2; base_turn *= 100; max_life = 1; wander_speed = 0.75; defense = 10; //allegiance = theTestBoss; basic_attack.add(new Aim(40)); basic_attack.add(new Explode(40)); } void attackThink() { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); if(attack_timer == front.duration && front instanceof Explode) { is_dead = true; } } super.attackThink(); } int smack(int strength, PVector direction) { if(current_attack.isEmpty()) { current_attack.addAll(basic_attack); attack_timer = 0; } return super.smack(strength, direction); } } class SkyLancer extends Enemy { SkyLancer(int mapx, int mapy) { super(mapx, mapy); name = "Sky Lancer"; radius = 15; fill_color = color(128, 168, 208); stroke_color = color(100, 120, 140); threat_level = 10; is_boss = false; aggro_radius = 400; attack_radius = 150; max_life = 4; base_speed *= 1.2; base_turn *= 1.5; wander_speed = 0.75; //allegiance = theTestBoss; basic_attack.add(new Aim(100)); basic_attack.add(new Lance(60, 1, radius)); basic_attack.add(new Idle(90)); } } class Drakeling extends Enemy { Drakeling(int mapx, int mapy) { super(mapx, mapy); name = "Drakeling"; radius = 13; fill_color = color(230, 50, 20); stroke_color = color(100, 30, 0); threat_level = 15; is_boss = false; aggro_radius = 400; attack_radius = 90; max_life = 3; defense = 1; base_speed *= 0.7; base_turn *= 1; wander_speed = 0.75; //allegiance = theTestBoss; basic_attack.add(new Idle(40)); basic_attack.add(new PteroSwipe(30, 2, radius)); basic_attack.add(new Idle(80)); } } abstract class Boss extends Enemy { boolean encountered = false; boolean had_chat = false; ArrayList chat; Boss(int mapx, int mapy) { super(mapx, mapy); chat = new ArrayList(); wander_speed = 0; } void steerThink() { if(encountered) super.steerThink(); if(inRadius(player_pos, pos, aggro_radius) && !encountered) { encountered = true; if(!had_chat) { dialog_queue.addAll(chat); had_chat = true; music.pause(); } else bgm("duel_music.mp3"); } } int smack(int strength, PVector direction) { if(encountered) return super.smack(strength, direction); return -1; } void skyEntry() { super.skyEntry(); encountered = false; } boolean recruit() { if(is_dead) return false; if(map_threat[map_x][map_y] < 100) { Enemy e ; e = randomNewEnemy(); e.tickTime(-random(RECRUIT_DELAY)); all_enemies.add(e); calculateThreat(); return true; } else return false; } abstract Enemy randomNewEnemy(); } class BigBuzzard extends Boss { BigBuzzard(int mapx, int mapy) { super(mapx, mapy); name = "Grand Roc"; radius = 36; fill_color = color(255, 235, 210); stroke_color = color(178, 168, 148); threat_level = 20; is_boss = true; aggro_radius = 450; attack_radius = 80; defense = 0; recoil_mult = 0.1; base_speed *= 1.2; base_turn *= 1.2; max_life = 20; wander_speed = 0; chat.add(new DialogItem("Kobolds are tasty!", this)); chat.add(new DialogItem("...that may be true, but it's\nstill my job to protect them!")); basic_attack.add(new Idle(30)); basic_attack.add(new Peck(30, 3, radius)); basic_attack.add(new Idle(60)); } void steerThink() { if(encountered) aggro_radius = WIDTH*2; else aggro_radius = 450; super.steerThink(); } Enemy randomNewEnemy() { return new DartHawk(map_x, map_y); } } class Wyvern extends Boss { Wyvern(int mapx, int mapy) { super(mapx, mapy); name = "Dark Wyvern"; radius = 36; fill_color = color(80, 60, 100); stroke_color = color(60, 20, 0); threat_level = 20; is_boss = true; aggro_radius = 450; defense = 2; base_speed *= 0.5; attack_radius = 90; recoil_mult = 0.3; max_life = 12; wander_speed = 0; chat.add(new DialogItem("Whoa, what's your problem?")); chat.add(new DialogItem("A whelp like you is unfit\nto rule this domain!", this)); chat.add(new DialogItem("We'll just see about that!")); basic_attack.add(new Aim(60)); basic_attack.add(new PteroSwipe(18, 2, 34)); basic_attack.add(new Aim(30)); basic_attack.add(new PteroSwipe(18, 2, 34)); basic_attack.add(new FireWarn(520)); basic_attack.add(new Aim(9)); basic_attack.add(new FireBreath(40)); basic_attack.add(new Idle(90)); } Enemy randomNewEnemy() { return new Drakeling(map_x, map_y); } void attackThink() { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); if(attack_timer == 0 && front instanceof FireBreath) { PVector attack_pos = new PVector(pos.x + cos(heading)*front.hitbox_dist,pos.y + sin(heading)*front.hitbox_dist); for(int i = 0; i < 20; i++) particles.add(new Ember(PVector.add(attack_pos, randomVector(random(front.hitbox_size*0.9))))); } } super.attackThink(); } } class Turtle extends Boss { ArrayList offense_mode; ArrayList defense_mode; Turtle(int mapx, int mapy) { super(mapx, mapy); name = "Ironshell"; radius = 36; fill_color = color(130, 180, 100); stroke_color = color(64, 128, 32); threat_level = 25; is_boss = true; aggro_radius = 450; defense = 0; attack_radius = 400; max_life = 12; base_speed = 0.25; recoil_mult = 0.25; wander_speed = 0; chat.add(new DialogItem("I am unstoppable!", this)); chat.add(new DialogItem("Yeah. Sure.")); defense_mode = new ArrayList(); offense_mode = new ArrayList(); defense_mode.add(new Warn(60)); defense_mode.add(new Shell(9000)); offense_mode.add(new Idle(180)); offense_mode.add(new Warn(120)); offense_mode.add(new BlastWave(30, 40)); offense_mode.add(new BlastWave(30, 80)); offense_mode.add(new BlastWave(30, 120)); offense_mode.add(new BlastWave(30, 160)); offense_mode.add(new BlastWave(30, 200)); offense_mode.add(new BlastWave(30, 240)); offense_mode.add(new BlastWave(30, 280)); offense_mode.add(new BlastWave(30, 320)); offense_mode.add(new BlastWave(30, 360)); offense_mode.add(new BlastWave(30, 400)); offense_mode.add(new BlastWave(30, 440)); offense_mode.add(new Idle(360)); basic_attack = offense_mode; } Enemy randomNewEnemy() { if(random(3) < 1) return new MirrorBird(map_x, map_y); else return new AirMine(map_x, map_y); } void attackThink() { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); if(inRadius(pos, player_pos, 120) || (fireball_life > 0) ) { if(front instanceof Idle || front instanceof BlastWave) { current_attack.clear(); attack_timer = 0; current_attack.addAll(defense_mode); } } else if(front instanceof Shell) { current_attack.clear(); attack_timer = 0; current_attack.addAll(offense_mode); } } super.attackThink(); } int smack(int strength, PVector direction) { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); if(fireball_pos != null && fireball_life > 0 && inRadius(fireball_pos, pos, FIREBALL_RADIUS + radius) && front instanceof Shell) { if(!fireball_hostile) { fireball_hostile = true; fireball_vel.mult(-1); fireball_life = FIREBALL_MAX_LIFE; if(!muted) reflectSound.trigger(); } return super.smack(0, new PVector(0, 0)); } } return super.smack(strength, direction); } } class Pterodactyl extends Boss { ArrayList lunge_attack; ArrayList swipe_attack; boolean lunging; Pterodactyl(int mapx, int mapy) { super(mapx, mapy); name = "Pterodactyl"; radius = 36; fill_color = color(240, 144, 80); stroke_color = color(144, 64, 0); threat_level = 25; is_boss = true; aggro_radius = 450; defense = 4; attack_radius = 400; max_life = 16; base_speed = 0.75; recoil_mult = 0.75; wander_speed = 0; chat.add(new DialogItem("Rragarrh!", this)); chat.add(new DialogItem("Yikes!")); swipe_attack = new ArrayList(); lunge_attack = new ArrayList(); swipe_attack.add(new Idle(12)); swipe_attack.add(new PteroSwipe(18, 2, 24)); swipe_attack.add(new Idle(90)); lunge_attack.add(new Warn(180)); lunge_attack.add(new PteroLunge(60)); lunge_attack.add(new PteroSwipe(18, 3, 24)); lunge_attack.add(new Idle(120)); basic_attack = lunge_attack; } void attackThink() { if(!current_attack.isEmpty()) { AttackElement front = (AttackElement) current_attack.get(0); if((front instanceof Warn) && inRadius(pos, player_pos, 80)) { current_attack.clear(); attack_timer = 0; current_attack.addAll(swipe_attack); } } super.attackThink(); } int smack(int strength, PVector direction) { int r = super.smack(strength, direction); if(r > 0) { current_attack.clear(); current_attack.add(new Idle(180)); } return r; } Enemy randomNewEnemy() { if(random(3) < 1) return new SkyLancer(map_x, map_y); else return new Buzzard(map_x, map_y); } } ////////////////////////////////////ATTACK ELEMENTS/////////// class AttackElement { int duration; int defense_bonus = 0; float speed_mult = 1; float turning_mult = 1; float hitbox_size = 0; float hitbox_dist = 0; int damage = 0; float knockback = 0; String announcement; AudioSample sound; void draw(float progress) {} } class Aim extends AttackElement { Aim(int time) { speed_mult = 0; turning_mult = 10000; duration = time; } } class Shell extends AttackElement { Shell(int time) { speed_mult = 0; turning_mult = 0; duration = time; defense_bonus = 10; announcement = "reflect shell!"; sound = dashReady; } void draw(float progress) { fill(0, 255, 127+64*((draw_frame / 2)%3)); noStroke(); ellipse(0, 0, 42, 42); } } class BlastWave extends AttackElement { BlastWave(int time, float distance) { damage = 3; duration = time; hitbox_dist = distance; hitbox_size = 50; knockback = 0; speed_mult = 0; turning_mult = 0.5; sound = fireSpit2; } void draw(float progress) { fill(255, 255*(1-progress), 255*(1-2*progress), 255*(1-progress)); noStroke(); ellipse(0, 0, hitbox_size, hitbox_size); } } class Idle extends AttackElement { Idle(int time) { duration = time; } } class Warn extends AttackElement { Warn(int time) { duration = time; speed_mult = 0; turning_mult = 5; } void draw(float progress) { strokeWeight(2); stroke(255); noFill(); drawCharge(0, 0, progress, 90, 36, 4); } } class FireWarn extends AttackElement { FireWarn(int time) { duration = time; speed_mult = 0; turning_mult = 5; sound = fireCharge; defense_bonus = -2; } void draw(float progress) { strokeWeight(2); noStroke(); fill(255, 127+64*((draw_frame / 2)%3), 0); drawCharge(0, 0, progress, 90, 36, 5); } } class FireBreath extends AttackElement { FireBreath(int time) { damage = 4; duration = time; hitbox_dist = 170; hitbox_size = 170; announcement = "fire breath!"; knockback = 8; speed_mult = 0; turning_mult = 0; sound = fireSpit2; } void draw(float progress) { fill(255, 255*(1-progress), 0, 2*255*(1-progress)); noStroke(); ellipse(0, 0, hitbox_size, hitbox_size); } } class Dash extends AttackElement { Dash(int time, float speedmult) { speed_mult = speedmult; turning_mult = 0; duration = time; } } class PteroLunge extends AttackElement { PteroLunge(int time) { speed_mult = 8; turning_mult = 1.5; defense_bonus = -6; duration = time; sound = dashGo; announcement = "lunge!"; } } class PteroSwipe extends AttackElement { PteroSwipe(int time, int power, float scope) { damage = power; duration = time; hitbox_dist = scope; hitbox_size = scope * 1.5; announcement = "talon swipe!"; knockback = power * 2; speed_mult = 0; turning_mult = 0; sound = clawSound; } void draw(float progress) { stroke(255, 255*(1-progress)); noFill(); strokeWeight(2); ellipse(0, 0, hitbox_size, hitbox_size); ellipse(0, 0, hitbox_size* 0.8, hitbox_size*0.8); ellipse(0, 0, hitbox_size*0.6, hitbox_size*0.6); } } class Peck extends AttackElement { Peck(int time, int power, float scope) { damage = power; duration = time; hitbox_dist = scope; hitbox_size = scope * 0.75; announcement = "peck!"; knockback = power; speed_mult = 1; turning_mult = 0; sound = peckSound; } void draw(float progress) { fill(255, 255*(1-progress)); noStroke(); ellipse(0, 0, hitbox_size, hitbox_size); } } class Explode extends AttackElement { Explode(int time) { damage = 4; duration = time; hitbox_dist = 0; hitbox_size = 100; announcement = "explode!"; knockback = 0; speed_mult = 0; turning_mult = 0; sound = fireSpit2; } void draw(float progress) { fill(255, 255*(1-progress), 255*(1-2*progress), 255*(1-progress)); noStroke(); ellipse(0, 0, hitbox_size, hitbox_size); } } class Lance extends AttackElement { Lance(int time, int power, float scope) { damage = power; duration = time; hitbox_dist = scope; hitbox_size = scope * 0.75; announcement = "lance!"; knockback = power; speed_mult = 3; turning_mult = 0; sound = peckSound; } void draw(float progress) { fill(255, 255*(1-progress)); noStroke(); ellipse(0, 0, hitbox_size, hitbox_size); } }