"use strict";
/* skychild Raviene; world's most powerful sorceress, world's most neurotic satisfaction complex */
var GO_Raviene = function(){
	this.sprite = new Sprite(GO_Raviene.spriteSheet,"Raviene");
	this.sprite.collision_flags = 
		DEFINE_SPRITE_COLLISION_FLAGS.GROUND |
		DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_ACTIVE |
		DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_PASSIVE;
	this.sprite.setBoundingBox(2,0,13,15);
	this.sprite.game_object = this; // no longer a generic GO

	this.controls = new Controls(); // just to init

	this.horizontal_move_index=0;
	this.vertical_move_index=0;
	this.jump_index = 0;
	this.fall_index = 0;
	this.grounded = false;
	this.in_water_cache = false;
	this.anim_state = this.ANIM_STATES.fall;
	this.fall_air = this.fall_air_fast;
	this.frame_timer=0;

	this.direction = 1; // +/-1 for facing

	this.casting_cooldown = 0;
	this.flying = false;
	this.flight_timer = 0;
	this.hurt = false;

	this.spell = this.SPELLS.KILL; //temp
	this.spell_target = null; //temp
	this.spell_target_name = "Hedgeboar"; // temp
	this.spell_cooldown = 0;
	this.spell_timer = 0;
	this.spellshots = [];

	this.treasuresOverlay = new Sprite(GO_Raviene.treasuresSheet,"Raviene_treasures");
	this.treasuresOverlay.game_object = this;

	// easter egg
	if (GO_Raviene.costumes[SDS.loadDate.getMonth()][SDS.loadDate.getDate()]){
		if (!GO_Raviene.costumeImg){
			GO_Raviene.costumeImg = SDS_Load_Image(GO_Raviene.costumes[SDS.loadDate.getMonth()][SDS.loadDate.getDate()]);
			GO_Raviene.costumeSheet = new TileSheet(4, 4, 2, 3, GO_Raviene.costumeImg);
		}
	}

	this.costumeOverlay = null;
	if (GO_Raviene.costumeSheet){
		this.costumeOverlay = new Sprite(GO_Raviene.costumeSheet,"Raviene_costume");
		this.costumeOverlay.game_object = this;
	}

	this.bubbleOverlay = new Sprite(GO_Raviene.bubbleSheet,"Raviene_bubble");
	this.bubbleOverlay.game_object = this;

	this.AI = {
		active: false,
		STATES: {
			REACT: 0,
			JUMP: 1,
			CAST: 2,
			RECOIL: 3,
			END: 4,
		},
		state: 0,
		state_timer: 0,

		meli: null,
		meli_find: false,
		//meli_controls: null;
		hp: 10,
		shield_strength: 5,
		last_shield: null,
		cornered: false,

		wave_watch: 0,
		waves: [],

		harmed: false,
	};

	this.SUP = {
		active: false,

		meli: null,
		meli_find: false,

		//waypoints: [],

		v_force_dir: 0,
		v_force_count: 0,
		v_force_limit: 2,

		h_force_dir: 0,
		h_force_count: 0,
		h_force_limit: 8,

		//again: 0,
	};
};
GO_Raviene.spriteImg = SDS_Load_Image("art/sheets/sprite/sorceress.png");
GO_Raviene.spriteSheet = new TileSheet(4, 4, 2, 3, GO_Raviene.spriteImg);

GO_Raviene.treasuresImg = SDS_Load_Image("art/sheets/sprite/sorceress_treasures.png");
GO_Raviene.treasuresSheet = new TileSheet(4, 4, 2, 3, GO_Raviene.treasuresImg);

GO_Raviene.bubbleImg = SDS_Load_Image("art/sheets/sprite/bubble.png");
GO_Raviene.bubbleSheet = new TileSheet(4, 4, 0, 0, GO_Raviene.bubbleImg);

GO_Raviene.costumeImg = null;
GO_Raviene.costumeSheet = null;
GO_Raviene.costumes = {};
GO_Raviene.costumes[0] = {};
GO_Raviene.costumes[1] = {};
GO_Raviene.costumes[2] = {};
GO_Raviene.costumes[3] = {};
GO_Raviene.costumes[4] = {};
GO_Raviene.costumes[5] = {};
GO_Raviene.costumes[6] = {};
GO_Raviene.costumes[7] = {};
GO_Raviene.costumes[8] = {};
GO_Raviene.costumes[9] = {};
GO_Raviene.costumes[10] = {};
GO_Raviene.costumes[11] = {};
// 2017 Women's Rights March
GO_Raviene.costumes[0][21] = "art/sheets/sprite/sorceress_pcap.png";

GO_Raviene.prototype = new GameObject(
	new Sprite(
		GO_Raviene.spriteSheet,
		"Raviene"
	),
	"Raviene"
	);

	GO_Raviene.prototype.interactions = 
		DEFINE_GAMEOBJECT_INTERACTIONS.CONTROL |
		DEFINE_GAMEOBJECT_INTERACTIONS.CHECK;

	// NPC walk controls
	GO_Raviene.prototype.leftward_move_normal = [0,-1,-2];
	GO_Raviene.prototype.rightward_move_normal = [0,1,2];
	GO_Raviene.prototype.leftward_move_fast = [0,-1,-2,-3];
	GO_Raviene.prototype.rightward_move_fast = [0,1,2,3];

	GO_Raviene.prototype.leftward_move_land = GO_Raviene.prototype.leftward_move_normal;
	GO_Raviene.prototype.rightward_move_land = GO_Raviene.prototype.rightward_move_normal;
	GO_Raviene.prototype.leftward_move_water = [0,-1];
	GO_Raviene.prototype.rightward_move_water = [0,1];
	GO_Raviene.prototype.jump_rise_air_normal = [ //40px = 2.5 blocks
		-4,-4,-4,-4,-4,-4,
		-3,-3,-3, 
		-2,-2,-2,
		-1,
		0, 0];
	GO_Raviene.prototype.jump_rise_air_boss = [ //72px = 4.5 blocks
		-5,-5,-5,-5,-5,-5,
		-4,-4,-4,-4,-4,
		-3,-3,-3,-3,-3,
		-2,-2,-2,
		-1,
		0, 0];
	GO_Raviene.prototype.jump_rise_air = GO_Raviene.prototype.jump_rise_air_normal;
	GO_Raviene.prototype.fall_air_slow = [ 
		1, 
		2, 2, 2, 
		3, 3, 3]; // truncated at 3 because wings
	GO_Raviene.prototype.fall_air_fast = [ 
		1, 
		2, 2, 2, 
		3, 3, 3, 
		4, 4, 4, 4, 4, 4, 
		5, 5, 5, 5, 5, 5,
		6, 6, 6, 6, 6, 6,
		7, 7, 7, 7, 7, 7, 
		8];
	GO_Raviene.prototype.jump_rise_water = [ //8px = .5 blocks
		-2, -2, -2, 
		-1, -1, 
		0]; // maybe a bird; not a duck.
	GO_Raviene.prototype.fall_water = [ 
		-1]; // naturally buoyant; "fall" towards surface

	// SimpleMove (fly) controls
	GO_Raviene.prototype.leftward_move = GO_Raviene.prototype.leftward_move_land;
	GO_Raviene.prototype.rightward_move = GO_Raviene.prototype.rightward_move_land;
	GO_Raviene.prototype.upward_move = GO_Raviene.prototype.leftward_move_land;
	GO_Raviene.prototype.downward_move = GO_Raviene.prototype.rightward_move_land;


	GO_Raviene.prototype.ANIM_STATES = {
		"stand" : 0,
		"walk" : 1,
		"jump" : 2,
		"fall" : 3,
		"cast" : 4,
		"discharge" : 5,
		"down" : 6,
		"hurt" : 7
	};
	GO_Raviene.prototype.anim_frames = [
		[0], //"stand" : 
		[0,1,0,2], //"walk" : 
		[5], //"jump" : 
		[4], //"fall" : 
		[8], //"cast" : 
		[10], //"discharge" : 
		[3], //"down" : 
		[6], //"hurt" : 
	];

	GO_Raviene.prototype.SPELLS = {
		"NONE" : 0,
		"KILL" : 1,
		"HOLD" : 2,
		"SMOKE" : 3,
		"LIGHTNING": 4,
		"TAKE" : 5,
		"BARRIER": 6,
	};

	GO_Raviene.prototype.SPELL_PARAMS = [
		{ // NONE
			spell_cooldown: -1, // time between castSpell logic runs while holding, -1 for infinite (one-shot)
			casting_cooldown: 0 // discharge state length
		},
		{ // KILL
			spell_cooldown: 5, // 1 spark every 5 ticks
			casting_cooldown: 15 // acknowledging time for sparks to fade
		},
		{ // HOLD
			spell_cooldown: -1, // one-shot toggle per target
			casting_cooldown: 0 // no unload
		},
		{ // SMOKE
			spell_cooldown: 5, // 1 spark every 5 ticks
			casting_cooldown: 15 // acknowledging time for smoke to fade
		},
		{ // LIGHTNING
			spell_cooldown: 1, // 1 segment every tick
			casting_cooldown: 5 // no need for this beyond aesthetic
		},
		{ // TAKE
			spell_cooldown: -1, // nop
			casting_cooldown: 0 // nop
		},
		{ // BARRIER
			spell_cooldown: 6, // 1 spark every 6 ticks
			casting_cooldown: 0 // no unload
		},
	];

	GO_Raviene.prototype.addToRoom = function(mapRoom){
		mapRoom.addSprite(this.sprite);

		this.treasuresOverlay.visible = GLOBAL_DIARY.check(4,"NOTREASURE",true);
		mapRoom.addSprite(this.treasuresOverlay);
		if (this.costumeOverlay){
			mapRoom.addSprite(this.costumeOverlay);
		}
		mapRoom.addSprite(this.bubbleOverlay);
	}
	GO_Raviene.prototype.clearFromRoom = function(mapRoom){
		mapRoom.removeSprite(this.sprite);
		mapRoom.removeSprite(this.treasuresOverlay);
		if (this.costumeOverlay){
			mapRoom.removeSprite(this.costumeOverlay);
		}
		mapRoom.removeSprite(this.bubbleOverlay);
	}

	GO_Raviene.prototype.checkEvent = function(sourceGO){
		if (sourceGO && sourceGO.sprite) this.direction = (sourceGO.sprite.x<this.sprite.x) ? -1 : 1;
		
		// just for fun & debugging
		//if (sourceGO.controls){
		//	this.controls = sourceGO.controls;
		//}

		if (this.AI.state==this.AI.STATES.END){
			this.AI.state = 0;
			return this.AI.harmed ? "Raviene_endfight_bad" : "Raviene_endfight_good";
		}
		else{
			// can only afford to call the getter once,
			// since it advances the line we're on
			this._heap.dlg = Dialogue_getCurrentEvent[this.name]();
			if (this._heap.dlg == "GD_Raviene_c5_teamwork_9"){
				(new GO_HeartDrop(GO_HeartDrop.SIZE.BIG)).spawnOnSprite(this.sprite,this.manager);
			}
			else if (this._heap.dlg == "GD_Raviene_c5_teamwork_10"){
				(new GO_HeartDrop(GO_HeartDrop.SIZE.SMALL)).spawnOnSprite(this.sprite,this.manager);
			}
			// ideally we'd clean up _heap.dlg here,
			// but short of some funky postdelete
			// operator, we're stuck.
			return this._heap.dlg;
		}
	}

	GO_Raviene.prototype.castSpell = function(){
		if (this.spell_cooldown!==0) return;

		switch (this.spell){
			case this.SPELLS.KILL:
				//this._heap.spell = {obj: new GO_SinOrbit(this,1,3,2), lifetime: 300};
				//this._heap.spell = {obj: new GO_SinOrbit(this.spell_target,3,11,1), lifetime: 300};
				this._heap.spell = {
					type: this.spell, 
					obj: new GO_SinOrbit(this.spell_target,6,3,0),
					lifetime: 300
				};
				this.spellshots.push(this._heap.spell);
				this.sprite.map_room.addSprite(this._heap.spell.obj.sprite);
				this.manager.add(this._heap.spell.obj);

				this._heap.spell=null;
			break;
			case this.SPELLS.HOLD:
				// single press toggles on/off for given target

				this._heap.spell = null;
				for (this._heap.i=0; this._heap.i<this.spellshots.length; ++this._heap.i){
					if (this.spellshots[this._heap.i].type===this.SPELLS.HOLD && this.spellshots[this._heap.i].obj.target===this.spell_target){
						this._heap.spell = this.spellshots[this._heap.i];
						break;
					}
				}
				if (this._heap.spell){
					this._heap.spell.lifetime = 0; // nonnegative will get it pulled next upkeep
				}
				else{
					this._heap.spell = {
						type: this.spell, 
						obj: new GO_Holdshield(this.spell_target, this.AI.active ? this.AI.shield_strength : -1),
						lifetime: -1 // indefinite length, will clean up manually
					};
					this.spellshots.push(this._heap.spell);
					//this.sprite.map_room.addSprite(this._heap.spell.obj.sprite);
					//this.manager.add(this._heap.spell.obj);
					// if during fight, spawn on self and let track
					// otherwise, this is cutscene and just drop it on target
					this._heap.spell.obj.spawnOnSprite(this.AI.active ? this.sprite : this.spell_target.sprite,this.manager);
					if (this.AI.active) this.AI.last_shield = this._heap.spell.obj;
				}

				this._heap.spell=null;
			break;
			case this.SPELLS.SMOKE:
				this._heap.spell = {
					type: this.spell, 
					obj: new GO_SinOrbit(this.spell_target,6,3,1,1),
					lifetime: 100
				};
				this.spellshots.push(this._heap.spell);
				this.sprite.map_room.addSprite(this._heap.spell.obj.sprite);
				this.manager.add(this._heap.spell.obj);

				this._heap.spell=null;
			break;
			case this.SPELLS.LIGHTNING:
				// straight down onto target, or straight ahead from self if no target
				if (this.spell_target!=this){
					this._heap.spell = {
						type: this.spell, 
						obj: new GO_Lightning(0,8),
						lifetime: -1 // does its own thing as an independent GO; don't auto-clean
					};
					this._heap.spell.obj.sprite.x = this.spell_target.sprite.x + this.spell_target.sprite.center.x - 4;
					this._heap.spell.obj.sprite.y = this.spell_target.sprite.y + this.spell_target.sprite.center.y - 64;
				}
				else{
					this._heap.spell = {
						type: this.spell, 
						obj: new GO_Lightning(this.direction<<3,0),
						lifetime: -1 // does its own thing as an independent GO; don't auto-clean
					};
					this._heap.spell.obj.sprite.x = (this.direction>0) ? 
						this.spell_target.sprite.x + this.spell_target.sprite.box_right + 1 :
						this.spell_target.sprite.x + this.spell_target.sprite.box_left - 8;
					this._heap.spell.obj.sprite.y = this.spell_target.sprite.y + this.spell_target.sprite.center.y - 4;
				}
				this.spellshots.push(this._heap.spell);
				this.sprite.map_room.addSprite(this._heap.spell.obj.sprite);
				this.manager.add(this._heap.spell.obj);

				this._heap.spell=null;
			break;
			case this.SPELLS.BARRIER:
				this._heap.spell = {
					type: this.spell, 
					obj: new GO_InterceptOrbit(this.spell_target,3,5,1,2),
					lifetime: 180
				};

				this.spellshots.push(this._heap.spell);
				this.sprite.map_room.addSprite(this._heap.spell.obj.sprite);
				this.manager.add(this._heap.spell.obj);

				this._heap.spell=null;
			break;
		}

		this.spell_cooldown = this.SPELL_PARAMS[this.spell].spell_cooldown;
	}

	GO_Raviene.prototype.upkeepSpell = function(){
		++this.spell_timer;
		this.spell_timer = this.spell_timer&255; // should be enough for anything

		// per spell object updates
		for (this._heap.i=0; this._heap.i<this.spellshots.length; ++this._heap.i){
			this._heap.spell = this.spellshots[this._heap.i];

			switch(this._heap.spell.type){
				case this.SPELLS.KILL:
					this._heap.spell.obj.sprite.frame = (this.spell_timer&31)>>2;
				break;
				case this.SPELLS.SMOKE:
					this._heap.spell.obj.sprite.frame = ((this.spell_timer+this._heap.i)&31)>>2;
				break;
				case this.SPELLS.HOLD:
					if (this._heap.spell.obj.durability==0) this._heap.spell.lifetime=0;
				break;
				case this.SPELLS.LIGHTNING:
					if (!this._heap.spell.obj.sprite.map_room) this._heap.spell.lifetime=0;
				break;
				case this.SPELLS.BARRIER:
					if (this._heap.spell.obj.dead) this._heap.spell.lifetime=0;
					else this._heap.spell.obj.sprite.frame = ((this.spell_timer+this._heap.i)&31)>>2;
				break;
			}

			if (this._heap.spell.lifetime>0) --this._heap.spell.lifetime;

			if (this._heap.spell.lifetime===0){
				if (this.sprite.map_room) this.sprite.map_room.removeSprite(this._heap.spell.obj.sprite);
				this.manager.remove(this._heap.spell.obj);
				this.spellshots.splice(this._heap.i,1);
				--this._heap.i;
			}
		}
		this._heap.spell=null;
	}

	GO_Raviene.prototype.cleanSpell = function(){
		switch(this.spell){
			case this.SPELLS.KILL:
				for (this._heap.i=0; this._heap.i<this.spellshots.length; ++this._heap.i){
					if (this.spellshots[this._heap.i].type===this.SPELLS.KILL){
						this.spellshots[this._heap.i].lifetime=this.spellshots[this._heap.i].lifetime>>3;
					}
				}
				this._heap.spell=null;

				// apply "kill" to target
				if (this.spell_target.takedamage){
					if(this.spell_target_name=="Meli"){
						// don't even actually hurt Meli, just trigger a flinch
						++GLOBAL_DIARY.life;
					}
					this.spell_target.takedamage({hp:1, chi:100});
				}
			break;
			case this.SPELLS.SMOKE:
				for (this._heap.i=0; this._heap.i<this.spellshots.length; ++this._heap.i){
					if (this.spellshots[this._heap.i].type===this.SPELLS.SMOKE){
						this.spellshots[this._heap.i].lifetime=this.spellshots[this._heap.i].lifetime>>3;
					}
				}
				this._heap.spell=null;
			break;
		}

		this.spell_cooldown = 0;
	}

	GO_Raviene.prototype.flushSpellshots = function(){
		while (this.spellshots.length){
			this._heap.spell = this.spellshots.pop();

			this.sprite.map_room.removeSprite(this._heap.spell.obj.sprite);
			this.manager.remove(this._heap.spell.obj);
		}
		this._heap.spell=null;
	}

	GO_Raviene.prototype.startFight = function(){
		this.leftward_move_land = this.leftward_move = this.upward_move = this.leftward_move_fast;
		this.rightward_move_land = this.rightward_move = this.downward_move = this.rightward_move_fast;
		this.jump_rise_air = this.jump_rise_air_boss;

		this.AI.active = true;
		this.AI.state_timer = 30;
		this.AI.state = this.AI.STATES.REACT;
		this.spell = this.SPELLS.LIGHTNING;
		this.sprite.collision_flags = 
			DEFINE_SPRITE_COLLISION_FLAGS.GROUND | 
			DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_ACTIVE |
			DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_PASSIVE;
		this.interactions = 
			DEFINE_GAMEOBJECT_INTERACTIONS.CONTROL |
			DEFINE_GAMEOBJECT_INTERACTIONS.DAMAGE;// |
			//DEFINE_GAMEOBJECT_INTERACTIONS.INFLICT;
	};

	GO_Raviene.prototype.endFight = function(){
		SDS.bgmName = null;
		SDS.bgm.stop();
		//SDS.bgm = new Chiptune(SDS.bgmName,SDS.bgmchip);
		//SDS.bgm.play(true);
		
		this.AI.active = false;
		this.AI.state = this.AI.STATES.END;
		this.AI.waves = [];
		this.flushSpellshots();

		this.controls.unpress(this.controls,"A");
		this.controls.unpress(this.controls,"B");
		this.controls.unpress(this.controls,"left");
		this.controls.unpress(this.controls,"right");
		this.controls.unpress(this.controls,"up");
		this.controls.unpress(this.controls,"down");

		this.sprite.collision_flags = 
			DEFINE_SPRITE_COLLISION_FLAGS.GROUND |
			DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_ACTIVE |
			DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_PASSIVE;
		this.interactions = 
			DEFINE_GAMEOBJECT_INTERACTIONS.CONTROL |
			DEFINE_GAMEOBJECT_INTERACTIONS.CHECK;
	};

	GO_Raviene.prototype.cleanupFight = function(){
		/*
		this._heap.soul = new GO_Soul("GUY");
		this._heap.soul.spawnOnSprite(this.sprite,this.manager);
		this._heap.soul = null;

		this._heap.poof = new GO_Poof();
		this._heap.poof.spawnOnSprite(this.sprite,this.manager);
		this._heap.poof = null;

		this.sprite.map_room.removeSprite(this.sprite);
		this.manager._remove(this); // outside the update loop, so do it immediately
		*/
	};

	// Enemy-style support routines
	GO_Raviene.prototype.damage = function(source,points){
		this.hurt = true;
		this.AI.state_timer = 16;
		this.AI.state = this.AI.STATES.RECOIL;

		this.direction = (source.sprite.x+source.sprite.center.x) < (this.sprite.x+this.sprite.center.x) ? -1 : 1;

		if (points>0){
			this.AI.hp-=points;
			if (!(source instanceof GO_Holdshield)) this.AI.harmed = true;
			
			if (this.AI.hp<=0){
				if (this.AI.harmed && DATA_SDS_DIALOGUE["Raviene_c5_boss_scream"]){
					SDS.dlgMgr.open("Raviene_c5_boss_scream",function(index,retval){});
				}

				this.endFight();
			}
			else{
				if (DATA_SDS_DIALOGUE["Raviene_hp_"+this.AI.hp]){
					SDS.dlgMgr.open("Raviene_hp_"+this.AI.hp,function(index,retval){});
				}
			}

			return (this.AI.hp<=0);
		}
		else{
			return false;
		}
	};
	GO_Raviene.prototype._inflictVal = {hp:1, chi:0};
	GO_Raviene.prototype.inflict = function(){
		return this._inflictVal;
	};

	GO_Raviene.prototype.aiControl = function(){
		/*
		ORIGINAL NOTES:
		auto-run at ground level
		- or only when player moving?
		turn around when hit wall
		when player jumps, teleport to [altitude] and half way close distance
		when player kicks, [up+jump] to jump or hover-stall for a few frames unless already below player
		when player begins advance while in view, run away
		when player retreats while in view, or comes into view due to turning around, toss shield
		- first shield takes N taps to break out of
		- each successive shield takes N more taps
		- past tap threshold K, every shield tossed costs boss 1HP?
		-> pacifist kill option to redeem combat choice?
		When player stands still for T time, toss lightning/projectile
		*/
		if (!this.AI.meli && !this.AI.meli_find){
			this.AI.meli = this.manager.getNamedObject("Meli");
			this.AI.meli_find = true;
		}

		// get hurt if meli broke free
		if (this.AI.last_shield && this.AI.last_shield.durability<=0){
			this.damage(this.AI.last_shield,1);
			this.AI.last_shield = null;
			++this.AI.shield_strength;

			if (this.AI.hp<=0){
				return;
			}
		}

		switch(this.AI.state){
			case this.AI.STATES.REACT:


				this._heap.meli_still = true;
				this._heap.side = 0;
				this._heap.in_view = true;
				this._heap.threatening = false;
				if (this.AI.meli){
					for (this._heap.c in DEFINE_CONTROLS_KEYCODES){
						this._heap.meli_still &= !this.AI.meli.controls.held[this._heap.c];
					}
					if (this.AI.meli.sprite.x>this.sprite.x) this._heap.side = 1;
					if (this.AI.meli.sprite.x<this.sprite.x) this._heap.side = -1;
					this._heap.in_view = (this.direction==this._heap.side || this._heap.side == 0);
					this._heap.threatening = Math.abs(this.sprite.x - this.AI.meli.sprite.x)<80;
				}
				// if meli's been held or just not done anything for ~1 sec, ZAP
				if (this.AI.last_shield || this._heap.meli_still){
					if (!this._heap.threatening || this.AI.last_shield || this.AI.state_timer<=1){
						this.controls.unpress(this.controls,"left");
						this.controls.unpress(this.controls,"right");
						if (this.AI.meli.sprite.x < this.sprite.x) this.direction = -1; 
						else if (this.AI.meli.sprite.x > this.sprite.x) this.direction = 1;
					}

					--this.AI.state_timer;
					if (this.AI.state_timer<=0){
						this.controls.unpress(this.controls,"left");
						this.controls.unpress(this.controls,"right");
						this.spell = this.SPELLS.LIGHTNING;
						this.spell_target = this.AI.meli;
						this.AI.state_timer = 20;
						this.AI.state = this.AI.STATES.CAST;
					}
				}
				else{
					// reset zap wait to ~1sec if meli active
					this.AI.state_timer = 30;

					// if bonked last eval
					if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
						this.controls.unpress(this.controls,"left");
						this.controls.unpress(this.controls,"right");
						this.direction *= -1;
						
						this.AI.cornered = true;
					}
					else if (this.controls.held.left || this.controls.held.right){
						// clear if definitively free-moving
						this.AI.cornered = false;
						// otherwise, wait until AI logic clears intent
					}

					if (this.AI.meli){
						
						if (this._heap.in_view){
							// if meli retreating, toss shield
							if ((this._heap.side<=0 && this.AI.meli.controls.held.left) || (this._heap.side>=0 && this.AI.meli.controls.held.right)){
								this.AI.cornered = false;
								if (!this.AI.last_shield){
									this.controls.unpress(this.controls,"left");
									this.controls.unpress(this.controls,"right");
									this.spell = this.SPELLS.HOLD;
									this.spell_target = this.AI.meli;
									this.AI.state_timer = 5;
									this.AI.state = this.AI.STATES.CAST;
								}
							}
							// if cornered and pressed, teleport behind
							else if (this.AI.cornered){
								if (Math.abs(this.sprite.x - this.AI.meli.sprite.x)<64){
									// hack trust that the room is flat-floored and wider than 128px
									this._heap.poof = new GO_Poof();
									this._heap.poof.spawnOnSprite(this.sprite,this.manager);
									this._heap.poof = null;
									// also trust that the cornered turnaround response has faced us at meli
									this.sprite.x = this.AI.meli.sprite.x + (this.direction<<6);
									this.direction *= -1;
								}
							}
							// if meli advancing, flee
							else if (this._heap.threatening){
								if (this._heap.side<=0 && this.AI.meli.controls.held.right){
									this.controls.unpress(this.controls,"left");
									this.controls.press(this.controls,"right");
								}
								else if (this._heap.side>=0 && this.AI.meli.controls.held.left){
									this.controls.unpress(this.controls,"right");
									this.controls.press(this.controls,"left");
								}
							}
						}
						else if (!this._heap.threatening){
							// if ran away enough, turn to face
							this.controls.unpress(this.controls,"left");
							this.controls.unpress(this.controls,"right");
							this.direction = (this.AI.meli.sprite.x > this.sprite.x) ? 1 : -1;
						}
						

						// if meli attacking, jump/fly to avoid
						if (this.AI.meli.kicking && this.sprite.y<=this.AI.meli.sprite.y+16){
							//this.AI.state_timer = 30;
							//this.AI.state = this.AI.STATES.JUMP;
							this.AI.wave_watch = 8;
						}
					}
					if (this.AI.wave_watch>0){
						--this.AI.wave_watch;
						if (this.AI.wave_watch<=0){
							this.manager.getAllNamedObjects("DarkWave",this.AI.waves);
						}
					}
					if (this.AI.waves.length){
						while (this.AI.waves.length && this.AI.waves[0].power<=0){
							this.AI.waves.shift();
						}
						for (this._heap.i=0; this._heap.i<this.AI.waves.length; ++this._heap.i){
							this._heap.wave = this.AI.waves[this._heap.i];
							if (Math.abs(this._heap.wave.sprite.x - this.sprite.x)<64 && 
								(this._heap.wave.direction<0 ^ this._heap.wave.sprite.x<this.sprite.x) &&
								this.sprite.y<=this._heap.wave.sprite.y+16){
								this.AI.state_timer = 15;
								this.AI.state = this.AI.STATES.JUMP;
							}
						}
						this._heap.wave = null;
					}
				}
			break;
			case this.AI.STATES.JUMP:
				this.controls.press(this.controls,"up");
				this.controls.press(this.controls,"A");
				--this.AI.state_timer;
				if (this.AI.state_timer<=0){
					this.controls.unpress(this.controls,"up");
					this.controls.unpress(this.controls,"A");
					this.AI.state_timer = 30;
					this.AI.state = this.AI.STATES.REACT;
				}
			break;
			case this.AI.STATES.CAST:
				this.controls.press(this.controls,"B");
				--this.AI.state_timer;
				if (this.AI.state_timer<=0){
					this.controls.unpress(this.controls,"B");
					this.AI.state_timer = 30;
					this.AI.state = this.AI.STATES.REACT;
				}
			break;
			case this.AI.STATES.RECOIL:
				this.hurt = true;
				for (this._heap.c in DEFINE_CONTROLS_KEYCODES){
					this.controls.unpress(this.controls,this._heap.c);
				}
				//this.sprite.move(this.direction*-1, 0,  this._heap.spriteMoveRes);
				--this.AI.state_timer;
				this.sprite.visible = !(this.AI.state_timer & 1);
				if (this.AI.state_timer<=0){
					this.hurt = false;
					this.sprite.visible = true;
					this.AI.state_timer = 30;
					this.AI.state = this.AI.STATES.REACT;
				}
			break;
		}
	}

	GO_Raviene.prototype.supportControl = function(){
		// contemplated a system of static graph nodes per room
		// and/or a chain of waypoints based on Meli's position
		// but if we can get by with some more basic state-condition
		// logic, that's more in the spirit of the game
		// (and also I'm lazy)
		// besides, if we make Ravi's support effect range-dependent,
		// players will have an incentive to negotiate her past any terrain.
		// ...
		// ok, ok, if YOU can make a less hacky follow script that's
		// more fluid and interesting than "be where Meli was 3 frames ago"
		// I encourage YOU to fit the overhead into ~6 js numeric
		// instance variables.  I'm kinda proud of this, tbh.

		if (!this.SUP.meli && !this.SUP.meli_find){
			this.SUP.meli = this.manager.getNamedObject("Meli");
			this.SUP.meli_find = true;

			this.spell = this.SPELLS.BARRIER;
			//this.spell_target = this.SUP.meli; // must be set every cast
		}

		if (!this.SUP.meli) return;

		this._heap.cx = this.sprite.x + this.sprite.center.x;
		this._heap.cy = this.sprite.y + this.sprite.center.y;

		this._heap.mcx = this.SUP.meli.sprite.x + this.SUP.meli.sprite.center.x;
		this._heap.mcy = this.SUP.meli.sprite.y + this.SUP.meli.sprite.center.y;

		this._heap.dx = this._heap.mcx - this._heap.cx;
		this._heap.dy = this._heap.mcy - this._heap.cy;

		// X: easy all cases
		if (this._heap.dx>=8){
			this.controls.unpress(this.controls,"left");
			this.controls.press(this.controls,"right");
		}
		else if (this._heap.dx<=-8){
			this.controls.press(this.controls,"left");
			this.controls.unpress(this.controls,"right");
		}
		else{
			this.controls.unpress(this.controls,"left");
			this.controls.unpress(this.controls,"right");
		}

		// Y: easy as if flying...
		if (this._heap.dy>=8){
			//this.controls.press(this.controls,"down"); // jic... except no, Ravi has a crouch
			this.controls.unpress(this.controls,"up");

			// really, falling will be faster than flying down
			if (this.controls.held["A"]){
				this.controls.unpress(this.controls,"A");
			}
		}
		else if (this._heap.dy<=-8){
			this.controls.unpress(this.controls,"down");
			this.controls.press(this.controls,"up");
			// but need to be sure we're flying if meli's above.

			if (!this.controls.held["A"]){
				// press/hold A to jump (faster) OR maintain flight
				this.controls.press(this.controls,"A");
			}
			else if (this.anim_state == this.ANIM_STATES.fall){
				// if A held but we're falling, we're in a jump state
				// not a fly state.
				// release for a frame to engage fly mode next frame
				this.controls.unpress(this.controls,"A");
			}
		}
		else{
			//this.controls.unpress(this.controls,"down");
			this.controls.unpress(this.controls,"up");
		}

		// Ravi can get stuck on the perniciously rough underdark floor
		// not to mention in holes.
		
		if ((this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_H)){
			// Most common case is stuck on the ground, so if ejected-H,
			// try jumping until not.
			this.controls.press(this.controls,"A"); // force maintain jump or fly
			if (this.anim_state == this.ANIM_STATES.fall){ // only release to engage fly if we detect fall
				this.controls.unpress(this.controls,"A");
			}
			// if a jump didn't clear, and we entered fly state,
			// get creative with some dynamic motion
			if (this.flying){//} && (this.SUP.v_force_dir || (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_V))){
				// start by going opposite what got us stuck
				if (!this.SUP.v_force_dir){
					this.SUP.v_force_dir = this.controls.held["up"] ? 1 : -1;
				}
				// now oscillate, increasing amplitude by 50% each swing
				++this.SUP.v_force_count;
				if (this.SUP.v_force_count>=this.SUP.v_force_limit){
					this.SUP.v_force_limit += (this.SUP.v_force_limit>>1);
					this.SUP.v_force_count = 0;
					this.SUP.v_force_dir *= -1;
				}

				if (this.SUP.v_force_dir>0){
					this.controls.press(this.controls,"down"); // drop fast and/or be sad
					this.controls.unpress(this.controls,"up");
				}
				else if (this.SUP.v_force_dir<0){
					this.controls.unpress(this.controls,"down");
					this.controls.press(this.controls,"up");
				}
			}
			else{
				// if NOT flying, be sure we clear down so we can jump again
				this.controls.unpress(this.controls,"down");
			}
		}
		else if (!(this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_H)){
			// if unstuck,
			if (this.SUP.v_force_dir){
				// unstick down in case that's what we were doing when we resolved
				this.controls.unpress(this.controls,"down");
				this.controls.unpress(this.controls,"up"); // sure why not
			}
			// reset the oscillation
			this.SUP.v_force_dir=0;
			this.SUP.v_force_count=0;
			this.SUP.v_force_limit=2;
		}

		// can do a similar trick horizontally,
		// but it will need different triggering logic to play nice.
		// rather than trying to long-term track whether we're
		// in a horizontal-normal vs horizontal-recovery state
		// AND INDEPENDENTLY a vertical-mormal vs vertical-recovery
		// state (under the assumption that there will be consistent
		// horizontal effort), just say that Ravi "panics" if too far
		// from Meli
		if (Math.abs(this._heap.dy)>=32 && (this.SUP.h_force_dir || this.grounded || (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_V))){
			// only go left if proactive right got us into this mess
			if (!this.SUP.h_force_dir){
				this.SUP.h_force_dir = this.controls.held["right"] ? -1 : 1;
			}
			// now oscillate, increasing amplitude by 50% each swing
			++this.SUP.h_force_count;
			if (this.SUP.h_force_count>=this.SUP.h_force_limit){
				this.SUP.h_force_limit += (this.SUP.h_force_limit>>1);
				this.SUP.h_force_count = 0;
				this.SUP.h_force_dir *= -1;
			}

			if (this.SUP.h_force_dir>0){
				this.controls.press(this.controls,"right");
				this.controls.unpress(this.controls,"left");
			}
			else if (this.SUP.h_force_dir<0){
				this.controls.unpress(this.controls,"right");
				this.controls.press(this.controls,"left");
			}
		}
		else if (Math.abs(this._heap.dy)<32){ // if un-panic'd
			if(this.SUP.h_force_dir){
				// unstick our overrides
				this.controls.unpress(this.controls,"right");
				this.controls.unpress(this.controls,"left");
			}

			// reset the oscillation
			this.SUP.h_force_dir=0;
			this.SUP.h_force_count=0;
			this.SUP.h_force_limit=8;
		}

		this._heap.r2 = this._heap.dx*this._heap.dx+this._heap.dy*this._heap.dy;
		// if ravi is quite far away, give her an extra update cycle
		if (this._heap.r2>4096){ // (4 16px blocks ^2)
			//++this.SUP.again; // +25% speed
			this.leftward_move_land = this.leftward_move = this.upward_move = this.leftward_move_fast;
			this.rightward_move_land = this.rightward_move = this.downward_move = this.rightward_move_fast;
		}
		else{
			this.leftward_move_land = this.leftward_move = this.upward_move = this.leftward_move_normal;
			this.rightward_move_land = this.rightward_move = this.downward_move = this.rightward_move_normal;
		}
		//if (this._heap.r2>16384){ // (8 16px blocks ^2)
		//	++this.SUP.again; // +50% speed
		//}

		if (this._heap.r2<144){ // (12px ^2)
			// cast barrier
			this.spell_target = this.SUP.meli;
			this.controls.press(this.controls,"B");
		}
		else{
			// stop barrier
			this.controls.unpress(this.controls,"B");
		}

	}

	GO_Raviene.prototype.Update = function(){
		if (this.AI.active) this.aiControl();
		else if (this.SUP.active) this.supportControl();

		this._heap.oldAnimState = this.anim_state;

		// check controls, set flying
		// to fly, "jump" in midair & hold
		if ((this.flying && this.controls.held.A) || (!this.grounded && !this.in_water_cache && this.controls.pressed.A)){
			this.flying = true;
			this.fall_index = 0;
		}
		else{
			this.flying=false;
		}

		
		if (this.controls.held.B){// yeah, casting & discharge will hold position
			this.casting_cooldown = this.SPELL_PARAMS[this.spell].casting_cooldown;
			this.fall_index = 0;
			this.anim_state = this.ANIM_STATES.cast;
		}
		else if (this.casting_cooldown>0){
			--this.casting_cooldown;
			this.anim_state = this.ANIM_STATES.discharge;
		}
		else if (!this.flying && this.grounded && this.controls.held.down){
			this.anim_state = this.ANIM_STATES.down;
		}
		else if (this.flying){
			this.SimpleMove();
			this.anim_state=this.ANIM_STATES.stand; // not because we're standing but because it's the base for the flight frame
			if (this.controls.held.left) this.direction=-1;
			if (this.controls.held.right) this.direction=1;
		}
		else{
			this.fall_air = this.controls.held.A ? this.fall_air_slow : this.fall_air_fast;
			this.NPCWalk();
		}
		
		// actual spells logic
		if (this.spell_cooldown>0) --this.spell_cooldown;
		if (this.controls.held.B){
			if (this.spell_target_name && !this.spell_target){
				this.spell_target = this.manager.getNamedObject(this.spell_target_name);
			}
			if (!this.spell_target){
				this.spell_target = this; // don't ask me; I'm not her shrink
			}
			this.castSpell();
		}
		else if (this.controls.released.B){
			this.cleanSpell();
			this.spell_target = null;
		}
		if (this.spellshots.length){
			this.upkeepSpell();
		}

		if (this._heap.oldAnimState != this.anim_state){
			this.frame_timer=0;
		}
		++this.frame_timer;
		if (Math.abs(this.horizontal_move_index)>2 || Math.abs(this.vertical_move_index)>2){
			++this.frame_timer; // boost framerate if gofast
		}
		this._heap.aniFrame = (this.frame_timer>>3) % this.anim_frames[this.anim_state].length;

		this.sprite.frame = this.anim_frames[this.anim_state][this._heap.aniFrame];

		if (this.flying){
			this.sprite.frame += 4;
			++this.flight_timer;
			if (Math.abs(this.horizontal_move_index)>2 || Math.abs(this.vertical_move_index)>2){
				++this.flight_timer; // boost framerate if gofast
			}
			this.flight_timer = this.flight_timer & 15;
			if (this.flight_timer & 8){
				++this.sprite.frame;
			}

			if (this.hurt){
				this.sprite.frame+=2;
			}
		}
		else if (this.hurt){
			this.sprite.frame = 3;
		}

		this.sprite.frame = this.sprite.frame & 15;
		if (this.direction<0) this.sprite.frame+=16;

		this.treasuresOverlay.frame = this.sprite.frame;
		this.treasuresOverlay.x = this.sprite.x;
		this.treasuresOverlay.y = this.sprite.y;

		if (this.costumeOverlay){
			this.costumeOverlay.frame = this.sprite.frame;
			this.costumeOverlay.x = this.sprite.x;
			this.costumeOverlay.y = this.sprite.y;
		}

		this.bubbleOverlay.visible = this.in_water_cache ? !this.bubbleOverlay.visible : false;
		this.bubbleOverlay.x = this.sprite.x;
		this.bubbleOverlay.y = this.sprite.y;

		//if (this.SUP.again > 0){
		//	this.SUP.again-=4; // if every frame wants 1 "again", apply it every 4
		//	this.Update();
		//}
	}

