"use strict";
/* not technically a zombie */
var GO_Meli = function(){
	this.sprite = new Sprite(GO_Meli.spriteSheet,"Meli");
	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,1,13,15);
	this.sprite.game_object = this; // no longer a generic GO

	// a secondary sprite in the player object, determining whether we can swim upwards
	this.tail = new Sprite(null,"Meli_tail");
	this.tail.collision_flags = 
		DEFINE_SPRITE_COLLISION_FLAGS.AIR; // originally permanently air; for suspended droplets it now clears unless moving upwards
	this.tail.setBoundingBox(2,11,13,15);
	this.tail.game_object = this;

	// a secondary sprite in the player obejct, used for checking proximity to carryables & investigation
	this.sparkle = new Sprite(GO_Meli.sparkleSheet,"Meli_sparkle");
	this.sparkle.collision_flags = DEFINE_SPRITE_COLLISION_FLAGS.SPRITES_ACTIVE;
	this.sparkle.setBoundingBox(0,0,7,7);
	this.sparkle.game_object = this;

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

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

	// jewelery
	this.braceletsOverlay = new Sprite(GO_Meli.braceletsSheet,"Meli_bracelets");
	this.braceletsOverlay.game_object = this;
	this.necklaceOverlay = new Sprite(GO_Meli.necklaceSheet,"Meli_necklace");
	this.necklaceOverlay.game_object = this;
	this.beltOverlay = new Sprite(GO_Meli.beltSheet,"Meli_belt");
	this.beltOverlay.game_object = this;
	this.circletOverlay = new Sprite(GO_Meli.circletSheet,"Meli_circlet");
	this.circletOverlay.game_object = this;
	this.blessingOverlay = new Sprite(GO_Meli.blessingSheet,"Meli_blessing");
	this.blessingOverlay.game_object = this;
	this.treasures = [];
	this.treasures.push(this.braceletsOverlay);
	this.treasures.push(this.necklaceOverlay);
	this.treasures.push(this.beltOverlay);
	this.treasures.push(this.circletOverlay);
	this.treasures.push(this.blessingOverlay);

	// since it behaves like the treasures for positioning
	// and adding/removal
	if (this.costumeOverlay) this.treasures.push(this.costumeOverlay);

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

	// and now customize it
	this.sparkleFrame = 0;

	this.horizontal_move_index=0;// into leftward_/rightward_move_*
	this.vertical_move_index = 0; // into upward_/downward_move_*
	this.jump_index = 0; // into jump_rise_*
	this.jumpArr = this.jump_rise_normal; // which of the defined arrays are we currently indexing against
	this.fall_index=0; // into fall

	this.anim_state = this.ANIM_STATES.bob;

	this.direction = 1; // +/-1 for facing
	this.grounded = false;
	this.in_water_cache = undefined; // declared as undefined.  Because we don't know yet.
	this.jump_grounded = false;

	this.frame_timer=0;

	this.kicking=false;
	this.kick_strength=-1;
	this.kick_direction = this.direction; // because if I don't, I'll catch hell for the lag between the input and actual wave release
	this.kick_timer=0;

	this.dashing=false;
	this.dash_directions = {
		left: false,
		up: false,
		right: false,
		down: false
	};

	this.punching=false;
	this.punch_timer=0; // countdown to hold punch ani frame
	this.punch_accumulator=-1; // frames of charge held

	this.lifering_accumulator=0; // frames still for life recharge

	this.hurt_timer=0; // flinch reaction
	this.iframes=0; // controllable

	this.holding = false;
	this.held_obj = null;

	this.pushing=0;//int used like a boolean- set to ~2 when pushing, rshifts every frame

	this.in_door=null; // the GO_Passage or GO_Door object whose sprite we hit in our move check; used by Main to change rooms
	this.checking=null; // as long as the precedent is set, use the same mechanism as doors to elevate investigation to where we have an event manager
	this.check_timer=0;
	this.triggering=null; // ditto for triggered events
	this.collecting=null; // it's almost getting silly at this point, but grouping these would let one clobber another

	this.overrideFrame = null;

	this.recent_damage_sources = [];
	this.dead = false; // to resolve ANY possible race conditions between life loss and life gain, this is set on takedamage and cleared on Deathwarp

	this._heap.oldSources=[];
	this._heap.gripPenetration={x:0, y:0};
	this._heap.interactTargets=[];
	this._heap.portTargets=[];
	this._heap.tailMoveRes = new SpriteMoveResult();
};
GO_Meli.spriteImg = SDS_Load_Image("art/sheets/sprite/protagonist.png");
GO_Meli.spriteSheet = new TileSheet(4, 4, 3, 3, GO_Meli.spriteImg);
GO_Meli.sparkleImg = SDS_Load_Image("art/sheets/sprite/sparkle.png");
GO_Meli.sparkleSheet = new TileSheet(3, 3, 3, 0, GO_Meli.sparkleImg);

GO_Meli.braceletsImg = SDS_Load_Image("art/sheets/sprite/protagonist_bracelet.png");
GO_Meli.braceletsSheet = new TileSheet(4, 4, 3, 3, GO_Meli.braceletsImg);
GO_Meli.necklaceImg = SDS_Load_Image("art/sheets/sprite/protagonist_necklace.png");
GO_Meli.necklaceSheet = new TileSheet(4, 4, 3, 3, GO_Meli.necklaceImg);
GO_Meli.beltImg = SDS_Load_Image("art/sheets/sprite/protagonist_belt.png");
GO_Meli.beltSheet = new TileSheet(4, 4, 3, 3, GO_Meli.beltImg);
GO_Meli.circletImg = SDS_Load_Image("art/sheets/sprite/protagonist_circlet.png");
GO_Meli.circletSheet = new TileSheet(4, 4, 3, 3, GO_Meli.circletImg);
GO_Meli.blessingImg = SDS_Load_Image("art/sheets/sprite/protagonist_blessing.png");
GO_Meli.blessingSheet = new TileSheet(4, 4, 3, 3, GO_Meli.blessingImg);
GO_Meli.costumeImg = null;
GO_Meli.costumeSheet = null;
GO_Meli.costumes = {};
GO_Meli.costumes[0] = {};
GO_Meli.costumes[1] = {};
GO_Meli.costumes[2] = {};
GO_Meli.costumes[3] = {};
GO_Meli.costumes[4] = {};
GO_Meli.costumes[5] = {};
GO_Meli.costumes[6] = {};
GO_Meli.costumes[7] = {};
GO_Meli.costumes[8] = {};
GO_Meli.costumes[9] = {};
GO_Meli.costumes[10] = {};
GO_Meli.costumes[11] = {};
// 2017 Women's Rights March
GO_Meli.costumes[0][21] = "art/sheets/sprite/protagonist_pcap.png";

GO_Meli.prototype = new GameObject(
	new Sprite(
		GO_Meli.spriteSheet,
		"Meli"
	),
	"Meli"
	);
//GO_Meli.prototype.sprite.game_object = null; // disconnect until I have time for a better overhaul

	GO_Meli.prototype.interactions = 
		DEFINE_GAMEOBJECT_INTERACTIONS.CONTROL |
		DEFINE_GAMEOBJECT_INTERACTIONS.FORCE_CRUSH;

	GO_Meli.prototype.leftward_move_air = [0,-1,-2];
	GO_Meli.prototype.leftward_move_water = [0,-1,-2];
	GO_Meli.prototype.leftward_move_flounder = [0];
	GO_Meli.prototype.leftward_move_fly = [0,-1,-2,-3];
	GO_Meli.prototype.rightward_move_air = [0,1,2];
	GO_Meli.prototype.rightward_move_water = [0,1,2];
	GO_Meli.prototype.rightward_move_flounder = [0];
	GO_Meli.prototype.rightward_move_fly = [0,1,2,3];

	GO_Meli.prototype.upward_move_water = [0,-1,-2];
	GO_Meli.prototype.downward_move_water = [0,1,2];

	GO_Meli.prototype.jump_rise_normal = [ //40px = 2.5 blocks
		-4,-4,-4,-4,-4,-4,
		-3,-3,-3, 
		-2,-2,-2,
		-1,
		0, 0];
	GO_Meli.prototype.jump_rise_flounder = [ //8px = .5 blocks
		-2, -2, -2, 
		-1, -1, 
		0];
	GO_Meli.prototype.jump_rise_high = [ //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_Meli.prototype.fall = [ 
		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_Meli.prototype.dash = 2; // px/frame in addition to base movement during a dash

	GO_Meli.prototype.ANIM_STATES = {
		"bob" : 0,
		"swim" : 1,
		"stand" : 2,
		"run" : 3,
		"jump" : 4,
		"fall" : 5,
		"dive" : 6, // art detail on fall state; not(?) a functionally-checked state
		"flounder" : 7,
		"kick" : 8,
		"bob_check" : 9,
		"stand_check" : 10,
		"hurt" : 11,
		// bolt-on special mechanics; using these for frame references, 
		// but not actually as standalone animation states.
		// too many possible timings for transitioning into and out of them :P
		"dash" : 12,
		"dash_up" : 13,
		"dash_down" : 14,
		"punch" : 15
	};

	GO_Meli.prototype.anim_frames = [
		[0,0,1,1], //"bob" : 
		[0,1,0,2], //"swim" : 
		[8], //"stand" : 
		[8,9,8,10], //"run" : 
		[16], //"jump" : 
		[17], //"fall" : 
		[18], //"dive" : 
		[19], //"flounder" : 
		[0,24,25,24,0], //"kick" : 
		[3], // "bob_check"
		[11], // "stand_check"
		[27], // "hurt"
		[26], // "dash"
		[16], // "dash_up"
		[18], // "dash_down"
		[26], // "punch"
	];



	GO_Meli.prototype.takedamage = function(damage){
		if (this.iframes>0) return;
		if (damage.hp>0){
			this.hurt_timer=10;
			this.iframes = 20;
			if (this.holding){
				this.held_obj.popContext(); // Update
				this.held_obj.drop();
				this.held_obj = null;
				this.holding=false;
			}
			this.kicking=false;
			this.pushing=false;

			this.playSound("FX_ouch");

			GLOBAL_DIARY.life-=damage.hp;

			if (GLOBAL_DIARY.life<=0){
				GLOBAL_DIARY.life=0;
				// moving this to the update loop so it can occur at/near the end of the hurt timer
				//Deathwarp();

				// instead, detatch all interactions briefly
				this.dead = true;
				this.interactions &= ~DEFINE_GAMEOBJECT_INTERACTIONS.FORCE_CRUSH;
				this.sprite.collision_flags = 0;
			}
		}
		this.punch_charging = false;
		this.punching = false;
		this.punch_timer = 0;
		this.dashing = false;

		if (GLOBAL_DIARY.blessingActive()){
			// Malfose's "blessing": chi inverted into darkpower
			GLOBAL_DIARY.chi += damage.chi;
			GLOBAL_DIARY.chi = Math.min(GLOBAL_DIARY.chi, GLOBAL_DIARY.max_chi);
		}
		else{
			GLOBAL_DIARY.chi-=damage.chi;
			// huh.  Just realized that with the comparatively newer negative-chi support in Diary,
			// there's a possibility for damage-boosting chi back to 0.
			// Developer stance: if someone figures out how to abuse it, more power to them.
			if (GLOBAL_DIARY.chi<=0){
				GLOBAL_DIARY.chi=0;
			}
		}
	};

	GO_Meli.prototype.Deathwarp = function(){
		/*alert("Death eyes you strangely: \"You look familiar... did you have a sister?\"");
		alert("\"No... no, it's more than that... Your soul is... ...\"");
		alert("\"BEGONE!  You do not belong here!\"");
		GLOBAL_DIARY.life=GLOBAL_DIARY.max_life;*/

		GLOBAL_DIARY.life=0;
		this.dead = false;
		this._heap.deathwarp = new GO_Deathwarp(this);
		this.sprite.map_room.addSprite(this._heap.deathwarp.sprite);
		this.manager.add(this._heap.deathwarp);
		this.in_door = this._heap.deathwarp;

		//if (!GLOBAL_DIARY.check(0,"MET_DEATH")){
		//	this.triggering = this._heap.deathwarp; // with the deathwarp set to interaction TRIGGER, this shouldn't be needed
		//}

		this._heap.deathwarp=null; // until object pooling is implemented...

		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.FORCE_CRUSH;
	}

	GO_Meli.prototype.crush = function(){
		SDS.dlgMgr.open("Crushed",function(index,retval){
			if (retval!=null){
				
			}
		},true);

		this.iframes = 0; // ha ha ha ha nope
		this.takedamage({hp:100,chi:0});
		//alert("OWWWWIEEEEEEE!!! (if this had been the real game, you would be dead...)");
	};

	GO_Meli.prototype.canFly = function(){
		return this.controls.held.up && GLOBAL_DIARY.circletActive();
	};
	GO_Meli.prototype.doFly = function(){
		GLOBAL_DIARY.circletFly();
		this.playSound("FX_hup");

		this.jump_index = 0;
		this.anim_state = this.ANIM_STATES.jump;
		this.jump_grounded = this._heap.wasGrounded;

		//this._heap.new_jumpindex = this.jump_index - this.jumpArr.length; // we are this far from the apex
		this.jumpArr = this.jump_rise_high;
		//this.jump_index = Math.max(0,this.jumpArr.length + this._heap.new_jumpindex); // we remain this far from the new apex
	}
	GO_Meli.prototype.canHighJump = function(){
		return this.canJump() && GLOBAL_DIARY.hasBelt;
	};
	GO_Meli.prototype.doHighJump = function(){
		GLOBAL_DIARY.collarJump();
		this.playSound("FX_hup");
		
		this.jump_index = 0;
		this.anim_state = this.ANIM_STATES.jump;
		this.jump_grounded = this._heap.wasGrounded;

		//this._heap.new_jumpindex = this.jump_index - this.jumpArr.length; // we are this far from the apex
		this.jumpArr = this.jump_rise_high;
		//this.jump_index = Math.max(0,this.jumpArr.length + this._heap.new_jumpindex); // we remain this far from the new apex
	};
	GO_Meli.prototype.canJump = function(){
		return this.canFlounder() && GLOBAL_DIARY.collarActive();
	};
	GO_Meli.prototype.doJump = function(){
		GLOBAL_DIARY.collarJump();
		this.playSound("FX_hup");
		
		this.jump_index = 0;
		this.anim_state = this.ANIM_STATES.jump;
		this.jump_grounded = this._heap.wasGrounded;

		//this._heap.new_jumpindex = this.jump_index - this.jumpArr.length; // we are this far from the apex
		this.jumpArr = this.jump_rise_normal;
		//this.jump_index = Math.max(0,this.jumpArr.length + this._heap.new_jumpindex); // we remain this far from the new apex
	};
	GO_Meli.prototype.canFlounder = function(){
		return this._heap.wasGrounded;
	};
	GO_Meli.prototype.doFlounder = function(){

		this.jump_index = 0;
		this.anim_state = this.ANIM_STATES.jump;
		this.jump_grounded = this._heap.wasGrounded;

		//this._heap.new_jumpindex = this.jump_index - this.jumpArr.length; // we are this far from the apex
		this.jumpArr = this.jump_rise_flounder;
		//this.jump_index = Math.max(0,this.jumpArr.length + this._heap.new_jumpindex); // we remain this far from the new apex
	};

	GO_Meli.prototype.canWaterJump = function(){
		return this.sprite.isTouchingTerrain(DEFINE_MAP_TERRAIN_TYPES.AIR);
	};
	GO_Meli.prototype.doWaterJump = function(){
		
		this.jump_index = 0;
		this.anim_state = this.ANIM_STATES.jump;
		this.jump_grounded = true; // effectively

		//this._heap.new_jumpindex = this.jump_index - this.jumpArr.length; // we are this far from the apex
		this.jumpArr = GLOBAL_DIARY.hasBelt ? this.jump_rise_high : this.jump_rise_normal;
		//this.jump_index = Math.max(0,this.jumpArr.length + this._heap.new_jumpindex); // we remain this far from the new apex
	};

	GO_Meli.prototype.canKick = function(water){
		return !this.kicking && (water || GLOBAL_DIARY.collarActive());
	}
	GO_Meli.prototype.doKick = function(water){
		this.kicking=true;
		if (water){
			this.kick_strength = (GLOBAL_DIARY.hasBelt) ? GO_KickWave.STRENGTH.SUPER : GO_KickWave.STRENGTH.STRONG;
		}
		else{
			this.kick_strength = (GLOBAL_DIARY.hasBelt) ? GO_KickWave.STRENGTH.STRONG : GO_KickWave.STRENGTH.WEAK;
		}
		this.kick_direction = this.direction;
		this.kick_timer=-1;
	}

	GO_Meli.prototype.canGrab = function(){
		return !this.holding && GLOBAL_DIARY.braceletsActive();
	}
	GO_Meli.prototype.tryGrab = function(obj){
		if (obj.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.GRAB){
			this.held_obj = obj;
			this.held_obj.pushContext(["Update"]);
			this.held_obj.Update=noop;
			this.held_obj.grab();
			this.holding=true;
		}
	}

	GO_Meli.prototype.canDash = function(water){
		// need the collar.
		// if in water, we can dash at any time.
		// if on land, we can only start a dash when grounded, until we get the belt
		return GLOBAL_DIARY.collarActive() && !this.holding && (water || this._heap.wasGrounded || GLOBAL_DIARY.beltActive());
	};
	GO_Meli.prototype.doDash = function(){
		this.playSound("FX_whoosh");
		this.dashing=true;
		this.dash_directions.up = this.controls.held.up;
		this.dash_directions.right = this.controls.held.right;
		this.dash_directions.down = this.controls.held.down;
		this.dash_directions.left = this.controls.held.left;
		// 2020.11.6 - as Meli cannot dash directly upwards unless in water,
		// her gender identity remains plausibly inconclusive in the upper
		// half of the game.
		// ( o7 Maddie )
	};
	GO_Meli.prototype.checkDash = function(){
		this.dashing &= this.controls.held.A;
		this.dashing &= this.hurt_timer===0;
		if (!GLOBAL_DIARY.blessingActive()){
			this.dashing &= !this.holding; // I don't know; the physics might even allow this. Logic won't unless you have blessing.
			// exact-match what you started
			this.dashing &= (this.controls.held.up===this.dash_directions.up);
			this.dashing &= (this.controls.held.right===this.dash_directions.right);
			//this.dashing &= (this.controls.held.down===this.dash_directions.down);
			this.dashing &= (this.controls.held.left===this.dash_directions.left);

			// special caveat: can turn a down dash into a straight dash if grounded
			if (this.grounded){
				if (this.dash_directions.down && !this.controls.held.down) this.dash_directions.down = false;
			}
			else{
				this.dashing &= (this.controls.held.down===this.dash_directions.down);
			}
		}
		else{
			// keep holding what you started
			//this.dashing &= !(this.dash_directions.down && !this.controls.held.down);
			this.dashing &= !(this.dash_directions.up && !this.controls.held.up);
			this.dashing &= !(this.dash_directions.left && !this.controls.held.left);
			this.dashing &= !(this.dash_directions.right && !this.controls.held.right);
			// special caveat: can turn a down dash into a straight dash if grounded
			if (this.grounded){
				if (this.dash_directions.down && !this.controls.held.down) this.dash_directions.down = false;
			}
			else{
				this.dashing &= !(this.dash_directions.down && !this.controls.held.down);
			}
			// don't reverse
			this.dashing &= !(this.dash_directions.up && this.controls.held.down);
			this.dashing &= !(this.dash_directions.down && this.controls.held.up);
			this.dashing &= !(this.dash_directions.left && this.controls.held.right);
			this.dashing &= !(this.dash_directions.right && this.controls.held.left);
		}
		// But really, since we're on the subject and it probably WON'T be made
		// explicit in game dialogue, I fully accept Meli being whatever you
		// read her as. I would not be surprised if my own identity rubbed off
		// on her; I like girls, am ambivalent on sex/promiscuity, and don't
		// really grasp the desirability of the male body despite, for the 
		// moment at least, having one. Some of the early plot is definitely
		// a critique of toxic masculinity, and [spoilers] you/Meli are actively
		// given the option to be brusque or generous to both Raviene and Gabe,
		// although the in-game consequence only penalizes hostility, it does
		// not per se reward affection.  As to [deeper spoilers] Meli's mysterious
		// paramour from the cabin photo/backstory, in my head, they're merfolk.
		// Beyond that, I've been back and forth on their gender enough 
		// times that ultimately I'm also leaving them up to interpretation.
		// (With a gun to my head, I'd probably get more warm fuzzies if they
		// were a girl, but why are you putting a gun to my head? By the time
		// the game is well and truly out, you're free to edit/expand any part
		// of it you don't like!)
		// I do worry that by not making these things explicit but rather explicitly
		// encouraging interpretation, I'll come across as queer-baiting.
		// If you're concerned that I'm too concerned with the popularity of
		// my game among the cishet populace... I think you have greater faith in
		// the popularity of my game than I do, for one thing. It's honestly hard
		// for me to imagine alienating a more conservative wide-audience when 
		// it's hard for me to imagine a player base extending outside word-
		// of-mouth trickle from the universally LGBTQIA+ social crowds I 
		// frequent enough to even plug the game in to begin with. But... this
		// isn't a bad conversation to have. The more I think on it, here and now,
		// the more I'm inclined to say, between us, why not? Meli _is_ young
		// and open and optimistic and a canonically blank slate figuring herself
		// out.  Pending actually connecting deeply with someone, she's bicurious.
		// Likewise, while the body she was given is, as far as she can ascertain,
		// female, she doesn't see it as per se right for her, or per se wrong for
		// her; she enjoys cute things and dislikes having to fight, but is proud 
		// of her ability to fill more of a masculine-action-hero role when given
		// the power to do so. If and when faced with outside pressure ascribing
		// traits she's proud or ashamed of to various genders, she may contextualize
		// herself as more masculine or more feminine. Raviene, for instance [you
		// HAVE finished the game by now, right?] is I will say Very Cis but, 
		// as a plot conceit, neurotically caught up in "masculine" disregard for
		// others as a "positive" power-trait, and Meli definitely feels a bit
		// dirtied and lessened when forced to cede some of her own compassion.
		// But mostly, for now, she doesn't think much about gender and is female
		// out of the expediency of reading as female.
		// Which is to say that, back to the original premise, you as the player
		// can see her faced with prompts and backstory and allow her to explore
		// and envision herself as whatever makes sense to you, and you aren't wrong,
		// because she simply doesn't know anything more correct herself.
		// 'Ship her with Gabe. 'Ship her with Ravi (just... please work respectfully
		// around their respective issues). Heck, 'ship her with Charis and Charis'
		// ghost-fiancé in a polycule if they can all be happy with it. Or call
		// her ace because no in-game dialogue really allows her romantic depth with 
		// anyone. Her consiousness is assembled from various individuals of
		// various genders, and is in a very real way subject to change over time.
		// (You are also welcome to thereby interpret her as plural; I don't feel
		// my own experience entitles me to make that call one way or the other.)
		// And if her headspace is subject to change, and she lives in a world
		// with the *mancy to re/make a person from parts, it stands to reason
		// that she might think to change her body in the future if she drifts
		// too far out of sync with it.
		// But seriously, shoutouts to Celeste; I would love to know it better
		// than I do, but from watching speedruns, it looks absolutely terrifying
		// to casualplay o.o;
	};

	GO_Meli.prototype.canPunch = function(){
		return GLOBAL_DIARY.braceletsActive() && !this.holding && !this.punching;
	};
	GO_Meli.prototype.startPunch = function(){
		this.punch_accumulator=-30;
		this.punch_charging = true;
	};
	GO_Meli.prototype.accumulatePunch = function(){
		if (!this.controls.held.B){
			this.punch_accumulator=0;
			this.punch_charging = false;
		}
		else if (this.punch_charging && !GLOBAL_DIARY.braceletsActive()){
			this.punch_accumulator=0;
			this.punch_charging = false;
			this.playSound("FX_oof")
		}
		else{
			++this.punch_accumulator;
			if (this.punch_accumulator>0){
				GLOBAL_DIARY.braceletsPunchCharge();
			}
		}
	};
	GO_Meli.prototype.doPunch = function(){
		if (this.punch_accumulator>0){
			this.playSound("FX_yah");
			this._heap.damage = 0;
			// punch damage is logarithmic- each additional point takes twice as long a charge as the last
			// <16 frames = 0 damage (same as a kickwave)
			// 16-32 frames = 1 damage @ ~1/2 second
			// 32-64 frames = 2 damage @ ~1 second
			// 64-128 frames = 3 damage @ ~2 seconds
			// 128-256 frames = 4 damage @ ~4 seconds
			// ...
			this.punch_accumulator = this.punch_accumulator >> 4;
			while (this.punch_accumulator){
				++this._heap.damage;
				this.punch_accumulator = this.punch_accumulator >> 1;
			}

			this.punching=true;
			this.sparkle.getCollisions(this._heap.interactTargets);
			for (this._heap.i=0; this._heap.i<this._heap.interactTargets.length; this._heap.i++){
				if (this._heap.interactTargets[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.DAMAGE){
					this._heap.interactTargets[this._heap.i].game_object.damage(this,this._heap.damage);
				}
			}
			
			this.punch_timer=5;
		}

		this.punch_charging = false;
		this.punch_accumulator = 0;
	};

	GO_Meli.prototype.accumulateLife = function(){
		if (!GLOBAL_DIARY.hasRing) return;
		if (SDS.dlgMgr.dialogues.length>0) return;
		if (this.controls.held.up || this.controls.held.down || this.controls.held.left || this.controls.held.right || this.controls.held.A || this.controls.held.B){
			this.lifering_accumulator = 0;
			return;
		}
		if (GLOBAL_DIARY.life <= 0 || GLOBAL_DIARY.life >= GLOBAL_DIARY.max_life){
			this.lifering_accumulator = 0;
			return;
		}
		++this.lifering_accumulator;
		if ((this.lifering_accumulator>>7) >= GLOBAL_DIARY.life){
			this.lifering_accumulator -= (GLOBAL_DIARY.life<<7);
			++GLOBAL_DIARY.life;
		}
	};

	// ONLY TO AVOID TEDIOUS COPYPASTA
	// this would basically be an inline;
	// be sure not to use it inside an existing this._heap.i loop
	GO_Meli.prototype.reviewSpriteHits = function(){
		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.COLLIDING_S){
			for (this._heap.i=0; this._heap.i<this._heap.spriteMoveRes.collisions.length; this._heap.i++){
				if (this._heap.spriteMoveRes.collisions[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.AUTOPORT){
					this.in_door = this._heap.spriteMoveRes.collisions[this._heap.i].game_object;
					// not going to mess with carrying things between rooms :P
					if (this.holding){
						this.held_obj.popContext(); // Update
						this.held_obj.drop();
						this.held_obj = null;
						this.holding=false;
					}
				}
				if (this._heap.spriteMoveRes.collisions[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.TRIGGER){
					this.triggering = this._heap.spriteMoveRes.collisions[this._heap.i].game_object;
				}
				if (this._heap.spriteMoveRes.collisions[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.INFLICT){
					this.recent_damage_sources.push(this._heap.spriteMoveRes.collisions[this._heap.i]);
					// deduct damage
					if (this.hurt_timer==0 && this._heap.oldSources.indexOf(this._heap.spriteMoveRes.collisions[this._heap.i])==-1){
						this._heap.damage = this._heap.spriteMoveRes.collisions[this._heap.i].game_object.inflict();
						this.takedamage(this._heap.damage);
					}
				}
				if (this._heap.spriteMoveRes.collisions[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.COLLECT){
					//this._heap.spriteMoveRes.collisions[this._heap.i].game_object.collect(this);
					this.collecting = this._heap.spriteMoveRes.collisions[this._heap.i].game_object;
				}
			}
		}

		// this was commented out / omitted for awhile, 
		// then added back, then moved to the consolidated
		// reviewSpriteHits routine so it happens with ALL checks.
		// future-me: if you encounter regression issues,
		// it was added back in to support BossRoom ejection
		// from solid blocks into solid beamwalls
		// (along with an added test in Sprite.move for ejections
		// back into solid sprites)
		if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.CRUSHED){
			this.crush();
		}
	};

	GO_Meli.prototype.Update = function(){
		this.tail.x = this.sprite.x;
		this.tail.y = this.sprite.y;

		this.pushing=this.pushing>>1;

		this._heap.dx = 0;
		this._heap.dy = 0;
		//var old_xs = [];
		//var new_xs = [];
		//var dxs = [];
		//var old_ys = [];
		//var new_ys = [];
		//var dys = [];
		this._heap.old_dir = this.directon;

		//var heldMoveRes = null;

		this._heap.leftMoveArr = null;
		this._heap.rightMoveArr = null;
		this._heap.upMoveArr = null;
		this._heap.downMoveArr = null;

		this._heap.oldAnimState = this.anim_state;

		this._heap.i=0;

		if (this.tail.isTouchingTerrain(DEFINE_MAP_TERRAIN_TYPES.WATER) && !this.tail.isTouchingTerrain(DEFINE_MAP_TERRAIN_TYPES.AIR)){
			this.anim_state = this.ANIM_STATES.bob;
			this.grounded = false;
			
			if (typeof(this.in_water_cache)!=='undefined' && !this.in_water_cache){
				this._heap.splash = new GO_Splash(1);
				this._heap.splash.spawnOnSprite(this.sprite,this.manager);
				this._heap.splash.sprite.y+=7;
				this._heap.splash=null; // until pooling implemented
			}
			this.in_water_cache=true;

			this._heap.leftMoveArr = this.leftward_move_water;
			this._heap.rightMoveArr = this.rightward_move_water;
			this._heap.upMoveArr = this.upward_move_water;
			this._heap.downMoveArr = this.downward_move_water;
//			this.jumpArr = (GLOBAL_DIARY.hasBelt) ? this.jump_rise_high : this.jump_rise_normal;

			if (this.hurt_timer>0){
				this._heap.dy=0;
				this._heap.dx=-this.direction;
				this.anim_state = this.ANIM_STATES.hurt;
			}
			else{
				if (this.controls.held.left){
					this.horizontal_move_index--;
					if (this.horizontal_move_index>0){
						this.horizontal_move_index--;
					}
					//this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
					this.direction=-1;
					this.anim_state = this.ANIM_STATES.swim;
				}
				else if (this.controls.held.right){
					this.horizontal_move_index++;
					if (this.horizontal_move_index<0){
						this.horizontal_move_index++;
					}
					//this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);
					this.direction=1;
					this.anim_state = this.ANIM_STATES.swim;
				}
				else{
					if (this.horizontal_move_index>0){
						this.horizontal_move_index--;
					}
					else if (this.horizontal_move_index<0){
						this.horizontal_move_index++;
					}
				}

				if (this.controls.held.up){
					this.vertical_move_index--;
					if (this.vertical_move_index>0){
						this.vertical_move_index--;
					}
					//this.vertical_move_index = Math.max(this.vertical_move_index, -(this._heap.upMoveArr.length)+1);
					this.anim_state = this.ANIM_STATES.swim;
				}
				else if (this.controls.held.down){
					this.vertical_move_index++;
					if (this.vertical_move_index<0){
						this.vertical_move_index++;
					}
					//this.vertical_move_index = Math.min(this.vertical_move_index, this._heap.downMoveArr.length-1);
					this.anim_state = this.ANIM_STATES.swim;
				}
				else{
					if (this.vertical_move_index>0){
						this.vertical_move_index--;
					}
					else if (this.vertical_move_index<0){
						this.vertical_move_index++;
					}
				}

				// in case our moveArr changed to a smaller one between frames and we're still coasting at a higher value
				this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
				this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);
				this.vertical_move_index = Math.max(this.vertical_move_index, -(this._heap.upMoveArr.length)+1);
				this.vertical_move_index = Math.min(this.vertical_move_index, this._heap.downMoveArr.length-1);

				this._heap.dx = (this.horizontal_move_index>0) 
					? this._heap.rightMoveArr[this.horizontal_move_index]
					: this._heap.leftMoveArr[-(this.horizontal_move_index)];
				this._heap.dy = (this.vertical_move_index>0) 
					? this._heap.downMoveArr[this.vertical_move_index]
					: this._heap.upMoveArr[-(this.vertical_move_index)];

				// yes, the dash mechanic is as conceptually bolted-on as it appears.
				// still, the game needs it.
				// the ghettoer, the bhettoer
				this.checkDash();
				if (this.dashing){
					if (this.dash_directions.up) this._heap.dy-=this.dash;
					if (this.dash_directions.down) this._heap.dy+=this.dash;
					if (this.dash_directions.right) this._heap.dx+=this.dash;
					if (this.dash_directions.left) this._heap.dx-=this.dash;
				}
			}

			this.tail.collision_flags = (this._heap.dy<0) ? DEFINE_SPRITE_COLLISION_FLAGS.AIR : 0;

			this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);
			this.tail.move(this._heap.dx,this._heap.dy,  this._heap.tailMoveRes);


			if ((this._heap.spriteMoveRes.flags | this._heap.tailMoveRes.flags) & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
				this.horizontal_move_index = 0;
				if (this._heap.spriteMoveRes.flags & this._heap.tailMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
					// if both were ejected, use whichever got least far
					this.sprite.x = (this._heap.dx>0) 
						? Math.min(this.sprite.x, this.tail.x)
						: Math.max(this.sprite.x,this.tail.x);
				}
				else if (this._heap.tailMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
					// if only the tail was ejected, move the sprite to the tail
					this.sprite.x = this.tail.x;
				}
				// and if only the sprite was ejected, it will catch up next update

				if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_SH){
					for (this._heap.i=0; this._heap.i<this._heap.spriteMoveRes.ejections_h.length; this._heap.i++){
						if (this._heap.spriteMoveRes.ejections_h[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PUSH){
							this._heap.spriteMoveRes.ejections_h[this._heap.i].game_object.push(this._heap.dx);
							this.pushing=4;
						}
					}
				}

			}
			if ((this._heap.spriteMoveRes.flags | this._heap.tailMoveRes.flags) & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
				this.vertical_move_index = 0;
				if (this._heap.spriteMoveRes.flags & this._heap.tailMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
					// if both were ejected, use whichever got least far
					this.sprite.y = (this._heap.dy>0) 
						? Math.min(this.sprite.y, this.tail.y)
						: Math.max(this.sprite.y,this.tail.y);
				}
				else if (this._heap.tailMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
					// if only the tail was ejected, move the sprite to the tail
					this.sprite.y = this.tail.y;
				}
				// and if only the sprite was ejected, it will catch up next update
			}

			this._heap.oldSources.length=0;
			while(this.recent_damage_sources.length){
				this._heap.oldSources.push(this.recent_damage_sources.shift());
			}
			//var oldSources = this.recent_damage_sources;
			//this.recent_damage_sources.length=0;// = [];
			this.reviewSpriteHits();

			if (this.hurt_timer==0){
				// check for jump transition or continuation: single-tap when valid
				if (this.controls.pressed.A && this.canWaterJump()){
					this.doWaterJump();
				}
				// dash trigger: double-tap off a kick
				else if (this.controls.pressed.A && this.kicking && (this.controls.held.left || this.controls.held.right || this.controls.held.up || this.controls.held.down) && this.canDash(true)){
					this.doDash();
				}
				// kick trigger: single-tap when valid
				else if (this.controls.pressed.A && this.canKick(true)){
					this.doKick(true);
				}
				// continue jump: passive hold
				else if (this._heap.oldAnimState == this.ANIM_STATES.jump && this.controls.held.A){
					this.jump_index++;
					this.anim_state = this.ANIM_STATES.jump;
				}
			}
			if (this.anim_state == this.ANIM_STATES.jump){
				if (this.jump_index >= this.jumpArr.length-1){
					this.anim_state = (this._heap.dx!=0 || this._heap.dy!=0) ? this.ANIM_STATES.swim : this.ANIM_STATES.bob;
				}
				else{
					// apply jump dy
					this._heap.dx = 0;
					this._heap.dy = this.jumpArr[this.jump_index];

					this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);

					if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
						this.jump_index = this.jumpArr.length-1;
						//...and trust physics will sort itself out next frame
					}
					this.reviewSpriteHits();
				}
			}
			if (this.anim_state != this.ANIM_STATES.jump && this.tail.isTouchingTerrain(DEFINE_MAP_TERRAIN_TYPES.AIR)){
				this.anim_state = this.ANIM_STATES.fall;
				if (this._heap.oldAnimState!=this.ANIM_STATES.fall){
					this.fall_index=0;
				}
			}
			// even though we're touching water, still compute fall if we're in the state (tail isn't fully submerged)
			if (this.anim_state == this.ANIM_STATES.fall){
				if (this._heap.oldAnimState==this.ANIM_STATES.fall){
					this.fall_index++;
					//this.fall_index = Math.min(this.fall_index, this.fall.length-1);
				}
				// in case our moveArr changed to a smaller one between frames and we're still coasting at a higher value
				this.fall_index = Math.min(this.fall_index, this.fall.length-1);
				// apply fall dy
				this._heap.dx = 0;
				this._heap.dy = this.fall[this.fall_index];

				this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);

				// and next frame, if we're touching water or hit the grounded check, we'll transition anyway
				// no need to do it expressly here.
				this.reviewSpriteHits();
			}
		}
		else{ // air/land movement
			this._heap.wasGrounded = this.grounded;
			
			if (this.in_water_cache){
				this._heap.splash = new GO_Splash(0);
				this._heap.splash.spawnOnSprite(this.sprite,this.manager);
				this._heap.splash.sprite.y+=9;
				this._heap.splash=null; // until pooling implemented
			}
			this.in_water_cache=false;

			// resolve horizontal movement
			this.anim_state = this.ANIM_STATES.stand;

			if (GLOBAL_DIARY.collarActive()){ // with collar, speed & height are determined by belt & circlet
				this._heap.leftMoveArr = (!this.grounded & GLOBAL_DIARY.hasCirclet) ? this.leftward_move_fly : this.leftward_move_air;
				this._heap.rightMoveArr = (!this.grounded & GLOBAL_DIARY.hasCirclet) ? this.rightward_move_fly : this.rightward_move_air;
			}
			else if (GLOBAL_DIARY.hasCirclet){ // with only circlet, flounder on ground but can fly once airborne
				this._heap.leftMoveArr = this.grounded ? this.leftward_move_flounder : this.leftward_move_fly;
				this._heap.rightMoveArr = this.grounded ? this.rightward_move_flounder : this.rightward_move_fly;
			}
			else{
				this._heap.leftMoveArr = this.grounded ? this.leftward_move_flounder : this.leftward_move_air;
				this._heap.rightMoveArr = this.grounded ? this.rightward_move_flounder : this.rightward_move_air;
			}

			if (this.hurt_timer>0){
				this._heap.dy=0;
				this._heap.dx=-this.direction;
				this.anim_state = this.ANIM_STATES.hurt;
			}
			else{
				if (this.controls.held.left){
					this.horizontal_move_index--;
					if (this.horizontal_move_index>0){
						this.horizontal_move_index--;
					}
					//this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
					this.direction=-1;
					this.anim_state = this.ANIM_STATES.run;
				}
				else if (this.controls.held.right){
					this.horizontal_move_index++;
					if (this.horizontal_move_index<0){
						this.horizontal_move_index++;
					}
					//this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);
					this.direction=1;
					this.anim_state = this.ANIM_STATES.run;
				}
				else{
					if (this.horizontal_move_index>0){
						this.horizontal_move_index--;
					}
					else if (this.horizontal_move_index<0){
						this.horizontal_move_index++;
					}
				}
			
				// in case our moveArr changed to a smaller one between frames and we're still coasting at a higher value
				this.horizontal_move_index = Math.max(this.horizontal_move_index, -(this._heap.leftMoveArr.length)+1);
				this.horizontal_move_index = Math.min(this.horizontal_move_index, this._heap.rightMoveArr.length-1);

				this._heap.dx = (this.horizontal_move_index>0) 
					? this._heap.rightMoveArr[this.horizontal_move_index]
					: this._heap.leftMoveArr[-(this.horizontal_move_index)];
				this._heap.dy = 0;
			}

			// yes, the dash mechanic is as conceptually bolted-on as it appears.
			// still, the game needs it.
			// the ghettoer, the bhettoer
			this.checkDash();
			if (this.dashing){
				//if (this.dash_directions.up) this._heap.dy-=this.dash; // no vertical dashes in air because double-jump is its own power
				if (this.dash_directions.down) this._heap.dy+=this.dash; // ok, ok, downward is kosher
				if (this.dash_directions.right) this._heap.dx+=this.dash;
				if (this.dash_directions.left) this._heap.dx-=this.dash;

				// some additional steer if you have blessing
				if (GLOBAL_DIARY.blessingActive()){
					if (this.controls.held.up) this._heap.dy-=1;
					if (this.controls.held.down) this._heap.dy+=1;

					// also sparkles
					this._heap.sparkle = new GO_Sparkle(16);
					this._heap.sparkle.spawnOnSprite(this.sprite,this.manager);
					this._heap.sparkle.sprite.y+=((this.sprite.x&15)^0xa)-8;
					this._heap.sparkle.sprite.x+=((this.sprite.y&15)^0xa)-4;
					this._heap.sparkle=null; // until pooling implemented
				}
			}

			this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);

			if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_H){
				this.horizontal_move_index = 0;
			}

			// quick check for grounded vs terrain
			// (grounded by fall state + EJECTED_V is later)
			this.grounded = 
				!(this._heap.oldAnimState==this.ANIM_STATES.jump && this.jump_index < this.jumpArr.length-1 && this.controls.held.A) &&
				(
					(this.sprite.map_room.findVertically(
						DEFINE_MAP_TERRAIN_TYPES.GROUND,
						this.sprite.x+this.sprite.bottom_left.x, this.sprite.y+this.sprite.bottom_left.y,
						1,
						1,
						this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
					) !=-1) ||
					(this.sprite.map_room.findVertically(
						DEFINE_MAP_TERRAIN_TYPES.GROUND,
						this.sprite.x+this.sprite.bottom_right.x, this.sprite.y+this.sprite.bottom_right.y,
						1,
						1,
						this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
					) !=-1)
				);

			if ((this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_FLAGS.EJECTED_SH) && this._heap.wasGrounded){ // can't use this.grounded since it won't account for standing on SOLID objects until later
				for (this._heap.i=0; this._heap.i<this._heap.spriteMoveRes.ejections_h.length; this._heap.i++){
					if (this._heap.spriteMoveRes.ejections_h[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PUSH){
						this._heap.spriteMoveRes.ejections_h[this._heap.i].game_object.push(this._heap.dx);
						this.pushing=4;
					}
				}
			}

			this._heap.oldSources.length=0;
			while(this.recent_damage_sources.length){
				this._heap.oldSources.push(this.recent_damage_sources.shift());
			}
			//var oldSources = this.recent_damage_sources;
			//this.recent_damage_sources.length=0;// = [];
			this.reviewSpriteHits();

			if (this.hurt_timer==0){
				// check for jump transition: single tap when valid
				if (this.controls.pressed.A && !(GLOBAL_DIARY.collarActive() && this.controls.held.down) && (this.canHighJump() || this.canJump() || this.canFlounder() || this.canFly())){
					if (this.canHighJump()) this.doHighJump();
					else if (this.canJump()) this.doJump();
					else if (this.canFly()) this.doFly();
					else if (this.canFlounder()) this.doFlounder();
				}
				// dash trigger: double-tap off a kick
				else if (this.controls.pressed.A && this.kicking && (this.controls.held.left || this.controls.held.right || this.controls.held.up || this.controls.held.down) && this.canDash(false)){
					this.doDash();
				}
				// kick trigger: single-tap when overriding jump
				else if (this.controls.pressed.A && (!this._heap.wasGrounded || this.controls.held.down) && this.canKick(false)){
					this.doKick(false);
				}
				// sustain jump: passive hold
				else if (this._heap.oldAnimState == this.ANIM_STATES.jump && this.controls.held.A){
					this.jump_index++;
					this.anim_state = this.ANIM_STATES.jump;
				}
				// no input: cancel jump
				else if (!this.grounded){
					this.jump_index = this.jumpArr.length-1;
					this.anim_state = this.ANIM_STATES.fall;
					if (this._heap.oldAnimState!=this.ANIM_STATES.fall){
						this.fall_index = 0;
					}
				}
			}

			// jump/fall handling
			if (this.anim_state == this.ANIM_STATES.jump){
				if (this.jump_index >= this.jumpArr.length-1){
					this.anim_state = this.ANIM_STATES.fall;
					if (this._heap.oldAnimState!=this.ANIM_STATES.fall){
						this.fall_index = 0;
					}
				}
				else{
					// apply jump dy
					this._heap.dx = 0;
					this._heap.dy = this.jumpArr[this.jump_index];

					this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);
					this.grounded=false; // to not confuse the flounder check

					if (this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V){
						this.jump_index = this.jumpArr.length-1;
						//...and trust physics will sort itself out next frame
					}

					this.reviewSpriteHits();
				}
			}
			// with circlet, non-downwards air-dash state disables gravity
			if (!(this.dashing && !this.dash_directions.down && GLOBAL_DIARY.circletActive())){
				if (this.anim_state == this.ANIM_STATES.fall){
					if (this._heap.oldAnimState==this.ANIM_STATES.fall){
						this.fall_index++;
						//this.fall_index = Math.min(this.fall_index, this.fall.length-1);
					}
					// in case our moveArr changed to a smaller one between frames and we're still coasting at a higher value
					this.fall_index = Math.min(this.fall_index, this.fall.length-1);
					// apply fall dy
					this._heap.dx = 0;
					this._heap.dy = this.fall[this.fall_index];

					this.sprite.move(this._heap.dx,this._heap.dy,  this._heap.spriteMoveRes);

					// and next frame, if we're touching water or hit the grounded check, we'll transition anyway
					// no need to do it expressly here.
					if ((this._heap.spriteMoveRes.flags & DEFINE_SPRITE_MOVE_RESULT_EJECTED_V) ||
						(this.sprite.map_room.findVertically(
							DEFINE_MAP_TERRAIN_TYPES.GROUND,
							this.sprite.x+this.sprite.bottom_left.x, this.sprite.y+this.sprite.bottom_left.y,
							1,
							1,
							this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
						) !=-1) ||
						(this.sprite.map_room.findVertically(
							DEFINE_MAP_TERRAIN_TYPES.GROUND,
							this.sprite.x+this.sprite.bottom_right.x, this.sprite.y+this.sprite.bottom_right.y,
							1,
							1,
							this.sprite.constraints.x==DEFINE_SPRITE_MOVE_CONSTRAINT.WRAP
						) !=-1)){
						this.grounded = true;
						this.anim_state = (this.controls.held.left || this.controls.held.right) ? this.ANIM_STATES.run : this.ANIM_STATES.stand;
					}

					this.reviewSpriteHits();
				}
			}

			//if (this.grounded && !GLOBAL_DIARY.hasCollar){
			//	this.anim_state = this.ANIM_STATES.flounder;
			//	if (!this._heap.wasGrounded) this.playSound("FX_oof");
			//}
			if (this.grounded){
				if (!GLOBAL_DIARY.collarActive()){
					this.anim_state = this.ANIM_STATES.flounder;
					if (!this._heap.wasGrounded) this.playSound("FX_oof");
				}
				else{
					GLOBAL_DIARY.collarDrain();
					if (!this._heap.wasGrounded) this.playSound("FX_eh");
				}
			}

		}

		//this._heap.interactTargets.length=0;// = [];
		// use up to go through doors
		/*if (this.controls.pressed.up && this.hurt_timer==0){
			this._heap.interactTargets = this.sprite.getCollisions();			
			for (this._heap.i=0; this._heap.i<this._heap.interactTargets.length; this._heap.i++){
				if ((this._heap.interactTargets[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PORT) && 
					this._heap.interactTargets[this._heap.i].game_object.getDestName()){

					this.in_door = this._heap.interactTargets[this._heap.i].game_object;
					// not going to mess with carrying things between rooms :P
					if (this.holding){
						this.held_obj.popContext(); // Update
						this.held_obj.drop();
						this.held_obj = null;
						this.holding=false;
					}
				}
			}
		}*/


		if (this.hurt_timer==0){
			if (this.controls.pressed.B){
				
				this.sparkle.getCollisions(this._heap.interactTargets);

				// use interact button mainly to grab
				for (this._heap.i=0; this.canGrab() && this._heap.i<this._heap.interactTargets.length; this._heap.i++){
					this.tryGrab(this._heap.interactTargets[this._heap.i].game_object);
				}

				// use interact button to go through doors
				// higher priority than inspect/converse, lower priority than grab
				if (!this.holding){
					this.sprite.getCollisions(this._heap.portTargets);
					for (this._heap.i=0; this._heap.i<this._heap.portTargets.length; this._heap.i++){
						if ((this._heap.portTargets[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.PORT) && 
							this._heap.portTargets[this._heap.i].game_object.getDestName()){

							this.in_door = this._heap.portTargets[this._heap.i].game_object;
							// not going to mess with carrying things between rooms :P
							if (this.holding){
								this.held_obj.popContext(); // Update
								this.held_obj.drop();
								this.held_obj = null;
								this.holding=false;
							}
						}
					}
				}
				
				// split off second loop for checking to be sure grab always gets priority
				if (!this.holding){
					this.check_timer = (this.in_door) ? 2 : 30;
				}

				// append body collision to checklist since for many investigables,
				// overlap detection makes sense.
				this.sprite.getCollisions(this._heap.interactTargets,true); 
				
				for (this._heap.i=0; !this.in_door && !this.holding && this._heap.i<this._heap.interactTargets.length; this._heap.i++){
					if (this._heap.interactTargets[this._heap.i].game_object.interactions & DEFINE_GAMEOBJECT_INTERACTIONS.CHECK){
						this.checking = this._heap.interactTargets[this._heap.i].game_object;
						break;
					}
				}

				// finally, if no available targets, start a punch charge
				if (!this.holding && !this.checking && !this.in_door && this.canPunch()){
					this.startPunch();
				}
			}
			if (this.controls.held.B && this.punch_charging){
				this.accumulatePunch();
			}

			if (this.controls.released.B){
				if (this.holding){
					this.held_obj.popContext(); // Update
					this.held_obj.drop();
					this.held_obj = null;
					this.holding=false;
				}
				else if (this.punch_charging){
					this.doPunch();
				}
			}
		}
		// pretty sure charging will clear on hurt OR on B-release, 
		// but it would make for very weird exploits and possible bad states
		// if someone managed to wriggle around both cutoffs
		this.punch_charging &= this.controls.held.B;

		this.sparkle.x = (this.direction>0) ? this.sprite.x+15 : this.sprite.x-7;
		this.sparkle.y = this.sprite.y+4;
		// pre-apply object's IndisposedUpdate in case it causes itself to be un-held
		if (this.holding && this.held_obj.IndisposedUpdate) this.held_obj.IndisposedUpdate();
		if (this.holding){
			//this.sparkleFrame = (this.sparkleFrame+1) & 7;
			//this.sparkle.frame = this.sparkleFrame;

			// Flipping around into a wall while holding a solid thing can sometimes cause crush
			// but would otherwise resolve nicely.  So, just as a hack to no doubt be exploited
			// by future speedrunners, disable crushability for a frame when turning around
			// with a thing.
			if (this.direction!=this._heap.old_dir){
				this.interactions &= ~DEFINE_GAMEOBJECT_INTERACTIONS.FORCE_CRUSH;
			}
			else{
				this.interactions |= DEFINE_GAMEOBJECT_INTERACTIONS.FORCE_CRUSH;
			}
			// except with some more recent revisions to crush logic,
			// this seems to still be a problem.  See additional steps below.

			this._heap.newHeldX = (this.sparkle.x+4);
			this._heap.newHeldX -= (this.direction<0) ? this.held_obj.hoistpoint[0].x : this.held_obj.hoistpoint[1].x;
			this._heap.newHeldY = (this.sparkle.y+4);
			this._heap.newHeldY -= (this.direction<0) ? this.held_obj.hoistpoint[0].y : this.held_obj.hoistpoint[1].y;
			// very elegant way to resolve carrying: try to move it to its new home.  If it doesn't go, drop it.
			this.held_obj.sprite.move(this._heap.newHeldX-this.held_obj.sprite.x,this._heap.newHeldY-this.held_obj.sprite.y);

			// per above crush issue, if colliding with meli after motion,
			// first step is to try to back meli out, since basic collision
			// logic is no longer so generous.
			this.sprite.testCollision(this.held_obj.sprite,this.direction,1,  this._heap.gripPenetration);
			if (this._heap.gripPenetration.x>0){
				this.sprite.move(-this.direction * this._heap.gripPenetration.x,0,  this._heap.spriteMoveRes);
			}

			// NOW drop if sparkle isn't still touching.
			//var gripPenetration = this.sparkle.testCollision(this.held_obj.sprite,1,1);
			this.sparkle.testCollision(this.held_obj.sprite,1,1,  this._heap.gripPenetration);
			if (this._heap.gripPenetration.x==0 || this._heap.gripPenetration.y==0){
				this.held_obj.popContext(); // Update
				this.held_obj.drop();
				this.held_obj = null;
				this.holding=false;
			}
		}
		else{
			//this.sparkleFrame = 0;
			//this.sparkle.frame = this.sparkleFrame;
			this.interactions |= DEFINE_GAMEOBJECT_INTERACTIONS.FORCE_CRUSH;
		}

		if (this.holding || this.punching){
			this.sparkleFrame = (this.sparkleFrame+1) & 7;
			this.sparkle.frame = this.sparkleFrame;
		}
		else{
			this.sparkleFrame = 0;
			this.sparkle.frame = this.sparkleFrame;
		}
		

		this._heap.kickFrame = 0;
		if (this.kicking){
			this.kick_timer++;
			this._heap.kickFrame = this.kick_timer>>1;
			if (this.kick_timer == this.anim_frames[this.ANIM_STATES.kick].length){
				if (this.holding){
					this.held_obj.popContext(); // Update
					this.held_obj.drop();

					MGO_Projectile(
						this.held_obj,
						this.direction,
						this.controls.held.up ? -1 : this.controls.held.down ? 1 : 0);

					this.held_obj = null;
					this.holding=false;
				}

				else if (!this.dashing){
					// fire ze kickwave!
					if (GLOBAL_DIARY.blessingActive() && GLOBAL_DIARY.chi>0){
						this._heap.wave = new GO_DarkWave(GLOBAL_DIARY.blessingShot(),this.kick_direction,this.sprite.constraints);
						this._heap.wave.spawnOnSprite(this.sprite,this.manager);
						this._heap.wave.sprite.x += this.direction<<2;
						this.playSound("FX_zown");

						// DEBUG ONLY - but don't worry, there IS a way for Meli to get the trident legitimately- just visit Samus at the Rose Town inn and...
						//if (this.manager.getNamedObject("Malfose")){
						//	this._heap.wave = new GO_BattleTrident(true,"Malfose","Meli");
						//	this._heap.wave.spawnOnSprite(this.sprite,this.manager);
						//}
					}
					else{
						this._heap.wave = new GO_KickWave(this.kick_strength,this.kick_direction,this.sprite.constraints);
						this._heap.wave.spawnOnSprite(this.sprite,this.manager);
						this.playSound("FX_swish");
					}
					
					
					this._heap.wave=null; // pending pooling
				}
			}
			if (this._heap.kickFrame >= this.anim_frames[this.ANIM_STATES.kick].length){
				this.kicking=false;
			}
		}

		// collar/movement chi drain is (necessarily) accounted for in the movement logic.

		// bracelets chi drain
		if (this.holding){
			GLOBAL_DIARY.braceletsHold(this.held_obj.weight);
		}
		if (this.holding && !GLOBAL_DIARY.braceletsActive()){
			this.held_obj.popContext(); // Update
			this.held_obj.drop();
			this.held_obj = null;
			this.holding=false;
		}

		// dash chi drain
		if (this.dashing){
			GLOBAL_DIARY.collarDash();
		}
		if (this.dashing && !GLOBAL_DIARY.collarActive()){
			this.dashing = false;
			this.playSound("FX_oof");
		}

		// chi recharge
		GLOBAL_DIARY.chiCharge();
		
		if (this.check_timer>0){
			if (this.anim_state == this.ANIM_STATES.bob || this.anim_state == this.ANIM_STATES.stand){
				this.anim_state = this.tail.isTouchingTerrain(DEFINE_MAP_TERRAIN_TYPES.WATER) ? this.ANIM_STATES.bob_check : this.ANIM_STATES.stand_check;
				--this.check_timer;
			}
			else{
				this.check_timer=0;
			}
		}

		if (this._heap.oldAnimState != this.anim_state){
			this.frame_timer=0;
		}
		this.frame_timer++;
		this._heap.aniFrame = (this.frame_timer>>2) % this.anim_frames[this.anim_state].length;

		this.sprite.frame = this.anim_frames[this.anim_state][this._heap.aniFrame];
		if (this.kicking){
			this.sprite.frame = this.anim_frames[this.ANIM_STATES.kick][this._heap.kickFrame];
		}

		// no control of body in air w/o belt- once at velocity, go headfirst
		if (this.anim_state===this.ANIM_STATES.fall && this.fall_index>(this.fall.length>>1) && !GLOBAL_DIARY.hasBelt){
			this.sprite.frame = this.anim_frames[this.ANIM_STATES.dive][this._heap.aniFrame];
		}
		// icing on the cake
		else if (this.anim_state===this.ANIM_STATES.fall && !this.kicking){
			this._heap.waterLeft = this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.WATER,
					this.sprite.x+this.sprite.bottom_left.x, this.sprite.y+this.sprite.bottom_left.y,
					1,
					32,
					false
				);
			this._heap.waterRight = (this._heap.waterLeft!=-1) ? 
				this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.WATER,
					this.sprite.x+this.sprite.bottom_right.x, this.sprite.y+this.sprite.bottom_right.y,
					1,
					32,
					false
				) : -1;
			this._heap.groundLeft = (this._heap.waterLeft!=-1) ? 
				this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.GROUND,
					this.sprite.x+this.sprite.bottom_left.x, this.sprite.y+this.sprite.bottom_left.y,
					1,
					32,
					false
				) : -1;
			this._heap.groundRight = (this._heap.waterRight!=-1) ?
				this.sprite.map_room.findVertically(
					DEFINE_MAP_TERRAIN_TYPES.GROUND,
					this.sprite.x+this.sprite.bottom_right.x, this.sprite.y+this.sprite.bottom_right.y,
					1,
					32,
					false
				) : -1;
			if (this._heap.waterLeft!=-1 && (this._heap.groundLeft==-1 || this._heap.groundLeft>this._heap.waterLeft) && this._heap.waterRight!=-1 && (this._heap.groundRight==-1 || this._heap.groundRight>this._heap.waterRight)){
				this.sprite.frame = this.anim_frames[this.ANIM_STATES.dive][this._heap.aniFrame];
			}
		}

		// again, not treating dash as a dedicated animation state, just a conditional spriteswap
		if (this.dashing){
			if (this.dash_directions.left || this.dash_directions.right){
				this.sprite.frame = this.anim_frames[this.ANIM_STATES.dash][0];
			}
			else if (this.dash_directions.up){
				this.sprite.frame = this.anim_frames[this.ANIM_STATES.dash_up][0];
			}
			else if (this.dash_directions.down){
				this.sprite.frame = this.anim_frames[this.ANIM_STATES.dash_down][0];
			}
		}
		if (this.punching){
			this.sprite.frame = this.anim_frames[this.ANIM_STATES.punch][0];
			--this.punch_timer;
			this.punching &= this.punch_timer>0;
		}

		this.accumulateLife();

		if (this.hurt_timer>0){
			--this.hurt_timer;
			
			// in theory, being life<=0 has disabled interactions which can cause life gain
			// but if a lethal damage source and a lifeup were hit on the same frame (unsure if this can even happen...)
			// we'd never trip Deathwarp and never get out of I-frames.
			// so instead check the new dead flag
			if (this.hurt_timer==4 && this.dead){//GLOBAL_DIARY.life<=0){
				this.Deathwarp();
			}
		}
		if (this.iframes>0) --this.iframes;

		this.sprite.frame = this.sprite.frame & 27;
		if ((this.holding || this.pushing) && !this.dashing) this.sprite.frame+=4;

		this.sprite.frame = this.sprite.frame & 31;
		if (this.direction<0) this.sprite.frame+=32;

		if (this.overrideFrame!=null) this.sprite.frame=this.overrideFrame;

		this.sprite.visible = !(this.iframes&1);

		for (this._heap.i=0; this._heap.i<this.treasures.length; ++this._heap.i){
			this.treasures[this._heap.i].frame = this.sprite.frame;
			this.treasures[this._heap.i].x = this.sprite.x;
			this.treasures[this._heap.i].y = this.sprite.y;
		}
		this.braceletsOverlay.visible = GLOBAL_DIARY.hasBracelets && !GLOBAL_DIARY.hasBlessing && this.sprite.visible;
		this.necklaceOverlay.visible = GLOBAL_DIARY.hasCollar && !GLOBAL_DIARY.hasBlessing && this.sprite.visible;
		this.beltOverlay.visible = GLOBAL_DIARY.hasBelt && !GLOBAL_DIARY.hasBlessing && this.sprite.visible;
		this.circletOverlay.visible = GLOBAL_DIARY.hasCirclet && !GLOBAL_DIARY.hasBlessing && this.sprite.visible;
		this.blessingOverlay.visible = GLOBAL_DIARY.hasBlessing && this.sprite.visible;
	};

