
/*jshint -W099*/
/* global _*/

if (STEAM){
    var greenworks = require('./greenworks');
    try{
        greenworks.init();
    }
    catch (e) {
        STEAM = false;
    }
}

var BASE_FONT = getFont(14);
ctx.font = BASE_FONT;
ctx.lineWidth = 1;

var TICKS_PER_SECOND = 10;

var KEY_EMOTIONS = ['anger', 'sadness', 'joy', 'fear', 'disgust'];

var EMOTION_GRAIN = 20;
var MAX_KEY_EMOTION = (5 * EMOTION_GRAIN) - 1;

var debugSt = undefined;

var getEntityKey = function(entity){ return entity.identityComponent.key; };
var getUid = function(entity){ return entity.uid; };

var emotionTags = function(){
    return ['alive'];
};

var tickComp = function(comp, ticks){
    ticks = (ticks === undefined) ? 1 : ticks;
    comp.ticks -= ticks;
    if (comp.ticks <= 0){
        comp.ticks = comp.impulseTicks;
        return true;
    }
    return false;
};

var dictString = function(dict){
    return _.reduce(dict, function(memo, val, key){
        var lineString = key + ' - ' + val;
        return memo ? (memo + '\n' + lineString) : lineString;
    }, '');
};

var rendererSystem = function(world, entities, ui, timeToNextTick, lastInterval){
    var currentRenderState = world.entityForKey('gameStateEntity').rendererSystemComponent.currentRenderState;
    var renderers = _.map(_.range(25), function(){ return []; });
    _.each(entities, function(entity){
        var rect = undefined;
        if (entity.positionComponent){
            if (entity.positionComponent.x === undefined){
                console.log(entity);
            }
            else{
                try {
                    var cell = world.hexMap.cellForPosComp(entity.positionComponent);
                }
                catch (e){
                    console.log('cell failure');
                    console.log(entity);
                }
                rect = cell.rect;
            }
        }
        else if (entity.absolutePositionComponent){
            rect = [entity.absolutePositionComponent.x, entity.absolutePositionComponent.y, 
                    entity.absolutePositionComponent.size[0], entity.absolutePositionComponent.size[1]];
        }
        
        _.chain(entity.rendererComponents)
            .filter(function(renderer){
                return renderer.renderState == currentRenderState;
            })
            .each(function(renderer){
                renderers[renderer.priority].push({
                    renderer: renderer, 
                    rect: rect, 
                    entity: entity});
            });
    });
    
    _.each(renderers, function(renderList){
        var rectCount = {};
        _.each(renderList, function(group, i){
            var rectHash = _.reduce(group.rect, addFunction, 0);
            if (rectCount[rectHash] === undefined){
                rectCount[rectHash] = 0;
            }
            rectCount[rectHash] += 1;
            group.renderer.render(group.rect, group.entity, ui, rectCount[rectHash], timeToNextTick, lastInterval);
        });
    });
};

var posEqual = function(pos1, pos2){
    return pos1[0] == pos2[0] &&
        pos1[1] == pos2[1];
};

var determinePath = function(start, dest, paths){
    var currentNodes = [[start]];
    var checkNode = function(node, nodeList){
        return !_.any(nodeList, function(chkNode){
            return posEqual(node, chkNode);
        });
    };

    while (currentNodes.length > 0){
        var newNodes = [];
        for (var i = 0; i < currentNodes.length; i++){
            var node = currentNodes[i][currentNodes[i].length - 1];

            var skipIds = [];

            var momentNodes = _.chain(paths)
                    .map(function(path){
                        if (posEqual(path.pathMoveComponent.startPosition, node) &&
                            checkNode(path.pathMoveComponent.endPosition, currentNodes[i])){
                            skipIds.push(path.uid);
                            return path.pathMoveComponent.endPosition;
                        }

                        if (posEqual(path.pathMoveComponent.endPosition, node) &&
                            checkNode(path.pathMoveComponent.startPosition, currentNodes[i])){
                            skipIds.push(path.uid);
                            return path.pathMoveComponent.startPosition;
                        }

                        return null;
                    })
                    .filter(_.identity)
                    .value();

            if (momentNodes.length === 0){
                continue;
            }
            
            if (_.any(momentNodes, function(momentNode){ return posEqual(momentNode, dest); })){
                var path = currentNodes[i].concat([dest]);
                path.splice(0, 1);
                return path;
            }
            
            newNodes = newNodes.concat(_.map(momentNodes, function(momentNode){
                return currentNodes[i].concat([momentNode]);
            }));

            paths = _.filter(paths, function(path){ return !_.contains(skipIds, path.uid); });
        }

        currentNodes = newNodes;
    }
    return undefined;
};

var filterTrainsByControl = function(world, trains){
    if (world.lostControl()){
        trains = _.filter(trains, function(train){
            var trainPath = world.entityForUid(train.trainVelocityComponent.trainPath);
            return trainPath && !trainPath.interactionComponent.canInteractSelf();
        });
    }
    
    return trains;
};

var trainMoveSystem = function(world, entities){
    var paths = world.getAllPaths();

    var trains = _.filter(entities, function(worldEntity){
        return worldEntity.trainVelocityComponent;
    });

    trains = filterTrainsByControl(world, trains);

    _.chain(trains)
        .filter(function(entity){
            return !entity.trainVelocityComponent.isInteracting;
        })
        .each(function(entity){
            var newPos = entity.trainVelocityComponent.currentPath[0];

            entity.trainVelocityComponent.currentPath.splice(0, 1);
            entity.positionComponent.setPos(newPos);

            _.chain(entities)
                .filter(entity => (entity.positionComponent &&
                                   entity.rendererComponents &&
                                   entity.positionComponent.isPos(newPos)))
                .map(entity => entity.rendererComponents)
                .flatten(true)
                .filter(renderComponent => renderComponent.wobble !== undefined)
                .each(renderComponent => { renderComponent.wobble = 2 * Math.PI;});

            var carriage = entity.trainComponent.getCurrentCarriage(entity, world);

            carriage.positionComponent.x = entity.positionComponent.x;
            carriage.positionComponent.y = entity.positionComponent.y;
            carriage.carriageComponent.holding = _.filter(carriage.carriageComponent.holding, function(holdingId){
                var holding = world.entityForUid(holdingId);
                if (holding){
                    holding.positionComponent.x = carriage.positionComponent.x;
                    holding.positionComponent.y = carriage.positionComponent.y;
                    return true;
                }
                return false;
            });
            
            if (entity.trainVelocityComponent.currentPath.length == 0){
                entity.trainVelocityComponent.isInteracting = true;
            }
        });
    return true;
};

var trainInteractSystem = function(world, entities){
    var trains = _.filter(entities, entity => entity.trainVelocityComponent && entity.trainVelocityComponent.isInteracting);

    trains = filterTrainsByControl(world, trains);
    
    _.each(trains, function(entity){
        var index = entity.trainVelocityComponent.stopIndex;
        var stop = world.entityForUid(entity.trainVelocityComponent.trainStops[index]);

        if (!stop){
            entity.healthComponent.kill();
            return;
        }


        if (stop.managedComponent){
            var trait = world.entityForKey(stop.managedComponent.managerKey);
            if (_.any(entities, function(disableTraitEntity){
                return disableTraitEntity.disableTraitComponent &&
                    disableTraitEntity.disableTraitComponent.checkTraitDisabled(trait);
            })){
                return;
            }
        }
        
        entity.trainVelocityComponent.isInteracting = stop.focusPathStopComponent.interactWithTrain(stop, world, entity);
        
        if (!entity.trainVelocityComponent.isInteracting){
            entity.trainVelocityComponent.stopIndex += 1;

            if (entity.trainVelocityComponent.stopIndex < entity.trainVelocityComponent.allPaths.length){
                entity.trainVelocityComponent.currentPath = entity.trainVelocityComponent.allPaths[entity.trainVelocityComponent.stopIndex];
            }
            else{
                entity.healthComponent.kill();
            }
        }
    });
    
    return true;
};

var junctionSystem = function(world, entities){
    var holdableEntities = _.filter(entities, function(entity){ 
        return entity.pathVelocityComponent &&
            entity.pathVelocityComponent.chosenPath === false; 
    });

    _.chain(entities)
        .filter(function(entity){
            return entity.junctionComponent;
        })
        .each(function(entity){
            entity.junctionComponent.heldItems = _.filter(holdableEntities, function(holdEntity){
                return holdEntity.positionComponent.comparePos(entity.positionComponent);
            });
        });
    return true;
};

var trainPathSystem = function(world, entities){
    var trainPaths = _.filter(entities, entity => entity.trainPathComponent);

    if (world.lostControl()){
        trainPaths = _.filter(trainPaths, trainPath => !trainPath.interactionComponent.canInteractSelf());
    }

    _.each(trainPaths, function(entity){
        if (!entity.trainPathComponent.trainId || !world.entityForUid(entity.trainPathComponent.trainId)){
            if (_.all(entity.trainPathComponent.trainStops, function(stopId){ return world.entityForUid(stopId); })){
                if (entity.trainPathComponent.canMakeTrain(entity, world)){
                    entity.trainPathComponent.makeTrain(entity, world);
                }
            }
            else{
                entity.healthComponent.kill();
            }
        }
    });
    return true;
};

var damageSystem = function(world, entities){
    var relevantEntities = _.chain(entities)
            .filter(function(entity){
                return entity.damageComponent && 
                    (entity.damageComponent.charges > 0) &&
                    !entity.focusPathStopComponent.isDisabled;
            })
            .each(function(entity){
                var damageComp = entity.damageComponent;
                
                if (damageComp.ticks > 0){
                    damageComp.ticks -= 1;
                }
                else{
                    var bullet = undefined;
                    _.chain(entities)
                        .filter(function(targetEntity){
                            return damageComp.checkTargetEntity(targetEntity, entity, world);
                        })
                        .first(damageComp.targetCount)
                        .each(function(target){
                            var damage = damageComp.calculateDamage(target);
                            damage *= target.healthComponent.damageFactor;

                            target.healthComponent.health -= damage;

                            _.each(target.rendererComponents, function(renderer){
                                if (renderer.activated !== undefined){
                                    renderer.activated = 1;
                                }
                            });

                            var baseDamage = damageComp.damage;
                            
                            if (baseDamage > 0){
                                bullet = makeEcs(PARAMETERS.bullet, {
                                    bulletSourcePos: entity.positionComponent,
                                    bulletEndPos: target.positionComponent,
                                    damage: baseDamage,
                                    bonus: damage / baseDamage
                                });
                            }

                            _.each(entity.rendererComponents, function(renderer){
                                if (renderer.activated !== undefined){
                                    renderer.activated = 1;
                                }
                            });

                            damageComp.ticks = damageComp.impulseTicks;
                        });
                    
                    if (entity.healthModifyComponent){
                        entity.healthModifyComponent.activated = true;
                    }
                    
                    if (bullet){
                        world.addEntity(bullet);
                        damageComp.charges -= 1;
                    }
                }
            });
    return true;
};

var daySystem = function(world, entities){
    var keepGoing = true;
    var dayEnded = false;
    _.chain(entities)
        .filter(function(entity){ return entity.dayCounterComponent; })
        .each(function(entity){
            if (entity.dayCounterComponent.ticks == entity.dayCounterComponent.impulseTicks){
                startDay(world, entities);
            }

            var dayPart = entity.dayCounterComponent.getPartOfDay();
            entity.dayCounterComponent.ticks -= 1;
            var newPart = entity.dayCounterComponent.getPartOfDay();
            
            if (entity.dayCounterComponent.ticks <= 0){
                endDay(world, entities);
                entity.healthComponent.health = 0;
                dayEnded = false;
            }
            
            if (newPart != dayPart || (entity.dayCounterComponent.ticks == entity.dayCounterComponent.impulseTicks - 1)){
                _.chain(entities)
                    .filter(function(entity){ return entity.dayResponseComponent !== undefined &&
                                              entity.dayResponseComponent.dayChange !== undefined; })
                    .each(function(entity){
                        entity.dayResponseComponent.dayChange(entity, world, newPart);
                    });
            }
        });

    if (dayEnded){
        _.chain(entities)
            .filter(function(entity){ return entity.healthModifyComponent; })
            .each(function(entity){
                var dHealth = entity.healthComponent += entity.healthModifyComponent.healthModifyAmount();
                entity.healthModifyComponent.activated = false;
            });
    }

    return keepGoing;
};

var endDay = function(world, entities){
    _.chain(entities)
        .filter(function(entity){ return entity.dayResponseComponent !== undefined && 
                                  entity.dayResponseComponent.dayEnd !== undefined; })
        .each(function(entity){
            entity.dayResponseComponent.dayEnd(entity, world);
        });
};

var startDay = function(world, entities){
    _.chain(entities)
        .filter(function(entity){ return entity.dayResponseComponent && entity.dayResponseComponent.dayStart; })
        .each(function(entity){
            entity.dayResponseComponent.dayStart(entity, world);
        });
};

var mapSpawnSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.mapSpawnComponent;                
        })
        .each(function(entity){
            var spawnedEntities = [];
            if (tickComp(entity.mapSpawnComponent)){
                while (entity.mapSpawnComponent.mapShouldSpawn(entity, world, spawnedEntities)){
                    var spawnedEntity = entity.mapSpawnComponent.mapSpawn(entity, world, spawnedEntities);
                    if (spawnedEntity){
                        spawnedEntities.push(spawnedEntity);
                    }
                }
            }
        });
    return true;
};

var deriveEmotionTickModifiers = function(entities, tags){
    var emotionTickModifiers = {};

    _.chain(entities)
        .filter(function(entity){
            return entity.emotionTickModifyComponent;
        })
        .pluck('emotionTickModifyComponent')
        .each(function(emotionTickModifyComponent){
            if (_.contains(tags, emotionTickModifyComponent.emotionState)){
                if (emotionTickModifiers[emotionTickModifyComponent.towerTrait] === undefined){
                    emotionTickModifiers[emotionTickModifyComponent.towerTrait] = 1;
                }
                emotionTickModifiers[emotionTickModifyComponent.towerTrait] *= emotionTickModifyComponent.factor / 100;
            }
        });
    
    return emotionTickModifiers;
};

var updateCount = function(count, currency){
    _.each(count, function(val, key){
        if (currency[key] !== undefined){
            count[key] -= currency[key];
        }
    });
};

var notificationSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.notifyComponent;
        })
        .each(function(entity){
            var notifyEntities = entity.notifyComponent.notifyEntityKey ?
                world.entitiesForKey(entity.notifyComponent.notifyEntityKey) :
                entities;

            _.chain(notifyEntities)
                .filter(function(notifyEntity){
                    return notifyEntity.notificationComponents && notifyEntity.notificationComponents.length > 0;
                })
                .each(function(notifyEntity){
                    _.each(notifyEntity.notificationComponents, function(notificationComponent){
                        var fn = notificationComponent.notificationFunctions[entity.notifyComponent.notificationKey];
                        if (fn !== undefined){
                            fn(notifyEntity, entity.notifyComponent.notificationArgs, world);
                        }
                    });
                });
            entity.healthComponent.health = 0;
        });
    return true;
};

var replaceWorldEntities = function(world, entities, sourceKey, resultKey){
    _.chain(entities)
        .filter(function(entity){
            return entity.identityComponent && entity.identityComponent.key == sourceKey;
        })
        .each(function(entity){
            entity.healthComponent.health = 0;
            var args = {};
            if (entity.positionComponent){
                args.x = entity.positionComponent.x;
                args.y = entity.positionComponent.y;
            }
            world.addEntity(makeEcs(resultKey, args));
        });
};

var creepSystem = function(world, entities){
    var acceptEntities = _.filter(entities, entity => entity.creepAcceptComponent);
    
    _.chain(entities)
        .filter(function(entity){
            return entity.creepComponent;
        })
        .each(function(entity){
            _.each(acceptEntities, function(acceptEntity){
                if (acceptEntity.positionComponent.comparePos(entity.positionComponent)){
                    entity.healthComponent.kill();
                    acceptEntity.creepAcceptComponent.creepAccept(acceptEntity, world, entity);
                }
            });
        });
    return true;
};

var maintenanceSystem = function(world, entities){
    var selfEntity = world.entityForKey(PARAMETERS.self);

    if (selfEntity.healthComponent.health <= 0){
        selfEntity.healthComponent.health = 1;
        world.makeEntities(selfEntity, PARAMETERS.selfDeadMenu);
    }

    _.chain(entities)
        .filter(function(entity){
            return entity.personalityComponent;
        })
        .each(function(entity){
            entity.personalityComponent.heldOpinions = _.filter(entity.personalityComponent.heldOpinions, function(opinion){
                return opinion.healthComponent.health > 0;
            });
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.experienceComponent;
        })
        .each(function(entity){
            entity.experienceComponent.updateExpansion(entity);
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.activatableComponent && entity.activatableComponent.activated;
        })
        .each(function(entity){
            entity.activatableComponent.activated = entity.activatableComponent.canActivate();
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.panelTrackComponent;
        })
        .each(function(entity){
            entity.panelTrackComponent.trackPanels(entities);
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.trainPathComponent;
        })
        .map(function(entity){
            if (!world.entityForUid(entity.trainPathComponent.trainStartId)){
                entity.healthComponent.kill();
            }
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.trainPathComponent;
        })
        .each(function(entity){
            if (_.any(entity.trainPathComponent.trainStops, function(stopId){
                var stop = world.entityForUid(stopId);
                if (stop && stop.focusPathStopComponent.isDisabled){
                    entity.healthComponent.kill();
                    return;
                }
            }));
        });
    

    var trainPathTowers = _.chain(entities)
            .filter(function(entity){
                return entity.trainPathComponent;
            })
            .map(function(entity){
                return entity.trainPathComponent.trainStops;
            })
            .flatten()
            .map(function(entityId){
                return world.entityForUid(entityId);
            })
            .filter(function(entity){
                return entity && entity.identityComponent;
            })
            .value();

    _.chain(entities)
        .filter(function(entity){
            return entity.currencyActionComponent;
        })
        .each(function(entity){
            entity.currencyActionComponent.towerIds = _.chain(trainPathTowers)
                .filter(function(tower){
                    return tower.identityComponent.key == entity.currencyActionComponent.towerKey;
                })
                .map(function(tower){
                    return tower.uid;
                })
                .value();
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.panelComponent;
        })
        .each(function(entity){
            if (entity.panelComponent.sourceId && 
                !world.entityForUid(entity.panelComponent.sourceId) &&
                entity.healthComponent){
                entity.healthComponent.kill();
            }
        });

    var pathMakingPanel = world.entityForKey(PARAMETERS.trainPathMakingPanel);

    if (pathMakingPanel.panelComponent.closed){
        _.chain(entities)
            .filter(function(entity){
                return entity.trainPathMakerComponent;
            })
            .each(function(entity){
                entity.healthComponent.kill();
            });
    }

    _.chain(entities)
        .filter(function(entity){
            return entity.panelComponent && entity.pauseComponent;
        })
        .each(function(entity){
            entity.pauseComponent.paused = !(entity.panelComponent.closed || entity.panelComponent.minimized);
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.constructionComponent && entity.constructionComponent.killWithoutPath;
        })
        .each(function(entity){
            if (!_.any(entities, function(worldEntity){
                return worldEntity.trainPathComponent &&
                    _.contains(worldEntity.trainPathComponent.trainStops, entity.uid);
            })){
                entity.healthComponent.kill();
            }
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.focusPathStopComponent;
        })
        .each(function(entity){
            entity.focusPathStopComponent.isDisabled = entity.focusPathStopComponent.checkFocusStopDisabled(entity, world);
        });

    _.chain(entities)
        .filter(function(entity){
            return entity.goalTrackComponent && entity.managedComponent;
        })
        .each(function(entity){
            if (!_.any(entities, function(worldEntity){
                return (worldEntity.entityManagerComponent || worldEntity.sourceExtensionComponent) &&
                    worldEntity.positionComponent.comparePos(entity.positionComponent);
            })){
                entity.healthComponent.kill();
            }
        });

    var traitDisablers = _.filter(entities, function(entity){ return entity.disableTraitComponent; });

    _.chain(entities)
        .filter(function(entity){
            return entity.entityManagerComponent;
        })
        .each(function(entity){
            entity.entityManagerComponent.disabled = _.any(traitDisablers, function(traitDisabler){ 
                return traitDisabler.disableTraitComponent.checkTraitDisabled(entity);
            });
        });

    _.chain(entities)
        .filter(entity => entity.tutorialTextComponent)
        .each(function(entity){
            if (entity.tutorialTextComponent.tutorialDone(entity, world)){
                entity.healthComponent.kill();
                world.makeEntities(entity, entity.tutorialTextComponent.nextEntities);
                
            }
        });

    return true;
};

var goalCheckSystem = function(world, entities){
    var player = world.player;
    _.chain(entities)
        .filter(function(entity){
            return entity.goalTrackComponent && 
                entity.goalTrackComponent.checkCompleted(entity, world);
        })
        .each(function(entity){
            entity.healthComponent.kill();

            _.each(entity.goalTrackComponent.successExecutables, function(exec){
                exec.execute(entity, world);
            });
        });
    return true;
};

var constructionSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.constructionComponent;
        })
        .each(function(entity){
            if (_.all(entity.constructionComponent.currencyRequested, function(val){ return val <= 0; })){
                entity.healthComponent.kill();
                entity.constructionComponent.construct(entity, world);
            }
        });
    return true;
};

var tickSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.tickComponent;
        })
        .each(function(entity){
            entity.tickComponent.tick(entity, world);
        });
    return true;
};

var timedDeathSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.timedDeathComponent;
        })
        .each(function(entity){
            entity.timedDeathComponent.timeToDeath -= 1;
            if (entity.timedDeathComponent.timeToDeath <= 0){
                entity.healthComponent.kill();

                _.each(entity.timedDeathComponent.deathExecs, exec => exec.execute(entity, world));

                _.each(entity.timedDeathComponent.killOnTimeOut, function(killKey){
                    _.each(world.entitiesForKey(killKey), function(toKill){
                        toKill.healthComponent.kill();
                    });
                });

                world.makeEntities(entity, entity.timedDeathComponent.makeOnTimeOut);
            }
        });
    return true;
};

var deathSystem = function(world, entities){
    world.entityDeath();
};

var focusTrackSystem = function(world, entities){
    _.chain(world.entities)
        .filter(entity => entity.focusMakingComponent)
        .each(function(entity){
            entity.focusMakingComponent.focusUsing = _.chain(entities)
                .filter(focusEntity => (focusEntity.focusCostComponent &&
                                        focusEntity.interactionComponent.canInteract(entity.identityComponent.key)))
                .map(focusEntity => focusEntity.focusCostComponent.focusCost)
                .reduce(addFunction, 0)
                .value();
            
        });
};

var extensionMonitorSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.sourceExtensionComponent;
        })
        .each(function(entity){
              var sourceEntity = world.entityForKey(entity.sourceExtensionComponent.sourceKey);
              if (!sourceEntity || sourceEntity.healthComponent.health <= 0){
              entity.healthComponent.kill();
             }
        });


    _.chain(entities)
        .filter(function(entity){
            return entity.managedComponent || entity.onTraitComponent;
        })
        .each(function(entity){
            if (!_.any(entities, function(worldEntity){
                return (worldEntity.sourceExtensionComponent || worldEntity.entityManagerComponent) &&
                    entity.positionComponent.comparePos(worldEntity.positionComponent);
            })){
                entity.healthComponent.kill();
            }
        });

    if (PARAMETERS.extensionMonitorEnabled){
        _.chain(entities)
            .filter(function(entity){
                return entity.experienceComponent && entity.mapSpawnComponent;
            })
            .each(function(entity){
                var level = entity.experienceComponent.getXpLevel(entity);
                var extEntities = world.entitiesForKey(entity.mapSpawnComponent.mapSpawnKey);
                _.chain(extEntities)
                    .sample(extEntities.length + 1 - level)
                    .each(function(extEntity){
                        extEntity.healthComponent.health = 0;

                        _.chain(world.entities)
                            .filter(function(worldEntity){
                                return worldEntity.positionComponent &&
                                    worldEntity.positionComponent.comparePos(extEntity.positionComponent) &&
                                    worldEntity.managedComponent &&
                                    worldEntity.managedComponent.managerKey == entity.identityComponent.key;
                            })
                            .each(function(worldEntity){
                                worldEntity.healthComponent.health = 0;
                            });
                    });
            });

        
        _.chain(entities)
            .filter(function(entity){
                return entity.managedComponent;
            })
            .each(function(entity){
                var managerEntity = world.entityForKey(entity.managedComponent.managerKey);
                if (!managerEntity || managerEntity.healthComponent.health <= 0){
                    entity.healthComponent.health = 0;
                }
            });
    }

    return true;
};

var lifeSentinelSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.lifeSentinelComponent;
        })
        .each(function(entity){
            if (!entity.lifeSentinelComponent.checkAlive(entity, world)){
                entity.healthComponent.kill();
            }
        });
};

var pauseMonitorSystem = function(world, entities){
    return !_.any(entities, entity =>  entity.pauseComponent && entity.pauseComponent.paused) 
        || _.any(entities, entity => entity.timeForceComponent);
};

var TickSystemWrapper = function(systems){
    this.systems = systems;
    this.ticks = -1;
    this.running = false;
};

TickSystemWrapper.prototype.runSystems = function(world, entities){
    if (!this.running){
        return;
    }
    var that = this;

    this.ticks += 1;
    this.ticks = this.ticks % 10;

    _.each(this.systems[this.ticks], function(system){
        that.running = that.running && system(world, entities);
    });
};

var sentinelSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){ return entity.sentinelComponent; })
        .each(function(entity){
            entity.sentinelComponent.lastActive = entity.sentinelComponent.active;
            entity.sentinelComponent.active = entity.sentinelComponent.sentinel(entity, world);
        });
};


var currencyOverloadSystem = function(world, entities){
    _.chain(entities)
        .filter(function(entity){
            return entity.currencyOverloadComponent;
        })
        .each(function(entity){
            var resources = world.entitiesForKey(entity.currencyOverloadComponent.resourceKey);
            var currencies = world.entitiesForKey(entity.currencyOverloadComponent.currencyKey);
            _.each(resources, function(resource){
                var overlappingCurrencies = _.filter(currencies, function(currency){
                    return currency.positionComponent.comparePos(resource.positionComponent) &&
                        !currency.pathVelocityComponent.isHeld;
                });
                if (overlappingCurrencies.length > entity.currencyOverloadComponent.currencyCount){
                    _.each(overlappingCurrencies, function(currency){ currency.healthComponent.health = 0; });
                    world.makeEntities(resource, entity.currencyOverloadComponent.overloadEntities);
                }
            });
        });
    return true;
};

var getAudio = (function(){
    var sounds = {};
    var soundVolume = 1;
    return {
        get: function(soundName){
            if (sounds[soundName] === undefined){
                sounds[soundName] = new Audio(soundName);
                sounds[soundName].load();
                sounds[soundName].volume = soundVolume;
            }
            return sounds[soundName];
        },
        changeVol: function(newVol){
            soundVolume = newVol;
            _.each(sounds, (sound) => { sound.volume = newVol; });
        }
    };
})();

var focusTickerSystem = function(world, entities){
    if (!world.lostControl()){
        _.chain(entities)
            .filter(entity => entity.focusTickerComponent)
            .each(function(entity){
                if (tickComp(entity.focusTickerComponent, entity.focusMakingComponent.focusEfficiency())){
                    world.makeEntities(entity, PARAMETERS.towerFlash);
                    
                    if (entity.identityComponent.key == PARAMETERS.self && world.gameProperties.settings.sfx){
                        var pingEffect = getAudio.get('sounds/beep.ogg');
                        pingEffect.play();
                    }

                    _.chain(entities)
                        .filter(function(focusTickEntity){
                            return focusTickEntity.focusTickResponseComponent &&
                                focusTickEntity.interactionComponent &&
                                focusTickEntity.interactionComponent.canInteract(entity.identityComponent.key);
                        })
                        .each(function(focusTickEntity){
                            focusTickEntity.focusTickResponseComponent.focusTick(focusTickEntity, world);
                        });
                }
            });
    }

    return true;
};

var aiSystem = function(world, entities){
    _.chain(world.entities)
        .filter(entity => (entity.focusMakingComponent && 
                           entity.focusMakingComponent.ai &&
                           entity.focusMakingComponent.focusUsing < entity.focusMakingComponent.focusCount &&
                           tickComp(entity.focusMakingComponent)))
        .each(function(entity){
            var aiKey = entity.identityComponent.key;

            var trainPaths = _.filter(entities, trainPath => (trainPath.trainPathComponent &&
                                                              trainPath.interactionComponent.canInteract(aiKey)));

            var focusStops = _.filter(entities, function(focusStop){
                return focusStop.focusPathStopComponent &&
                    focusStop.interactionComponent &&
                    focusStop.interactionComponent.canInteract(aiKey) &&
                    !_.any(trainPaths, trainPath => _.contains(trainPath.trainPathComponent.trainStops, focusStop.uid));
            });
            
            var splitStops = _.groupBy(focusStops, function(focusStop){
                return focusStop.focusPathStopComponent.currenciesNeededAtPoint(focusStop, world).length > 0 &&
                    focusStop.focusPathStopComponent.currenciesMadeAtPoint(focusStop, world).length == 0;
            });

            var goals = splitStops[true];

            if (goals === undefined){
                goals = [];
            }

            var makers = splitStops[false];

            if (makers === undefined){
                makers = [];
            }
            
            goals = _.filter(goals, goal => {
                var currenciesNeeded = goal.focusPathStopComponent.currenciesNeededAtPoint(goal, world);
                var stops = findStops(currenciesNeeded, makers, world);
                return stops;
            });

            goals = _.filter(goals, goal => {
                return !goal.damageComponent || goal.damageComponent.charges < goal.damageComponent.maxCharges;
            });

            var goal = false;
            if (goals.length > 0){
                goal = goals[0];
            }
            else{
                //looking for what to build
                
                var buttons = _.chain(entities)
                        .filter(function(button){
                            var chk = false;
                            if (button.buttonControlComponent){
                                var manager = world.entityForKey(button.buttonControlComponent.managerKey);
                                chk = manager.interactionComponent.canInteract(aiKey);
                                chk = chk && button.interactionComponent.canInteract(aiKey);
                                chk = chk && button.buttonControlComponent.canMakeEntity(world);
                            }
                            return chk;
                        })
                        .filter(function(button){
                            var construction = button.buttonControlComponent.makeEntity(world, [0, 0], button, aiKey);
                            var constructCurrencies = construction.focusPathStopComponent.currenciesNeededAtPoint(construction, world);
                            return findStops(constructCurrencies, makers, world);
                        })
                        .filter(function(button){
                            var entityKey = button.buttonControlComponent.entityKey;
                            var currencyRequested = allArgs[entityKey].currencyRequested;
                            return !currencyRequested || findStops(Object.keys(currencyRequested), makers, world);
                        })
                        .value();

                var buttonVals = _.groupBy(buttons, button => button.buttonControlComponent.getAiVal(button, world));

                var maxVal = _.max(Object.keys(buttonVals));
                var button = _.sample(buttonVals[maxVal]);
                
                if (button){
                    var position = _.chain(entities)
                            .filter(function(entity){
                                return (entity.identityComponent && 
                                        entity.identityComponent.key == button.buttonControlComponent.managerKey) ||
                                    (entity.sourceExtensionComponent &&
                                     entity.sourceExtensionComponent.sourceKey == button.buttonControlComponent.managerKey);
                            })
                            .map(entity => entity.positionComponent.getAsPos())
                            .filter(function(pos){
                                return button.buttonControlComponent.checkMakePos(world, pos);
                            })
                            .sample()
                            .value();
                    if (position){
                        var madeEntity = button.buttonControlComponent.makeEntity(world, position, button, aiKey);
                        world.addEntity(madeEntity);

                        goal = madeEntity;
                    }
                }     
            }       

            //setting up a train

            if (goal){
                var currenciesNeeded = goal.focusPathStopComponent.currenciesNeededAtPoint(goal, world);
                var stops = findStops(currenciesNeeded, makers, world);
                if (stops){
                    stops = [goal.uid].concat(stops);
                    stops.reverse();

                    var pathMade = world.makeEntities(goal, PARAMETERS.trainPath, {
                        trainStartId: entity.uid,
                        trainStops: stops,
                        interactionKeys: [aiKey]
                    });
                }
            };
        });
    return true;
};

var findStops = function(currenciesNeeded, makers, world){
    if (currenciesNeeded.length === 0){
        return [];
    }
    
    for (var i = 0; i < makers.length; i++){
        var maker = makers[i];
        var currenciesMade = maker.focusPathStopComponent.currenciesMadeAtPoint(maker, world);

        for (var j = 0; j < currenciesMade.length; j++){
            var currencyMade = currenciesMade[j];
            if (_.contains(currenciesNeeded, currencyMade)){
                var newNeeded = currenciesNeeded.concat(maker.focusPathStopComponent.currenciesNeededAtPoint(maker, world));
                var idx = newNeeded.indexOf(currencyMade);
                newNeeded.splice(idx, 1);
                var stops = findStops(newNeeded, makers, world);
                if (stops){
                    return [maker.uid].concat(stops);
                }
            }
        }
    }

    return false;
};

var makeStartEntities = function(){
    var startEntities = _.map(PARAMETERS.startEntities, function(key){
        return makeEcs(key);
    });
    _.each(PARAMETERS.startEntityGroups, function(group){
        startEntities = startEntities.concat(makeAllEcs(group));
    });
    return startEntities;
};

var World = function(game){
    var that = this;
    this.hexMap = new HexMap(30, PARAMETERS.size[0], PARAMETERS.size[1], 
                             function(i, j, rect){ 
                                 var cell = new HexCell(rect);
                                 return cell;
                             });
    this.entities = [];
    this.entitiesToAdd = [];
    this.game = game;
    this.keyedEntities = {};
    this.destructionMode = false;
    this.destoryPathMode = false;
    this.builtPath = false;
    this.isMakingFocusPath = false;

    this.drawHex = false;

    this.actionables = [];
    this.trackingActionables = true;
    this.hasLostControl = undefined;
    this.flags = {};
    this.gameProperties = game.gameProperties;
};

World.prototype.createStartEntities = function(){
    var that = this;
    _.each(makeStartEntities(), function(entity){
        that.addEntity(entity);
        that.entities = that.entities.concat(that.entitiesToAdd);
        that.entitiesToAdd = [];
    });
};

World.prototype.draw = function(){
    if (this.drawHex){
        this.hexMap.draw();
    }
};

World.prototype.isPosTaken = function(pos){
    return _.any(this.entities, function(entity){ 
        return entity.positionComponent && 
            entity.positionComponent.isPos(pos) &&
            entity.positionComponent.planeOfExistence == undefined &&
            entity.playerMadeComponent;
    });
};

World.prototype.isPosInBounds = function(pos){
    return (pos[0] >= 0 && pos[0] < (PARAMETERS.size[0] - 1) &&
            pos[1] >= 0 && pos[1] < PARAMETERS.size[1]);
};

World.prototype.isPosBuildable = function(hexPos){
    return this.isPosInBounds(hexPos) && !this.isPosTaken(hexPos);
};

World.prototype.isPosEmptyTrait = function(hexPos, managerKey){
    return _.any(this.entities, function(entity){
        return entity.positionComponent && 
            entity.positionComponent.isPos(hexPos) &&
            entity.managedComponent &&
            entity.managedComponent.managerKey == managerKey;
    });
};

World.prototype.lostControl = function(){
    return _.any(this.entities, function(entity){
        return entity.holdControlComponent;
    });
};

World.prototype.isPosPathBuildable = function(pos){
    return _.all(this.entities, function(entity){
        return !(entity.positionComponent &&
                 entity.positionComponent.isPos(pos) &&
                 entity.pathMoveComponent);
    });
};

World.prototype.buildJunction = function(pos){
    var pathCount = 0;
    for (var i = 0; i < this.entities.length; i++){
        var entity = this.entities[i];
        
        if (entity.junctionComponent && entity.positionComponent.isPos(pos)){
            return false;
        }
        if (entity.pathMoveComponent &&
            ((entity.pathMoveComponent.startPosition[0] == pos[0] && entity.pathMoveComponent.startPosition[1] == pos[1]) ||
             (entity.pathMoveComponent.endPosition[0] == pos[0] && entity.pathMoveComponent.endPosition[1] == pos[1]))){
            pathCount += 1;
        }
    }
    if (pathCount >= 2){
        var junction = makeEcs(PARAMETERS.junction, {x: pos[0], y: pos[1]});
        this.addEntity(junction);
    }
    return true;
};

World.prototype.toggleFocusPath = function(){
    var trainPathMaker = _.chain(this.entities)
            .filter(function(entity){
                return entity.trainPathMakerComponent;
            })
            .first()
            .value();

    if (!trainPathMaker){
        this.addEntity(makeEcs(PARAMETERS.trainPathMaker));
    }
};

World.prototype.focusPathFromEntity = function(entity){
    this.resetCurrents();
    var trainPathMaker = makeEcs(PARAMETERS.trainPathMaker);
    this.addEntity(trainPathMaker);
    trainPathMaker.trainPathMakerComponent.addTrainStop(trainPathMaker, this, entity);
};

World.prototype.handleTouch = function(pos, ui){
    var maxRender = function(entity){
        return _.reduce(entity.rendererComponents, function(memo, renderComponent){
            return max(memo, renderComponent.priority);
        }, 0);
    };
            
    var that = this;
    var prevEntity = undefined;

    if (_.any(this.entities, function(entity){ return entity.gameEndComponent; })){
        window.location = 'select.html';
    }

    _.chain(this.entities)
        .filter(function(entity){
            return entity.absolutePositionComponent && 
                !(entity.panelComponent && (entity.panelComponent.closed ||
                                            (entity.panelComponent.minimized && entity.panelComponent.ignoreWhenMinimized))) &&
                containsPos([entity.absolutePositionComponent.x, entity.absolutePositionComponent.y,
                             entity.absolutePositionComponent.size[0], entity.absolutePositionComponent.size[1]], pos);
        })
        .each(function(entity){
            if (prevEntity){
                if (maxRender(entity) >= maxRender(prevEntity)){
                    prevEntity.absolutePositionComponent.isDragging = false;
                }
                else{
                    return;
                }
            }
            
            if (entity.absolutePositionComponent.draggable){
                entity.absolutePositionComponent.isDragging = true;
                if (entity.panelComponent){
                    entity.panelComponent.startMin = entity.panelComponent.minimized;
                }
            }
            
            prevEntity = entity;
        });

    if (prevEntity){

        if (prevEntity.uiClickComponent){
            prevEntity.uiClickComponent.uiClick(pos, prevEntity, ui);
        }

        return;
    }

    
    var hexPos = this.hexMap.convertPos(pos);
    if (!hexPos){
        return;
    }

    var trainPathMaker = _.chain(this.entities)
            .filter(function(entity){
                return entity.trainPathMakerComponent;
            })
            .first()
            .value();


    if (trainPathMaker){
        var entityClicked = _.chain(this.entities)
                .filter(function(entity){
                    return entity.positionComponent &&
                        entity.positionComponent.isPos(hexPos) &&
                        entity.focusPathStopComponent;
                })
                .first()
                .value();

        if (entityClicked){
            trainPathMaker.trainPathMakerComponent.addTrainStop(trainPathMaker, this, entityClicked);
        }
    }
    else if (this.destructionMode){
        _.chain(this.entities)
            .filter(function(entity){
                return entity.positionComponent && 
                    entity.positionComponent.isPos(hexPos) &&
                    entity.healthComponent &&
                    entity.playerMadeComponent &&
                    entity.playerMadeComponent.playerKillable &&
                    entity.interactionComponent &&
                    entity.interactionComponent.canInteractSelf() &&
                    !entity.pathMoveComponent;
            })
            .each(function(entity){
                entity.healthComponent.kill();
            });
    }
    else if (this.destroyPathMode){
        _.chain(this.entities)
            .filter(function(entity){
                return entity.pathMoveComponent &&
                    (comparePos(entity.pathMoveComponent.startPosition, hexPos) ||
                     comparePos(entity.pathMoveComponent.endPosition, hexPos)) &&
                    entity.healthComponent &&
                    entity.playerMadeComponent &&
                    entity.playerMadeComponent.playerKillable;
            })
            .each(function(entity){
                entity.healthComponent.kill();
            });
    }
    else if (this.currentMakingEntity){
        if (!this.currentMakingEntity.buttonControlComponent.checkMakePos(this, hexPos)){
            return;
        }
        var entity = this.currentMakingEntity.buttonControlComponent.makeEntity(this, hexPos, this.currentMakingEntity, PARAMETERS.self);
        this.addEntity(entity);
        this.resetCurrents();

        if (this.gameProperties.settings.sfx){
            var sound = getAudio.get('sounds/beep.ogg');
            sound.play();
        }
    }
    else if (this.currentPath){
        this.currentPathStart = hexPos;
        this.makeExtraPaths(hexPos);
        this.builtPath = true;
    }
    else if (this.currentMoving && !this.currentMoving.positionComponent.isPos(hexPos)){
        if (this.isPosBuildable(hexPos) && this.currentMoving.positionComponent.canBuildAtPos(this.currentMoving, this, hexPos)){
            this.currentMoving.positionComponent.x = hexPos[0];
            this.currentMoving.positionComponent.y = hexPos[1];
            this.currentMoving.positionComponent.defaultSource = undefined;
            this.currentMoving = null;
        }
    }
    else{
        var entities = _.filter(this.entities, function(entity){
            return entity.positionComponent && entity.positionComponent.isPos(hexPos) &&
                entity.identityComponent && entity.identityComponent.touchable;
        });
        
        var toClick = undefined;

        _.each(entities, function(entity){
            if (entity.clickComponent){
                if (toClick){
                    if (maxRender(entity) >= maxRender(toClick)){
                        toClick = entity;
                    }
                }
                else{
                    toClick = entity;
                }
            }
        });

        if (toClick){
            toClick.clickComponent.onClick(toClick, this, ui);
        }

        entities = _.filter(entities, function(entity){
            return !entity.healthComponent ||
                (entity.healthComponent.health > 0);
        });

        ui.setActiveControl(entities, this);
    }
};

World.prototype.makeExtraPaths = function(startPos){
    var connectableEntities = _.filter(this.entities, entity => (entity.focusPathStopComponent ||
                                                                 entity.entityManagerComponent ||
                                                                 entity.sourceExtensionComponent));
    var paths = this.getAllPaths();


    _.each(this.hexMap.neighbors(startPos[0], startPos[1]), (chkPos) => {
        var toMakePath = _.any(connectableEntities, entity => entity.positionComponent.isPos(chkPos)) ||
                _.any(paths, entity => (comparePos(chkPos, entity.pathMoveComponent.startPosition) ||
                                        comparePos(chkPos, entity.pathMoveComponent.endPosition)));
        
        if (toMakePath){
            this.addEntity(makeEcs(PARAMETERS.path, {
                start: startPos,
                end: chkPos
            }));
        }
    });
};

World.prototype.handleMove = function(pos, lastPos){
    _.chain(this.entities)
        .filter(function(entity){
            return entity.absolutePositionComponent &&
                entity.absolutePositionComponent.isDragging;
        })
        .each(function(entity){
            entity.absolutePositionComponent.x += pos[0] - lastPos[0];
            entity.absolutePositionComponent.y += pos[1] - lastPos[1];
            if (pos[0] != lastPos[0] || pos[1] != lastPos[1]){
                if (entity.panelComponent){
                    entity.panelComponent.minimized = entity.panelComponent.startMin;
                }
            }
        });

    if (this.currentPath && this.currentPathStart){
        var hexPos = this.hexMap.convertPos(pos);
        
        if (!hexPos || !this.isPosInBounds(hexPos)){
            return;
        }

        this.builtPath = true;

        var neighbors = this.hexMap.neighbors(this.currentPathStart[0], this.currentPathStart[1]);
        if (_.any(neighbors, function(neighbor){ return _.isEqual(neighbor, hexPos); })){
            var path = makeEcs(PARAMETERS.path, {start: this.currentPathStart, end: hexPos});

            this.makeExtraPaths(this.currentPathStart);
            this.makeExtraPaths(hexPos);

            
            this.addEntity(path);
            this.currentPathStart = hexPos;
        }
    }
};

World.prototype.handleMouseUp = function(pos){
    _.chain(this.entities)
        .filter(function(entity){
            return entity.absolutePositionComponent;
        })
        .each(function(entity){
            entity.absolutePositionComponent.isDragging = false;
        });
    this.currentPathStart = null;
    if (this.builtPath){
        this.currentPath = null;
        this.builtPath = false;
    }
};

World.prototype.handleKeyPress = function(key, ui){
    _.each(this.entities, function(entity){
        if (entity.keyPressComponent){
            entity.keyPressComponent.handleKeyPress(key, entity, ui);
        }
    });
};

World.prototype.entitiesForName = function(name){
    return _.filter(this.entities, function(entity){
        return entity.identityComponent && entity.identityComponent.name == name;
    });
};

World.prototype.entitiesForKey = function(key){
    return _.filter(this.entities, function(entity){
        return entity.identityComponent && entity.identityComponent.key == key;
    });
};

World.prototype.entityForKey = function(key){
    if (this.keyedEntities[key]){
        return this.keyedEntities[key];
    }
    var entity = this.entitiesForKey(key)[0];
    this.keyedEntities[key] = entity;
    return entity;
};

World.prototype.entityForKeyExists = function(key){
    return _.any(this.entities, function(entity){
        return entity.identityComponent && entity.identityComponent.key == key;
    });
};

World.prototype.entityForUid = function(uid){
    for (var i = 0; i < this.entities.length; i++){
        if (this.entities[i].uid == uid){
            return this.entities[i];
        }
    }
    return undefined;
};

World.prototype.currenciesAtEntity = function(chkEntity, currencyRequested){
    return [];
    var pos = [chkEntity.positionComponent.x, chkEntity.positionComponent.y];
    return _.filter(this.entities, function(entity){
        if (!entity.identityComponent){
            return false;
        }

        var key = entity.identityComponent.key;

        if ((currencyRequested[key] && currencyRequested[key] > 0) || 
            _.any(entity.identityComponent.tags, function(tag){
                return currencyRequested[tag] && currencyRequested[tag] > 0;
            })){
            return entity.pathVelocityComponent &&
                entity.pathVelocityComponent.chosenPath === false &&
                entity.positionComponent.isPos(pos);
        }

        return false;
    });
};

World.prototype.resetCurrents = function(){
    this.currentMakingEntity = null;
    this.currentPath = null;
    this.currentPathStart = null;
    this.currentMoving = null;
    this.destructionMode = false;
    this.destroyPathMode = false;

    _.each(this.entities, function(entity){
        if (entity.trainPathMakerComponent){
            entity.healthComponent.health = 0;
        }
    });
};

World.prototype.trackActionables = function(){
    _.each(this.actionables, function(actionable){
        if (actionable.menuModifierComponent || actionable.randomMenuComponent){
            actionable.healthComponent.kill();
        }
    });

    this.trackingActionables = true;
    this.actionables = [];
};

World.prototype.getActionables = function(){
    this.trackingActionables = false;
    return this.actionables;
};

World.prototype.addEntity = function(entity){
    if (entity.uniqueComponent){
        var orig = this.entityForKey(entity.identityComponent.key);
        if (orig && (!orig.healthComponent || orig.healthComponent.health > 0)){
            if (!orig.uniqueComponent.uniqueDuplicateEvent(entity, orig, this)){
                return;
            }
        }
        else{
            this.keyedEntities[entity.identityComponent.key] = entity;
        }
    }
    
    this.entityCreateExec(entity);

    if (entity.healthComponent && entity.healthComponent.health <= 0){
        return;
    }

    this.entitiesToAdd.push(entity);

    if (entity.isaacEntityComponent){
        entity.isaacEntityComponent.startLifecycle(entity, this);
    }

    if (this.trackingActionables && (entity.uiActionComponent || entity.menuModifierComponent || entity.randomMenuComponent)){
        this.actionables.push(entity);
    }
};

World.prototype.entityCreateExec = function(entity){
    if (!entity.createExecutables){
        return;
    }
    for (var i = 0; i < entity.createExecutables.length; i++){
        var exec = entity.createExecutables[i];
        if (exec){
            exec.execute(entity, this);
        }
        if (entity.healthComponent && entity.healthComponent.health <= 0){
            return;
        }
    }
};

World.prototype.addCost = function(name, cost){
    var key = name;
    var existingCostEntity = this.entitiesForKey(key)[0];
    if (existingCostEntity){
        addDict(existingCostEntity.currencyStoreComponent.currencies, cost);
    }
    else{
        this.addEntity(makeEcs(PARAMETERS.currencyStore, {
            currencies: _.clone(cost),
            name: key,
            key: key
        }));
    }
};

World.prototype.makeClickText = function(name, x, y){
    var clickText = makeEcs(PARAMETERS.clickText, {
        name: name,
        x: x,
        y: y
    });
    this.addEntity(clickText);
};

World.prototype.getPositionedEntities = function(planeOfExistence){
    return _.filter(this.entities.concat(this.entitiesToAdd), function(entity){
        return entity.positionComponent && 
            entity.positionComponent.x !== undefined &&
            entity.positionComponent.planeOfExistence == planeOfExistence &&
            entity.playerMadeComponent;
    });
};

World.prototype.randomOpenPosition = function(planeOfExistence, startPos, minSelfDist){
    minSelfDist = minSelfDist ? minSelfDist : 0;

    var that = this;
    var pos = false;
    var entitiesToCheck = this.getPositionedEntities(planeOfExistence);

    var positions = _.map(entitiesToCheck, function(entity){ return [entity.positionComponent.x, entity.positionComponent.y]; });
    

    var immediateNeighbors = _.chain(positions)
            .map(function(position){
                return that.hexMap.neighbors(position[0], position[1]);
            })
            .flatten(true)
            .value();

    var selfEntity = this.entityForKey(PARAMETERS.self);
    var selfPos = [selfEntity.positionComponent.x, selfEntity.positionComponent.y];

    var dPos = function(pos1, pos2){
        var dx = pos1[0] - pos2[0];
        var dy = pos1[1] - pos2[1];

        dx = (dx > 0) ? dx : (dx * -1);
        dy = (dy > 0) ? dy : (dy * -1);

        return max(dx, dy);
    };

    var firstPass = _.chain(positions)
            .map(function(position){
                var neighbors = that.hexMap.neighbors(position[0], position[1]);
                return _.chain(neighbors)
                    .map(function(neighbor){
                        return that.hexMap.neighbors(neighbor[0], neighbor[1]);
                    })
                    .flatten(true)
                    .value();
            })
            .flatten(true)
            .filter(function(position){
                if (position[0] < 1 || 
                    position[0] >= (PARAMETERS.size[0] - 1) ||
                    position[1] < 1 ||
                    position[1] >= (PARAMETERS.size[1] - 1)){
                    return false;
                }
                
                return _.all(immediateNeighbors, function(pos){ return !comparePos(position, pos); }) && 
                    !_.any(entitiesToCheck, function(chkEntity){ return chkEntity.positionComponent.isPos(position); });
            })
            .value();

    var narrowPass = _.filter(firstPass, function(position){
        if (startPos && dPos(startPos, position) > 3){
            return false;
        }

        if (dPos(selfPos, position) < minSelfDist){
            return false;
        }

        return true;
    });

    var position;

    if (narrowPass.length > 0){
        var weights = _.map(narrowPass, function(position){
            return 1 / dPos(selfPos, position);
        });

        var chk = Math.random() * _.reduce(weights, addFunction, 0);

        for (var i = 0; i < narrowPass.length; i++){
            chk -= weights[i];
            if (chk <= 0){
                position = narrowPass[i];
                break;
            }
        }
    }
    else {
        position = _.sample(firstPass);
    }
    
    return position;
};

World.prototype.tick = function(){
    for (var key in this.keyedEntities){
        if (!_.contains(this.entities, this.keyedEntities[key])){
            this.keyedEntities[key] = undefined;
        }
    }
};

World.prototype.makeEntities = function(entity, entitiesToMake, specificArgs){
    var that = this;
    var args = {
        source: entity
    };

    if (entity){
        args.sourceId = entity.uid;
    }

    if (entity.positionComponent){
        args.x = entity.positionComponent.x;
        args.y = entity.positionComponent.y;
    }

    if (entity.identityComponent && entity.identityComponent.key){
        args.sourceKey = entity.identityComponent.key;
        args.sourceName = entity.identityComponent.name;
    }

    if (entity.personalityComponent){
        args.personHeld = entity.identityComponent.key;
    }

    if (entity.personHeldComponent){
        args.personHeld = entity.personHeldComponent.personHeld;

        if (!entity.positionComponent){
            var person = this.entityForKey(entity.personHeldComponent.personHeld);
            if (person){
                args.x = person.positionComponent.x;
                args.y = person.positionComponent.y;
            }
        }
    }

    if (entity.managedComponent){
        args.sourceTrait = entity.managedComponent.managerKey;
    }

    if (entity.entityManagerComponent){
        args.sourceTrait = entity.identityComponent.key;
    }

    if (entity.opinionComponent){
        args.opinionName = entity.identityComponent.name;
    }

    if (entity.interactionComponent){
        args.interactionKeys = entity.interactionComponent.interactionKeys;
    }

    _.each(specificArgs, function(val, key){
        args[key] = val;
    });

    var entitiesMade = [];

    var mkEntity = function(entitiesToMake, args){
        if (!entitiesToMake){
            return;
        }
        if (_.isString(entitiesToMake)){
            var newArgs = _.clone(args);
            var baseArgs = _.clone(args);
            var entityMade = makeEcs(entitiesToMake, newArgs);

            if (entityMade.replaceMarker){
                entitiesToMake = entityMade.replaceMarker.getReplaceKeys(entityMade, that, baseArgs);
                mkEntity(entitiesToMake, baseArgs);
            }
            else{
                that.addEntity(entityMade);
                entitiesMade.push(entityMade);
            }
        }
        else if (_.isArray(entitiesToMake)){
            _.each(entitiesToMake, entity => mkEntity(entity, args));
        }
        else{
            _.each(entitiesToMake, function(val, key){
                if (Math.random() * 100 < val){
                    mkEntity(key, args);
                }
            });
        }
    };
    
    mkEntity(entitiesToMake, args);
    
    return entitiesMade;
};

World.prototype.togglePathBuilding = function(){
    if (this.currentPath){
        this.resetCurrents();
    }
    else{
        this.resetCurrents();
        this.currentPath = true;
    }
};

World.prototype.openSettingsPanel = function(){
    var menu = this.entityForKey(PARAMETERS.settingsPanel);
    if (!menu){
        menu = makeEcs(PARAMETERS.settingsPanel);
        this.addEntity(menu);
    }
    
    menu.panelComponent.expandPanel();
};

World.prototype.findStartPos = function(startEntityKey){
    var startEntity = this.entityForKey(startEntityKey);

    if (!startEntity){
        startEntity = _.chain(this.entitiesToAdd)
            .filter(function(entity){
                return entity.key == startEntityKey;
            })
            .first()
            .value();
    }

    if (!startEntity || !startEntity.positionComponent){
        console.log('could not find start key ' + startEntityKey);
        return undefined;
    }

    return [startEntity.positionComponent.x, startEntity.positionComponent.y];
};

World.prototype.killEntitiesForKey = function(key){
    _.chain(this.entities)
        .filter(function(entity){
            return entity.identityComponent &&
                entity.identityComponent.key == key;
        })
        .each(function(entity){
            entity.healthComponent.health = 0;
        });
};

World.prototype.ensurePath = function(start, stop, paths){
    if (comparePos(start, stop)){
        return [];
    }

    if (paths === undefined){
        paths = this.getAllPaths();
    }

    var i, j;
    
    var endPoint;
    var path = determinePath(start, stop, paths);
    if (path === undefined){

        var offsetDicts = [
            [
                [0, 0],
                [-1, 0],
                [-1, 1],
                [0, 1],
                [1, 0],
                [0, -1],
                [-1, -1]
            ],
            [
                [0, 0],
                [-1, 0],
                [0, 1],
                [1, 1],
                [1, 0],
                [1, -1],
                [0, -1]
            ]
        ];

        var dx = stop[0] - start[0];
        var dy = stop[1] - start[1];

        offsetDicts = _.map(offsetDicts, function(offsetDict){
            return _.filter(offsetDict, function(offset){
                return ((offset[0] * dx) > 0) || ((offset[1] * dy) > 0);
            });
        });

        var hitNeighbors = [];
        
        var getNeighbors = (i, j) => {
            var offsets = offsetDicts[Math.abs(j % 2)];
            return _.chain(offsets)
                .map(function(offset){
                    return [i + offset[0], j + offset[1]];
                })
                .filter((newPos) =>{
                    return this.isPosInBounds(newPos);
                })
                .filter((newPos) => {
                    if (_.any(hitNeighbors, hitNeighbor => posEqual(hitNeighbor, newPos))){
                        return false;
                    }
                    hitNeighbors.push(newPos);
                    return true;
                })
                .value();
        };

        var steps = _.map(getNeighbors(start[0], start[1]), function(neighbor){ return [neighbor]; });
        
        var keepExpanding = true;
        var stepsToTake;

        while (keepExpanding){
            var minDist = 10000;
            var stepIndex = -1;

            for (i = 0; i < steps.length; i++){
                var step = steps[i];
                endPoint = _.last(step);

                var dist = Math.abs(endPoint[0] - stop[0]) + Math.abs(endPoint[1] - stop[1]);
                
                if (dist == 0){
                    keepExpanding = false;
                    stepsToTake = [start].concat(step);
                    break;
                }

                if (endPoint[1] % 2 == 0){
                    dist -= 0.5;
                }

                if (dist < minDist){
                    minDist = dist;
                    stepIndex = i;
                }
            }

            if (keepExpanding){
                var stepToExpand = steps[stepIndex];
                steps.splice(stepIndex, 1);

                endPoint = _.last(stepToExpand);
                if (endPoint === undefined){
                    return undefined;
                }

                var neighbors = getNeighbors(endPoint[0], endPoint[1]);
                for (j = 0; j < neighbors.length; j++){
                    steps.push(stepToExpand.concat([neighbors[j]]));
                }
            }
        }
        
        for (i = 0; i < stepsToTake.length - 1; i++){
            var newPath = makeEcs(PARAMETERS.path, {start: stepsToTake[i], end: stepsToTake[i + 1]});
            this.addEntity(newPath);
            this.makeExtraPaths(newPath.pathMoveComponent.startPosition);
            this.makeExtraPaths(newPath.pathMoveComponent.endPosition);
            paths.push(newPath);
        }
        
        path = determinePath(start, stop, paths);
    }
    return path;
};

World.prototype.getAllPaths = function(){
    return _.filter(this.entities, function(entity){
        return entity.pathMoveComponent;
    });
};

World.prototype.connectEntities = function(start, end){
    if (start && end){
        start = [start.positionComponent.x, start.positionComponent.y];
        end = [end.positionComponent.x, end.positionComponent.y];
        var paths = [];
        this.ensurePath(start, end, paths);
    }
};

World.prototype.lostControl = function(){
    if (this.hasLostControl === undefined){
        this.hasLostControl = _.any(this.entities, function(worldEntity){
            return worldEntity.holdControlComponent &&
                worldEntity.holdControlComponent.holdControl;
        });
    }
    
    return this.hasLostControl;
};

World.prototype.afterLoad = function(){
    this.actionables = [];
    this.trackingActionables = false;

    _.each(PARAMETERS.ensureExistence, (key) => {
        if (!this.entityForKey(key)){
            var entity = makeEcs(key);
            this.entities.push(entity);
        }
    });
};

World.prototype.getGameStage = function(){
    return _.chain(this.entities)
        .filter(function(worldEntity){
            return worldEntity.stageChangeComponent;
        })
        .first()
        .value();
};

World.prototype.transformTrait = function(sourceTrait, newTrait, retainHealth, dontKill){
    if (!dontKill){
        _.chain(this.entities)
            .filter(function(entity){
                return entity.managedComponent &&
                    entity.managedComponent.killOnTransform &&
                    entity.managedComponent.managerKey == sourceTrait;
            })
            .each(function(entity){
                entity.healthComponent.kill();
            });
    }

    if (!sourceTrait.healthComponent){
        sourceTrait = this.entityForKey(sourceTrait);
    }
    

    if (!sourceTrait){
        return undefined;
    }

    var sourceHealth = sourceTrait.healthComponent.health;

    sourceTrait.healthComponent.kill();

    this.entityDeath();

    var args = {
        randomTraitPosition: false,
        skipBuiltSourceCreate: true,
        x: sourceTrait.positionComponent.x,
        y: sourceTrait.positionComponent.y
    };

    if (sourceTrait.memoryComponent){
        args.sourceKey = sourceTrait.memoryComponent.sourceKey;
        args.source2Key = sourceTrait.memoryComponent.source2Key;
    }
    else if (sourceTrait.fadingMemoryComponent){
        args.sourceKey = sourceTrait.fadingMemoryComponent.sourceKey;
        args.source2Key = sourceTrait.fadingMemoryComponent.source2Key;
    }

    newTrait = makeEcs(newTrait, args);
    

    if (retainHealth){
        newTrait.healthComponent.health = sourceHealth;
    }

    if (sourceTrait){
        var sourceTraitExt = sourceTrait.mapSpawnComponent.mapSpawnKey;
        var newTraitExt = newTrait.mapSpawnComponent.mapSpawnKey;

        _.chain(this.entities)
            .filter(function(worldEntity){
                return worldEntity.sourceExtensionComponent &&
                    worldEntity.sourceExtensionComponent.sourceKey == sourceTrait.identityComponent.key;
            })
            .each((sourceExt) => {
                sourceExt.healthComponent.kill();
                var newExt = makeEcs(newTraitExt, {
                    x: sourceExt.positionComponent.x,
                    y: sourceExt.positionComponent.y,
                    source: newTrait
                });
                this.addEntity(newExt);
            });
    }

    this.addEntity(newTrait);
    
    this.entityDeath();
    
    return newTrait;
};

World.prototype.entityDeath = function(){
    var killedEntities = [];

    this.entities = _.filter(this.entities.concat(this.entitiesToAdd), (entity) => {
        if (entity.healthComponent === undefined || entity.healthComponent.health > 0){
            return true;
        }
        else{
            killedEntities.push(entity);
            
            if (entity.isaacEntityComponent){
                entity.isaacEntityComponent.endLifecycle(this);
            }

            return false;
        }
    });
    this.entitiesToAdd = [];

    _.each(killedEntities, (entity) => {
        _.each(entity.killExecutables, (exec) => {
            exec.execute(entity, this);
        });
    });
    
    this.hasLostControl = undefined;
};

World.prototype.isEntityPaused = function(){
    return !pauseMonitorSystem(this, this.entities);
};

World.prototype.isEntityTimeLocked = function(){
    return _.any(this.entities, (entity) => {
        return (entity.pauseComponent && entity.pauseComponent.paused) ||
            entity.timeForceComponent; 
    });
};

World.prototype.getBlockingEntities = function(){
    return _.filter(this.entities, function(worldEntity){
        return worldEntity.positionComponent &&
            (!worldEntity.healthComponent || worldEntity.healthComponent.health > 0) &&
            (worldEntity.playerMadeComponent && !worldEntity.playerMadeComponent.canExpandInto);
    });
};

World.prototype.neighbors = function(pos){
    return this.hexMap.neighbors(pos[0], pos[1]);
};

var BasicButton = function(rect, onTouchFunction){
    this.rect = rect;
    this.onTouchFunction = onTouchFunction;
};

BasicButton.prototype.draw = function(){
    ctx.fillStyle = '#00ff00';
    ctx.fillRect(this.rect[0], this.rect[1], this.rect[2], this.rect[3]);
};

var Ui = function(world, game){
    this.world = world;
    this.game = game;
    this.setupUi(game, world);
    this.lines = {};
    this.currencyLines = {};
};

Ui.prototype.draw = function(){
};

var makeButton = function(name, cost){
    return '<button>' + name + ' (' + _.reduce(cost, function(memo, val, key){
        var st = key + ':' + (-1 * val);
        return (memo == '') ? st : memo + ', ' + st;
    }, '') + ')' + '</button>';
};

Ui.prototype.setupUi = function(game){
    var that = this;

    var ticksPerTick = PARAMETERS.ticksPerTick;

    this.gameProperties = game.gameProperties;
 
    this.pause = function(){
        if (game.world.isEntityTimeLocked()){
            return;
        }
        game.tickSystemWrapper.running = !game.tickSystemWrapper.running;
        if (!_.contains(ticksPerTick, game.ticksPerTick)){
            game.ticksPerTick = ticksPerTick[0];
        }
    };
    this.toggle = function(){
        if (game.world.isEntityTimeLocked()){
            return;
        }
        var idx = ticksPerTick.indexOf(game.ticksPerTick);
        idx = (idx + 1) % ticksPerTick.length;
        game.ticksPerTick = ticksPerTick[idx];
    };
    this.setSpeed = function(newSpeed){
        if (game.world.isEntityTimeLocked()){
            return;
        }
        game.tickSystemWrapper.running = true;
        game.ticksPerTick = newSpeed;
    };
};

Ui.prototype.getPanel = function(key){
    var infoPanel = this.world.entityForKey(key);
    
    if (!infoPanel){
        var ecsKey = PARAMETERS.infoPanel;

        infoPanel = makeEcs(ecsKey, {
            key: key,
            name: key
        });
        this.world.addEntity(infoPanel);
    }
    
    return infoPanel;
};

Ui.prototype.addFoldLine = function(key, line, foldout){
    var infoPanel = this.getPanel(key);
    var foldPanel = _.chain(infoPanel.panelComponent.lines)
            .filter(function(line){
                return line.foldout === foldout;
            })
            .first()
            .value();

    if (!foldPanel){
        foldPanel = makeEcs(PARAMETERS.infoFoldout, {
            foldout: foldout,
            foldLines: line ? [line] : []
        });
        infoPanel.panelComponent.lines.push(foldPanel);
    }
    else{
        if (line !== undefined){
            foldPanel.foldLines.push(line);
        }
    }
    return foldPanel;
};

Ui.prototype.addCurrencyLine = function(key, val){
    var dct = (this.currencyLines[key] === undefined) ? {} : this.currencyLines[key];
    
    _.each(val, function(v, k){
        if (dct[k] === undefined){
            dct[k] = 0;
        }
        dct[k] += v;
    });
    
    this.currencyLines[key] = dct;
};

Ui.prototype.setTitle = function(key, title){
    var infoPanel = this.getPanel(key);
    infoPanel.identityComponent.name = title;
};

var minDictString = function(dict){
    return _.reduce(dict, function(memo, val, key){
        var st = key + ':' + ((typeof val == 'object') ? '(' + minDictString(val) + ')' : val);
        return (memo === undefined) ? st : (memo + '|' + st);
    }, undefined);
};

var mangleIdentityWithTrait = function(key, trait){
    return key + '_' + trait;
};

Ui.prototype.update = function(){};

Ui.prototype.setActiveControl = function(entities, world){
    var that = this;
    var entity;
    
    if (entities.length === 1){
        entity = entities[0];
    }
    else{
        entities = _.filter(entities, function(entity){
            return entity.positionComponent.defaultSource;
        });
        
        if (entities.length === 0){
            return;
        }

        if (world.currentMoving){
            var index = entities.indexOf(world.currentMoving);
            index = (index == (entities.length - 1)) ? 0 : (index + 1);
            entity = entities[index];
        }
        else{
            entity = entities[0];
        }

        world.currentMoving = entity;
    }
};

Ui.prototype.showPanel = function(infoPanel){
    infoPanel.panelComponent.closed = false;
    infoPanel.panelComponent.minimized = false;

    this.world.addEntity(makeEcs('notification', {
        notificationKey: PARAMETERS.infoPanel,
        notificationArgs: {
            key: infoPanel.identityComponent.key
        }
    }));

    var otherInfoPanels = _.filter(this.world.entities, function(entity){
        return entity.panelComponent && entity !== infoPanel;
    });
    
    for (var i = 0; i < 5; i++){
        var panel = _.chain(otherInfoPanels)
                .filter(function(panel){
                    return containsPos([panel.absolutePositionComponent.x - 2, panel.absolutePositionComponent.y - 2,
                                        panel.absolutePositionComponent.size[0] + 2, panel.absolutePositionComponent.size[1] + 2],
                                       [infoPanel.absolutePositionComponent.x, infoPanel.absolutePositionComponent.y]);
                })
                .first()
                .value();
        if (panel){
            infoPanel.absolutePositionComponent.y = panel.absolutePositionComponent.y + panel.absolutePositionComponent.size[1] + 10;
        }
        else{
            return;
        }
    }
};

Ui.prototype.openPanelAtPos = function(panelKey, positionComponent){
    var panel = this.getPanel(panelKey);
    var rect = this.world.hexMap.cellForPosComp(positionComponent).rect;

    this.world.addEntity(makeEcs('notification', {
        notificationKey: PARAMETERS.infoPanel,
        notificationArgs: {
            key: panelKey
        }
    }));

    panel.absolutePositionComponent.x = rect[0] + rect[2];
    panel.absolutePositionComponent.y = rect[1] + rect[3];

    if (panel.panelComponent.closed || panel.panelComponent.minimized){
        panel.panelComponent.closed = false;
        panel.panelComponent.minimized = false;
        return true;
    }
    return false;
};

Ui.prototype.closePanel = function(panelKey){
    var panel = this.getPanel(panelKey);
    panel.panelComponent.closed = true;
    panel.panelComponent.minimized = true;
};

Ui.prototype.minimizeAllPanels = function(){
    _.chain(this.world.entities)
        .filter(function(entity){ return entity.panelTrackComponent; })
        .each((entity) => { entity.panelTrackComponent.closeAllPanels(this); });
};

Ui.prototype.expandPanel = function(panelKey){
    var menu = this.world.entityForKey(panelKey);
    if (!menu){
        menu = makeEcs(panelKey);
        this.world.addEntity(menu);
    }
    
    menu.panelComponent.expandPanel();
};

var Game = function(gameProperties){
    this.gameProperties = gameProperties;
    this.lastPos = [0, 0];
};

Game.prototype.initialize = function(){
    var that = this;
    this.world = new World(this);
    this.ui = new Ui(this.world, this);
    this.world.createStartEntities();
    this.background = new RectBackground();

    bindHandler.bindFunction(this.getTouchFunction());
    bindHandler.bindFunction(this.getMoveFunction(), 'move');
    bindHandler.bindFunction(this.getMouseUpFunction(), 'up');
    document.addEventListener('keydown', function(e){
        that.handleKeyPress(e);
    });

    this.timeToNextTick = 0;
    this.lastInterval = 0;
    this.ticksPerTick = PARAMETERS.ticksPerTick[0];
    this.tickSystemWrapper = new TickSystemWrapper({
        0: [daySystem, trainPathSystem, trainMoveSystem, trainInteractSystem, junctionSystem, creepSystem],
        1: [],
        2: [damageSystem],
        3: [focusTickerSystem],
        4: [mapSpawnSystem],
        5: [currencyOverloadSystem],
        6: [timedDeathSystem],
        7: [goalCheckSystem],
        8: [aiSystem],
        9: [tickSystem, pauseMonitorSystem]
    });
    this.systems = [
        notificationSystem,
        constructionSystem,
        maintenanceSystem,
        sentinelSystem,
        focusTrackSystem,
        extensionMonitorSystem,
        lifeSentinelSystem,
        deathSystem
    ];
    adminConsole.setGame(this);

    if (this.gameProperties.saveKey){
        console.log(this.gameProperties.saveKey);
        adminConsole.load(this.gameProperties.saveKey);
    }
};

Game.prototype.getTouchFunction = function(){
    var that = this;
    return function(e){
        e.preventDefault();
        if (e.button == 2){
            that.world.resetCurrents();
        }
        var pos = getPos(e);

        that.lastPos = pos;
        that.world.handleTouch(pos, that.ui);
        return false;
    };
};

Game.prototype.getMoveFunction = function(){
    var that = this;
    return function(e){
        e.preventDefault();
        var pos = getPos(e);
        that.world.handleMove(pos, that.lastPos);
        that.lastPos = pos;
        return false;
    };
};

Game.prototype.getMouseUpFunction = function(){
    var that = this;
    return function(e){
        e.preventDefault();
        var pos = getPos(e);
        that.world.handleMouseUp(pos);
        return false;
    };
};

Game.prototype.tick = function(){
    var that = this;
    var entities = this.world.entities;
    this.world.tick();
    this.tickSystemWrapper.runSystems(this.world, entities);
    _.each(this.systems, function(system){
        system(that.world, entities);
    });
    this.ui.update();
};

Game.prototype.update = function(interval){
    if (STEAM && debugSt){
        return true;
    }
    this.background.update(interval);

    this.timeToNextTick -= interval;
    this.lastInterval = interval;
    
    if (this.timeToNextTick < 0){
        this.timeToNextTick += 1 / (this.ticksPerTick * TICKS_PER_SECOND);
        if (STEAM){
            try{
                this.tick();
            }
            catch (e){
                debugSt = [e.message].concat(e.stack.split('\n'));
            }
        }
        else{
            this.tick();
        }
        this.timeToNextTick = max(this.timeToNextTick, 0);
    }

    return true;
};

Game.prototype.draw = function(){
    this.background.draw();

    this.world.draw();
    rendererSystem(this.world, this.world.entities, this.ui, this.timeToNextTick, this.lastInterval);
    this.ui.draw();
    
    ctx.fillStyle = 'rgba(0, 0, 48, 0.15)';
    ctx.fillRect(0, 0, width, height);

    if (debugSt){
        if (STEAM){
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, width, height);
        }
        ctx.fillStyle = '#ffffff';
        if (_.isString(debugSt)){
            ctx.fillText(debugSt, 100, 100);
        }
        else{
            _.each(debugSt, (st, i) => ctx.fillText(st, 100, i * 50 + 50));
        }
    }
};

Game.prototype.saveData = function(){
    return {
        ticks: this.tickSystemWrapper.ticks
    };
};

Game.prototype.loadData = function(data){
    this.tickSystemWrapper.ticks = data.ticks;
    _.each(this.systems, (system) => {
        system(this.world, this.world.entities);
    });
};

Game.prototype.handleKeyPress = function(event){
    var key = event.keyCode;
    this.world.handleKeyPress(key, this.ui);
};

var adminConsole = (function(){
    var game;

    var serializeComp = function(comp){
        if (!_.isObject(comp)){
            return comp;
        }

        if (_.isArray(comp)){
            return _.map(comp, serializeComp);
        }

        if (_.isFunction(comp)){
            return undefined;
        }

        if (comp.serializeComponent && comp.serializeComponent.serialize){
            comp.serializeComponent.serialize(comp);
        }

        var serialized = {};
        _.each(comp, function(val, key){
            if (!_.isObject(val) || _.isArray(val)){
                serialized[key] = serializeComp(val);
            }
            else if (!_.isFunction(val) && 
                     val !== undefined &&
                     val.identityComponent === undefined){
                serialized[key] = serializeComp(val);
            }
        });

        if (comp.serializeComponent && comp.serializeComponent.postSerialize){
            serialized = comp.serializeComponent.postSerialize(comp, serialized);
        }

        return serialized;
    };

    var compareParts = function(newPart, basePart){
        if (newPart == basePart){
            return undefined;
        }

        if (!_.isObject(newPart)){
            return newPart;
        }

        if (basePart === undefined){
            return newPart;
        }

        if (newPart.serializeComponent && newPart.serializeComponent.serialize){
            newPart.serializeComponent.serialize(newPart);
            basePart.serializeComponent.serialize(basePart);
        }

        var diff = {};
        _.each(newPart, function(val, key){
            if (!basePart[key]){
                if (val !== basePart[key]){
                    diff[key] = serializeComp(val);
                }
            }
            else if (!_.isObject(val)){
                if (val != basePart[key]){
                    diff[key] = val;
                }
            }
            else if (_.isArray(val)){
                if (_.all(val, function(ele){ return !_.isObject(ele); }) && val.length != basePart[key].length){
                    diff[key] = val;
                }
                else{
                    var diffs = _.map(val, function(ele, i){
                        return compareParts(ele, basePart[key][i]);
                    });
                    
                    if (_.any(diffs, function(chkDiff){ return chkDiff && !(_.isObject(chkDiff) && _.isEmpty(chkDiff)); })){
                        diff[key] = diffs;
                    }
                }
            }
            else if (!_.isFunction(val) && val !== undefined){
                var subDiff = compareParts(val, basePart[key]);
                if (subDiff && !_.isEmpty(subDiff)){
                    diff[key] = subDiff;
                }
            }
        });

        if (newPart.serializeComponent && newPart.serializeComponent.postSerialize){
            diff = newPart.serializeComponent.postSerialize(newPart, diff);
        }
        
        return diff;
    };

    var serializeEntity = function(entity){
        if (entity.passedArgs && entity.passedArgs.source){
            delete entity.passedArgs.source;
        }

        var base = {
            key: entity.key,
            passedArgs: serializeComp(entity.passedArgs)
        };

        if (!entity.key){
            console.log(entity);
        }
        
        var chkEntity = makeEcs(base.key, base.passedArgs);
        base.passedArgs = chkEntity.passedArgs;

        var diff = compareParts(entity, chkEntity);

        _.each(base, function(val, key){
            diff[key] = val;
        });

        return diff;
    };

    var updateEntity = function(entity, diff){
        _.each(diff, function(val, key){
            if (val === null){
                return;
            }
            if (!_.isObject(val)){
                entity[key] = val;
            }
            else{
                if (_.isArray(val) && !_.isArray(entity[key])){
                    entity[key] = [];
                }
                else if (_.isObject(val) && !_.isObject(entity[key])){
                    entity[key] = {};
                }
                
                updateEntity(entity[key], val);
            }
        });
    };

    var deserializeEntity = function(data){
        var entity = makeEcs(data.key, data.passedArgs);
        
        updateEntity(entity, data);
        
        return entity;
    };

    
    var getSerializedData = function(game){
        return {
            worldEntities: _.map(game.world.entities, serializeEntity),
            worldEntitiesToAdd: _.map(game.world.entitiesToAdd, serializeEntity),
            uid: uidManager.getUid(),
            gameData: game.saveData(),
            flags: game.world.flags
        };
    };

    return {
        setGame: function(gm){
            game = gm;
        },
        getGame: function(){
            return game;
        },
        save: function(saveKey){
            var serialized = getSerializedData(game);
            
            if (saveKey === undefined){
                localStorage['save'] = serialized;
                return;
            }
            
            
            var savedKeys = [];
            
            if (localStorage.savedKeys){
                savedKeys = JSON.parse(localStorage.savedKeys);
                if (savedKeys.length > 3 && !_.contains(savedKeys, saveKey)){
                    var keyToDelete = savedKeys[0];
                    savedKeys.splice(0, 1);
                    delete localStorage[keyToDelete];
                }
            }
            
            try{
                serialized = JSON.stringify(serialized);
                localStorage[saveKey] = serialized;

                savedKeys.push(saveKey);
                localStorage.savedKeys = JSON.stringify(_.uniq(savedKeys));

                if (STEAM){
                    greenworks.saveTextToFile(saveKey + '.txt', serialized, function(){}, function(err){});
                }
            }
            catch (e){
                console.log('save fail');
                console.log(e);
                console.log(serialized);
            }
        },
        saveChapter: function(saveKey){
            if (!game){
                return;
            }

            var serialized = getSerializedData(game);
            try{
                serialized = JSON.stringify(serialized);
                localStorage[saveKey] = serialized;
            }
            catch (e){
                console.log('save fail');
                console.log(e);
                console.log(serialized);
            }
            console.log('saved ' + saveKey);
        },
        load: function(saveKey){
            if (saveKey === undefined){
                saveKey = 'save';
            }
            
            var saveInfo = false;
            
            if (STEAM){
                if (greenworks.isCloudEnabledForUser() && greenworks.isCloudEnabled()){
                    greenworks.readTextFromFile(saveKey + '.txt',
                                                function(fileContent){
                                                    saveInfo = fileContent;
                                                },
                                                function(){
                                                });

                }
            }

            if (!localStorage[saveKey] && !saveInfo){
                return;
            }
            
            if (!saveInfo){
                saveInfo = localStorage[saveKey];
            }

            var deserialized = JSON.parse(saveInfo);
            game.world.entities = _.map(deserialized.worldEntities, deserializeEntity);
            game.world.entitiesToAdd = _.map(deserialized.worldEntitiesToAdd, deserializeEntity);
            game.world.flags = deserialized.flags;
            game.world.ticksPerTick = PARAMETERS.ticksPerTick[0];

            game.world.afterLoad();
            
            uidManager.setUid(deserialized.uid);
            game.loadData(deserialized.gameData);
        },
        loadLast: function(){
            var keys = JSON.parse(localStorage.savedKeys);
            var key = keys[keys.length - 1];
            this.load(key);
        },
        addEmotions: function(spawnSourceKey, emotionsToAdd, impulseTicks){
            game.world.addEntity(makeEcs(PARAMETERS.emotionSource, {
                key: 'debugEmotions',
                name: 'debug emotions',
                spawnSourceKey: spawnSourceKey,
                emotionsToAdd: emotionsToAdd,
                impulseTicks: impulseTicks
            }));
        },
        getEntity: function(key){
            return game.world.entityForKey(key);
        },
        listEntityKeys: function(){
            _.each(game.world.entities, function(entity){
                if (entity.identityComponent){
                    console.log(entity.identityComponent.key);
                }
            });
        },
        addEntity: function(key, args){
            var entity = makeEcs(key, args);
            game.world.addEntity(entity);
        }
    };
})();

var StartMenu = function(gameProperties){
    this.gameProperties = gameProperties;
    this.delta = 40;
    this.renderer = {
        textColor: "#ffffff",
        insetColor: "#ffffff",
        drawPanelButton: drawPanelButton,
        yTextOffset: 10
    };
    
    var textWidth = 250;

    var y = height / 2 - 127;
    var buttonHeight = 60;
    
    var keys = [];
    
    if (localStorage.autosave){
        keys.push('Continue');
    }

    keys = keys.concat(['Click to Play', 'README', 'Credits']);

    this.buttons = _.map(keys, function(name){
        var rct = [width / 3 - textWidth / 2 - 2, y, textWidth + 4, buttonHeight];
        y += 100;
        return [rct, name];
    });

    y = height / 2 - 77;
    textWidth = 200;

    this.chapterButtons = _.chain(csvIdentifiers.gameStage)
        .filter(function(gameStageKey){
            return localStorage[gameStageKey] !== undefined;
        })
        .map(function(gameStageKey){
            var rct = [2 * width / 3 - textWidth / 2 - 2, y, textWidth + 44, buttonHeight / 2 + 10];
            var name = allArgs[gameStageKey].name;
            
            y += 50;
            
            return [rct, name, gameStageKey];
        })
        .value();
};

StartMenu.prototype.update = function(interval){
    return this.stillRunning;
};

StartMenu.prototype.draw = function(){
    ctx.fillStyle = '#333333';
    ctx.fillRect(0, 0, width, height);
    ctx.fillStyle = '#ffffff';
    ctx.font = getFont(60);
    var txt = 'The Quiet Sleep';
    var textWidth = ctx.measureText(txt).width;
    ctx.fillText(txt, (width - textWidth) / 2, 100);
    
    ctx.font = getFont(40);

    _.each(this.buttons, (button) => {
        this.renderer.drawPanelButton(button[0], button[1]);
    });

    if (this.chapterButtons.length > 0){
        ctx.fillText('Load Chapter', 2 * width / 3 - 125, height / 2 - 100);
    }
    

    ctx.font = getFont(16);

    _.each(this.chapterButtons, (button) => {
        this.renderer.drawPanelButton(button[0], button[1]);
    });
    
    ctx.font = BASE_FONT;
};

StartMenu.prototype.initialize = function(){
    var that = this;
    this.stillRunning = true;
    this.gameProperties.next = new Game(this.gameProperties);
    var textWidth = 250;
    bindHandler.bindFunction((e) => {
        var pos = getPos(e);

        _.each(this.buttons, (rectName, i) => {
            var rect = rectName[0];
            var name = rectName[1];
            
            if (containsPos(rect, pos)){
                if (name == 'README'){
                    window.location = 'README.html';
                }
                else if (name == 'Click To Play'){

                }
                else if (name == 'Continue'){
                    this.gameProperties.saveKey = 'autosave';
                }
                else if (name == 'Credits'){
                    window.location = 'ATTRIBUTION.txt';
                }
                this.stillRunning = false;
            }
        });

        
        _.each(this.chapterButtons, (details, i) => {
            var rect = details[0];

            if (containsPos(rect, pos)){
                this.gameProperties.saveKey = details[2];
            }

            that.stillRunning = false;
        });
    });
};

var getGame = function(gameProperties){
    try{
        if (SKIP_TITLE){
            return new Game(gameProperties);
        }
    }
    catch (e) {}
    return new StartMenu(gameProperties);
};

var GameProperties = function(){
    this.next = null;
    this.saveKey = (window.location.search == '?continue') ? PARAMETERS.autosaveKey : undefined;

    var settings = localStorage.settings;

    if (settings){
        settings = JSON.parse(settings);
    }
    else{
        settings = {
            fullscreen: true,
            music: true,
            sfx: true
        };
    }

    if (settings.musicVolume === undefined){
        settings.musicVolume = 0.1;
        settings.soundVolume = 0.5;
    };

    this.settings = settings;
    
    if (this.settings.fullscreen){
        canvas.webkitRequestFullscreen();
    }
    
    this.backgroundAudio = new Audio("sounds/City-of-Dread_Looping.ogg");
    this.backgroundAudio.loop = true;
    this.backgroundAudio.volume = settings.musicVolume;
    this.backgroundAudio.load();

    getAudio.changeVol(settings.soundVolume);

    if (this.settings.music){
        this.backgroundAudio.play();
    }
};

GameProperties.prototype.save = function(){
    localStorage.settings = JSON.stringify(this.settings);
};
