/*
All this code is copyright Orteil, 2016-2020.
Spoilers ahead.
welcome to my awful soup
https://orteil.dashnet.org
*/
/*
Note : this is the game engine. It loads and interprets data files.
The default data file is located at /data.js and contains all techs, traits, units, policies, resources, terrains etc.
*/
VERSION=1;//increment by 1 with every major change; will make old datasets incompatible
SAVESLOT='alpha';//actual slot will be 'legacySave-'+SAVESLOT
UPDATELOG=[
{date:'2013-2014',title:'Prototype',text:['started working on a prototype for a cross between Civilization and an idle game after the sudden success of my game, Cookie Clicker','encountered some issues with gameplay design and ended up putting the project on hold']},
{date:'2015',title:'Prototype, part II',text:['the broken, buggy prototype has been put online for all to see']},
{date:'2016',title:'Return of the Prototype',text:['motivated by the positive response to the previous prototype, a new version was started from scratch with fresh new ideas']},
{date:'3/23/2017',title:'Alpha launch',text:['a playable alpha is launched publicly','at this point, the game features 54 technologies, 42 units, and 91 resources']},
{date:'3/24/2017',title:'Alpha patch',text:['added an outline to your explored territory on the map','hunters dying, quarries/mines collapsing and scouts getting lost should no longer disproportionately harm your population; those effects have also been made less frequent','people get sick less often','added a new policy to control birth rate','mines can now mine for salt','resources now display how much you\'re gaining and losing every tick']},
{date:'3/25/2017',title:'Alpha patch 2',text:['units are now queued for automatic purchase rather than being purchased directly; this allows them to be automatically replaced should they be harmed','fixed bug with units getting lost or wounded way too much (maybe for good this time?)','buildings no longer require you to have available tools and workers to build them, as this was confusing and not very fun','graves now decay over time to make room for more; architects now have an "undertaker" mode that automatically creates graves if there are unburied corpses','material and food decay were slowed and storage units have bigger capacity']},
{date:'3/25/2017',title:'Alpha patch 3',text:['units can now be active or inactive; a building that lacks workers, or a crafter that lacks its tools, will simply go inactive instead of disappearing, and will be made active again when the requirements are met; units are inactive when they\'re first created and when they\'ve just been set to a new mode','the mausoleum can be completed again','graves should behave better']},
{date:'3/26/2017',title:'Alpha patch 4',text:['fixed many miscellaneous bugs, hopefully','scouting and exploring speed now properly depends on how many wanderers or scouts you have','gathering is now soft-capped by natural resources; this means having many gathering units but few tiles won\'t have optimal results','removing units now removes the idle ones first']},
{date:'3/26/2017',title:'Alpha patch 5',text:['your people will no longer be completely apathetic and neutrally healthy from some bug with consuming food','fire pits warm more people','clothiers no longer need to know leatherworking to sew grass clothing','happiness and health sources are detailed more explicitly','many messages now have icons']},
{date:'3/28/2017',title:'Alpha patch 6',text:['unit modes now have icons','added custom bulk-buying in units','gathering was reworked, expect different rates for resource production','workers dying while working probably won\'t result in ghost workers anymore','units have innate priorities in the context of being created and acting, with food-producing units going first','happier people now produce more babies, while unhappy people just aren\'t feeling it as much','corpses decay slowly']},
];
//misc handy stuff
function l(what) {return document.getElementById(what);}
function choose(arr) {return arr[Math.floor(Math.random()*arr.length)];}
function randomFloor(x) {if ((x%1)= 1000 && isFinite(value))
{
value /= 1000;
while(Math.round(value) >= 1000)
{
value /= 1000;
base++;
}
if (base > notations.length) {return 'Inf';} else {notationValue = notations[base];}
}
return ( Math.round(value * 10) / 10 ) + notationValue;
};
}
function rawFormatter(value) {return Math.round(value * 1000) / 1000;}
var numberFormatters =
[
rawFormatter,
formatEveryThirdPower([
' thousand',
' million',
' billion',
' trillion',
' quadrillion',
' quintillion',
' sextillion',
' septillion',
' octillion',
' nonillion',
' decillion'
]),
formatEveryThirdPower([
'k',
'M',
'B',
'T',
'Qa',
'Qi',
'Sx',
'Sp',
'Oc',
'No',
'Dc'
])
];
function Beautify(value,floats)
{
var negative=(value<0);
var decimal='';
if (Math.abs(value)<1000 && floats>0) decimal='.'+(value.toFixed(floats).toString()).split('.')[1];
value=Math.floor(Math.abs(value));
var formatter=numberFormatters[2];
var output=formatter(value).toString().replace(/\B(?=(\d{3})+(?!\d))/g,',');
if (output=='0') negative=false;
return negative?'-'+output:output+decimal;
}
var B=Beautify;
function BeautifyTime(value)
{
//value should be in seconds
value=Math.max(Math.ceil(value,0));
var years=Math.floor(value/31536000);
value-=years*31536000;
var days=Math.floor(value/86400);
value-=days*86400;
var hours=Math.floor(value/3600)%24;
value-=hours*3600;
var minutes=Math.floor(value/60)%60;
value-=minutes*60;
var seconds=Math.floor(value)%60;
var str='';
if (years) str+=B(years)+'Y';
if (days || str!='') str+=B(days)+'d';
if (hours || str!='') str+=hours+'h';
if (minutes || str!='') str+=minutes+'m';
if (seconds || str!='') str+=seconds+'s';
if (str=='') str+='0s';
return str;
}
var BT=BeautifyTime;
function cap(str)
{return str.charAt(0).toUpperCase()+str.slice(1);}
//polyfills
if (!String.prototype.includes) {
Object.defineProperty(String.prototype, "includes", {value:
function(search, start) {
'use strict';
if (typeof start !== 'number') {
start = 0;
}
if (start + search.length > this.length) {
return false;
} else {
return this.indexOf(search, start) !== -1;
}
}
});
}
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, "includes", {value:
function(searchElement /*, fromIndex*/ ) {
'use strict';
var O = Object(this);
var len = parseInt(O.length, 10) || 0;
if (len === 0) {
return false;
}
var n = parseInt(arguments[1], 10) || 0;
var k;
if (n >= 0) {
k = n;
} else {
k = len + n;
if (k < 0) {k = 0;}
}
var currentElement;
while (k < len) {
currentElement = O[k];
if (searchElement === currentElement) { // NaN !== NaN
return true;
}
k++;
}
return false;
}
});
}
//other fun stuff
//seeded random function, courtesy of http://davidbau.com/archives/2010/01/30/random_seeds_coded_hints_and_quintillions.html
(function(a,b,c,d,e,f){function k(a){var b,c=a.length,e=this,f=0,g=e.i=e.j=0,h=e.S=[];for(c||(a=[c++]);d>f;)h[f]=f++;for(f=0;d>f;f++)h[f]=h[g=j&g+a[f%c]+(b=h[f])],h[g]=b;(e.g=function(a){for(var b,c=0,f=e.i,g=e.j,h=e.S;a--;)b=h[f=j&f+1],c=c*d+h[j&(h[f]=h[g=j&g+b])+(h[g]=b)];return e.i=f,e.j=g,c})(d)}function l(a,b){var e,c=[],d=(typeof a)[0];if(b&&"o"==d)for(e in a)try{c.push(l(a[e],b-1))}catch(f){}return c.length?c:"s"==d?a:a+"\0"}function m(a,b){for(var d,c=a+"",e=0;c.length>e;)b[j&e]=j&(d^=19*b[j&e])+c.charCodeAt(e++);return o(b)}function n(c){try{return a.crypto.getRandomValues(c=new Uint8Array(d)),o(c)}catch(e){return[+new Date,a,a.navigator.plugins,a.screen,o(b)]}}function o(a){return String.fromCharCode.apply(0,a)}var g=c.pow(d,e),h=c.pow(2,f),i=2*h,j=d-1;c.seedrandom=function(a,f){var j=[],p=m(l(f?[a,o(b)]:0 in arguments?a:n(),3),j),q=new k(j);return m(o(q.S),b),c.random=function(){for(var a=q.g(e),b=g,c=0;h>a;)a=(a+c)*d,b*=d,c=q.g(1);for(;a>=i;)a/=2,b/=2,c>>>=1;return(a+c)/b},p},m(c.random(),b)})(this,[],Math,256,6,52);
chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ0123456789'.split('');
makeSeed=function(len)
{
var str='';
for (var i=0;i';
for (var i in G.versions)
{
var version=G.versions[i];
str+='';
}
str+='';
l('versions').innerHTML=str;
}
//upscale pixel icons and apply to stylesheet (this is kind of cheaty)
//only for Edge and IE since they have """"trouble"""" with nearest-neighbor
G.iconScale=1;
G.iconURL='img/iconSheet.png?v=1';
if (G.isIE)
{
var img=Pic('img/iconSheet.png?v=1');
var c=document.createElement('canvas');c.width=img.width*2;c.height=img.height*2;
var ctx=c.getContext('2d');
ctx.mozImageSmoothingEnabled=false;
ctx.webkitImageSmoothingEnabled=false;
ctx.msImageSmoothingEnabled=false;
ctx.imageSmoothingEnabled=false;
ctx.drawImage(img,0,0,img.width*2,img.height*2);
var sheet=(function()
{
var style=document.createElement('style');
style.appendChild(document.createTextNode(''));
document.head.appendChild(style);
return style.sheet;
})();
addCSSRule(sheet,'.IE .icon','background-image:url('+c.toDataURL('image/png')+')');
G.iconURL=c.toDataURL('image/png');
addCSSRule(sheet,'.IE .icon.double','background-image:url('+G.iconURL+'),url('+G.iconURL+')');
G.wrapl.classList.add('IE');
G.iconScale=2;
}
G.w=window.innerWidth;
G.h=window.innerHeight;
G.resizing=false;
G.stabilizeResize=function()
{
G.resizing=false;
//change page layout to fit width
if (G.w<288*3) {G.wrapl.classList.remove('narrow');G.wrapl.classList.add('narrower');}
else if (G.w<384*3) {G.wrapl.classList.remove('narrower');G.wrapl.classList.add('narrow');}
else {G.wrapl.classList.remove('narrower');G.wrapl.classList.remove('narrow');}
//if (G.tab.id=='unit') G.cacheUnitBounds();
}
G.resize=function()
{
G.resizing=true;
}
window.addEventListener('resize',function(event)
{
G.w=window.innerWidth;
G.h=window.innerHeight;
G.resize();
});
G.mouseDown=false;//mouse button just got pressed
G.mouseUp=false;//mouse button just got released
G.mousePressed=false;//mouse button is currently down
G.clickL=0;//what element got clicked
AddEvent(document,'mousedown',function(event){G.mouseDown=true;G.mousePressed=true;G.mouseDragFrom=event.target;G.mouseDragFromX=G.mouseX;G.mouseDragFromY=G.mouseY;});
AddEvent(document,'mouseup',function(event){G.mouseUp=true;G.mouseDragFrom=0;});
AddEvent(document,'click',function(event){G.clickL=event.target;});
G.mouseX=0;
G.mouseY=0;
G.mouseMoved=0;
G.draggedFrames=0;//increment every frame when we're moving the mouse and we're clicking
G.GetMouseCoords=function(e)
{
var posx=0;
var posy=0;
if (!e) var e=window.event;
if (e.pageX||e.pageY)
{
posx=e.pageX;
posy=e.pageY;
}
else if (e.clientX || e.clientY)
{
posx=e.clientX+document.body.scrollLeft+document.documentElement.scrollLeft;
posy=e.clientY+document.body.scrollTop+document.documentElement.scrollTop;
}
var x=0;
var y=0;
G.mouseX=posx-x;
G.mouseY=posy-y;
G.mouseMoved=1;
}
AddEvent(document,'mousemove',G.GetMouseCoords);
G.Scroll=0;
G.handleScroll=function(e)
{
if (!e) e=event;
G.Scroll=(e.detail<0||e.wheelDelta>0)?1:-1;
};
AddEvent(document,'DOMMouseScroll',G.handleScroll);
AddEvent(document,'mousewheel',G.handleScroll);
G.keys=[];//key is being held down
G.keysD=[];//key was just pressed down
G.keysU=[];//key was just pressed up
//shift=16, ctrl=17
AddEvent(window,'keyup',function(e){
if ((document.activeElement.nodeName=='TEXTAREA' || document.activeElement.nodeName=='INPUT') && e.keyCode!=27) return;
if (e.keyCode==27) {}//esc
else if (e.keyCode==13) {}//enter
G.keys[e.keyCode]=0;
G.keysD[e.keyCode]=0;
G.keysU[e.keyCode]=1;
});
AddEvent(window,'keydown',function(e){
if (!G.keys[e.keyCode])//prevent repeats
{
if (e.ctrlKey && e.keyCode==83) {e.preventDefault();}//ctrl-s
if ((document.activeElement.nodeName=='TEXTAREA' || document.activeElement.nodeName=='INPUT') && e.keyCode!=27) return;
if (e.keyCode==32) {e.preventDefault();}//space
G.keys[e.keyCode]=1;
G.keysD[e.keyCode]=1;
G.keysU[e.keyCode]=0;
//console.log('Key pressed : '+e.keyCode);
}
});
AddEvent(window,'blur',function(e){
G.keys=[];
G.keysD=[];
G.keysU=[];
});
//latency compensator stuff
G.time=new Date().getTime();
G.fpsMeasure=new Date().getTime();
G.accumulatedDelay=0;
G.catchupLogic=0;
G.fpsStartTime=0;
G.frameNumber=0;
G.getFps=function()
{
G.frameNumber++;
var currentTime=(Date.now()-G.fpsStartTime )/1000;
var result=Math.floor((G.frameNumber/currentTime));
if (currentTime>1)
{
G.fpsStartTime=Date.now();
G.frameNumber=0;
}
return result;
}
G.fpsGraph=l('fpsGraph');
G.fpsGraphCtx=G.fpsGraph.getContext('2d');
var ctx=G.fpsGraphCtx;
ctx.fillStyle='#000';
ctx.fillRect(0,0,128,64);
G.currentFps=0;
G.previousFps=0;
G.animIntro=true;
G.introDur=G.fps*1;
//is there a file save already? if yes, load it, if not, hard-reset and start a new game
if (!G.Load())
{
G.Reset(true);
G.NewGame();
}
G.resize();
G.Loop();
}
/*=====================================================================================
UPDATES, DRAWS & LOGICS
=======================================================================================*/
G.update=[];//these involve rebuilding a whole display's DOM
G.draw=[];//these involve updating elements within the display and should be invoked within G.Draw
G.logic=[];//these involve updating gameplay elements and should be invoked within G.Logic
/*=====================================================================================
SAVING AND LOADING
=======================================================================================*/
G.saveTo='legacySave-'+SAVESLOT;
G.FileSave=function()
{
var filename='legacySave';
var text=G.Export();
var blob=new Blob([text],{type:'text/plain;charset=utf-8'});
saveAs(blob,filename+'.txt');
}
G.FileLoad=function(e)
{
if (e.target.files.length==0) return false;
var file=e.target.files[0];
var reader=new FileReader();
reader.onload=function(e)
{
G.Import(e.target.result);
}
reader.readAsText(file);
}
G.Export=function()
{
return G.Save(true);
}
G.Import=function(str)
{
G.importStr=str;
G.Load(false);
}
G.Save=function(toStr)
{
//if toStr is true, don't actually save; return a string containing the save
if (!toStr && G.local && G.isIE) return false;
var str='';
//general
G.lastDate=parseInt(Date.now());
str+=
parseFloat(G.engineVersion).toString()+';'+
parseFloat(G.startDate).toString()+';'+
parseFloat(G.fullDate).toString()+';'+
parseFloat(G.lastDate).toString()+';'+
parseFloat(G.year).toString()+';'+
parseFloat(G.day).toString()+';'+
parseFloat(G.fastTicks).toString()+';'+
parseFloat(G.furthestDay).toString()+';'+
parseFloat(G.totalDays).toString()+';'+
parseFloat(G.resets).toString()+';'+
'';
str+='|';
//settings
for (var i in G.settings)
{
var me=G.settings[i];
if (me.type=='toggle') str+=(me.value?'1':'0');
else if (me.type=='int') str+=parseInt(me.value).toString();
str+=';';
}
str+='|';
//mods
for (var i in G.mods)
{
var me=G.mods[i];
str+='"'+me.url.replaceAll('"','"')+'":';
if (me.achievs)
{
//we save achievements separately for each mod
for (var ii in me.achievs)
{
str+=parseInt(me.achievs[ii].won).toString()+',';
}
}
str+=':';
//tracked stats (not fully implemented yet)
str+=parseFloat(G.trackedStat).toString();
str+=';';
}
str+='|';
//culture and names
str+=(G.cultureSeed)+';';
str+=G.getSafeName('ruler')+';';
str+=G.getSafeName('civ')+';';
str+=G.getSafeName('civadj')+';';
str+=G.getSafeName('inhab')+';';
str+=G.getSafeName('inhabs')+';';
str+='|';
//maps
str+=(G.currentMap.seed)+';';
var map=G.currentMap;
for (var x=0;x0)
{
str+=parseInt(me.unit.id).toString()+','+
parseFloat(Math.round(me.amount)).toString()+
((me.unit.gizmos||me.unit.wonder)?
(','+parseInt(me.unit.wonder?me.mode:(me.mode?me.mode.num:0)).toString()+','+//mode
parseInt(me.percent).toString())//percent
:'')+
','+parseFloat(Math.round(me.targetAmount)).toString()+
','+parseFloat(Math.round(me.idle)).toString()+
';';
}
}
str+='|';
//chooseboxes
var len=G.chooseBox.length;
for (var i=0;i=0;i--)
{if (spl[i]!='') {G.gainTech(G.know[parseInt(spl[i])]);}}
var spl=str[s++].split(';');
//console.log('Traits : '+spl);
var len=spl.length;
for (var i=len-1;i>=0;i--)
{if (spl[i]!='') G.gainTrait(G.know[parseInt(spl[i])]);}
//policies
var spl=str[s++].split(';');
//console.log('Policies : '+spl);
var len=spl.length;
for (var i=len-1;i>=0;i--)
{if (spl[i]!='') {
var spl2=spl[i].split(',');
var me=G.policy[parseInt(spl2[0])];
G.gainPolicy(me);
me.mode=me.modesById[parseInt(spl2[1])];
}}
//res
var spl=str[s++].split(';');
//console.log('Resources : '+spl);
var len=G.res.length;
for (var i=0;i=0;i--)
{if (spl[i]!='')
{
var spl2=spl[i].split(',');
//unit id, amount, and if unit has gizmos : mode, percent
var obj={
id:G.unitN,
unit:G.unit[parseInt(spl2[0])],
amount:parseFloat(spl2[1]),
targetAmount:((typeof spl2[4]!=='undefined')?parseFloat(spl2[4]):parseFloat(spl2[1])),
idle:((typeof spl2[5]!=='undefined')?parseFloat(spl2[5]):0),
displayedAmount:0,
mode:parseInt(spl2[2])||0,
percent:parseInt(spl2[3]),
popups:[]
};
G.unitsOwned.unshift(obj);
var unit=G.unitsOwned[0];
if (unit.unit.modesById[0]) unit.mode=unit.unit.modesById[unit.mode];
G.unitsOwnedNames.unshift(G.unit[parseInt(spl2[0])].name);
G.unitN++;
}
}
//assign unit .splitOf
var prev=0;
var len=G.unitsOwned.length;
for (var i=0;i=0;i--)
{if (spl[i]!='')
{
G.chooseBox[i].choices=[];
var spl2=spl[i].split(',');
for (var ii in spl2)
{
if (ii==0) G.chooseBox[i].roll=parseFloat(spl2[ii]);
else G.chooseBox[i].choices[ii-1]=G.know[parseInt(spl2[ii])];
}
}
}
G.runUnitReqs();
G.runPolicyReqs();
G.applyAchievEffects('load');
G.updateEverything();
G.createTopInterface();
G.createDebugMenu();
if (G.tabs[G.settingsByName['tab'].value]) G.setTab(G.tabs[G.settingsByName['tab'].value]);
G.setSetting('forcePaused',0);
l('blackBackground').style.opacity=0;
if (timeOffline>=1) G.middleText('- Welcome back - You accumulated '+B(timeOffline)+' fast ticks while you were away.',true);
G.rememberAchievs=true;
G.animIntro=true;
G.introDur=G.fps*1;
G.doFunc('game loaded');
G.Logic(true);//force a tick (solves some issues with display updates; this howeverr means loading a paused game, saving and reloading will make a single day go by everytime, which isn't ideal)
console.log('Game loaded successfully.');
return true;
}
return false;
}
G.Clear=function()
{
//erase the save and start a new one, handy when the page crashes when testing new save formats
console.log('Save data cleared. Refresh the page to take effect.');
G.T=0;
window.localStorage.setItem(G.saveTo,'');
var debug=0;
if (G.getSetting('debug')) debug=1;
G.Reset(true);
if (debug) G.setSetting('debug',1);
G.NewGame();
}
G.Reset=function(hard)
{
//clean all data and set the stage
//console.log('Resetting...');
G.T=0;
if (hard) G.resetSettings();
G.startDate=parseInt(Date.now());//when we started playing
if (hard) G.fullDate=parseInt(Date.now());//when we started playing (carries over with resets)
G.lastDate=parseInt(Date.now());//when we last saved the game (used to compute offline progression)
G.tick=0;//how many ticks have elapsed since we started
G.nextTick=0;
G.year=0;//current game year (can be calculated from G.tick) - 5 irl minutes = 1 ingame year
G.day=0;//300 days in a year; 1 day = 1 irl second
if (hard)
{
G.resets=0;//how many ascending resets we've done
G.furthestDay=G.year;//furthest time we've pushed a game
G.totalDays=0;//total time we've done across all games
G.trackedStat=0;//this tracks population for now, but should be generalized in the future
}
G.fastTicks=0;//how many fast ticks we've accumulated by being paused or offline; we can run the game on fast speed until these run out
G.nextFastTick=G.tickDuration;
G.on=true;//set this to false when game over
G.deleteSelfUpdatingTexts();
G.dialogue.init();
G.widget.init();
G.tooltip.init();
G.infoPopup.init();
G.initMessages();
G.particlesInit();
G.buildTabs();
if (hard) {G.savedAchievs=[];G.rememberAchievs=false;}
G.CreateData();
G.resInstances=[];
G.unitN=0;
G.unitsOwned=[];
G.unitsOwnedNames=[];
G.techN=0;
G.traitN=0;
G.techsOwned=[];
G.techsOwnedNames=[];
G.traitsOwned=[];
G.traitsOwnedNames=[];
G.shortMemory=5;
G.longMemory=15;
G.shouldRunReqs=0;
G.exploreNewTiles=0;
G.exploreOwnedTiles=0;
G.allowShoreExplore=false;
G.allowOceanExplore=false;
G.names=[];
G.maps=[];
G.resCategoriesByName=[];
for (var i in G.resCategories)
{
if (typeof G.resCategories[i].open==='undefined') G.resCategories[i].open=true;
G.resCategoriesByName[i]=G.resCategories[i];
}
for (var i in G.res)
{
if (G.res[i].category && G.resCategoriesByName[G.res[i].category]) G.resCategoriesByName[G.res[i].category].base.push(G.res[i].name);
}
G.unitCategoriesByName=[];
for (var i in G.unitCategories)
{
if (typeof G.unitCategories[i].open==='undefined') G.unitCategories[i].open=true;
G.unitCategoriesByName[i]=G.unitCategories[i];
G.unitCategories[i].base=[];
}
for (var i in G.unit)
{
if (G.unit[i].category && G.unitCategoriesByName[G.unit[i].category]) G.unitCategoriesByName[G.unit[i].category].base.push(G.unit[i]);
}
G.policyCategoriesByName=[];
for (var i in G.policyCategories)
{
if (typeof G.policyCategories[i].open==='undefined') G.policyCategories[i].open=true;
G.policyCategoriesByName[i]=G.policyCategories[i];
G.policyCategories[i].base=[];
}
for (var i in G.policy)
{
if (G.policy[i].category && G.policyCategoriesByName[G.policy[i].category]) G.policyCategoriesByName[G.policy[i].category].base.push(G.policy[i]);
}
G.knowCategoriesByName=[];
for (var i in G.knowCategories)
{
if (typeof G.knowCategories[i].open==='undefined') G.knowCategories[i].open=true;
G.knowCategoriesByName[i]=G.knowCategories[i];
G.knowCategories[i].base=[];
}
for (var i in G.know)
{
if (G.know[i].category && G.knowCategoriesByName[G.know[i].category]) G.knowCategoriesByName[G.know[i].category].base.push(G.know[i]);
}
for (var i in G.chooseBox)
{
G.initChooseBox(G.chooseBox[i]);
}
/*for (var i in G.unit)
{
}
for (var i in G.tech)
{
}
for (var i in G.trait)
{
}*/
//reset map controls
G.initMap();
G.sequence='main';
/*this is the game's state, which can have the following values :
-"loading", when resources are being loaded
-"failed loading", when the loading sequence times out without completing
-"checking", when all mods are loaded and their status is being checked
-"updating", when all mods are checked and their manifests are being checked
-"main", regular gameplay
-"settle", a screen where the player can start a new game and settle on a tile
*/
l('foreground').style.display='none';
var div=l('deleteOnLoad');
if (div)
{
div.outerHTML='';
delete div;
}
}
G.NewGameWithSameMods=function()
{
var mods=[];
for (var i in G.mods)
{
mods.push(G.mods[i].url);
}
G.NewGame(false,mods);
}
G.savedAchievs=[];
G.rememberAchievs=false;
G.NewGame=function(doneLoading,mods)
{
//clean up data, create a map and ask the player to pick a starting location
if (!doneLoading)
{
//save achievements for each mod so we can reapply them later
if (G.rememberAchievs)
{
var achievs=[];
for (var i in G.mods)
{
var me=G.mods[i];
achievs[me.name]=[];
if (me.achievs)
{
for (var ii in me.achievs)
{
achievs[me.name].push(me.achievs[ii].won);
}
}
}
G.savedAchievs=achievs;
}
G.rememberAchievs=false;
G.LoadMods(mods||['data.js'],G.NewGame,true);
return 0;
}
try
{
G.Reset();
l('blackBackground').style.opacity=1;
G.modsStr='';
G.newModsStr='';
for (var i in G.mods)
{
G.modsStr+=G.mods[i].url+'\n';
}
G.applyAchievEffects('pre-new');
G.sequence='settle';
}
catch(err)
{
G.sequence='failed loading';
console.log('Something went wrong :');
console.log(err.message||err);
G.dialogue.popup(function(div){
return '
Error!
'+
'
Something went wrong when launching a new game :
'+
'
'+(err.message||err)+'
'+
'
'+
'
'+
G.button({tooltip:'Try to select different mods this time!',text:'Back to menu',classes:'frameless',onclick:function(){G.dialogue.forceClose();G.NewGame();}})+
'
';
},'noClose');
return 0;
}
//create starting names
G.cultureSeed=makeSeed(5);
G.setName('ruler',G.translate(cap(G.getRandomString(3,5)),['primitive'],G.cultureSeed));
var civname=cap(G.getRandomString(3,6));
G.setName('civ',G.translate(civname,['primitive'],G.cultureSeed));
if (Math.random()<0.05)
{
G.setName('inhab','child of '+G.getName('ruler'));
G.setName('inhabs','children of '+G.getName('ruler'));
}
else
{
G.setName('inhab',G.translate(civname+G.getRandomString(1),['primitive'],G.cultureSeed));
var str=G.getName('inhab');
var finds=['s','x','z'];
if (finds.indexOf(str.slice(-1))==-1) str+='s';
else str+='es';
G.setName('inhabs',str);
}
var str=G.getName('civ').toLowerCase();
var finds=['a','e','i','o','u','y'];
if (finds.indexOf(str.slice(-1))==-1) str+='';//ends in consonant
else str+=choose(['n','d','s','t','b','l']);//ends in vowel
str+=choose(['ian','ish','ese','an']);
G.setName('civadj',str);
G.dialogue.popup(function(div){
return '
Start a new game
'+
G.button({style:'position:absolute;right:-6px;top:-6px;',tooltip:'Select mods for this playthrough.',text:'Use mods',onclick:function(e){G.SelectMods();}})+
G.button({style:'position:absolute;left:-6px;top:-6px;',tooltip:'View the game\'s version history.',text:'Update log',onclick:function(e){G.dialogue.popup(G.tabPopup['updates'],'bigDialogue');}})+
G.button({style:'position:absolute;left:-6px;top:20px;',tooltip:'Change the game\'s settings.',text:'Settings',onclick:function(e){G.dialogue.popup(G.tabPopup['settings'],'bigDialogue');}})+
'
'+G.textWithTooltip('About this alpha','
The game in its current state features stone age technology and up to some parts of iron age.
Features to be added later include agriculture, religion, commerce, military, and interactions with other civilizations, among other things planned.
Feedback about bugs, oversights and technological inaccuracies are appreciated! (Send me a message to my tumblr at the top)
Thank you for playing this alpha!
-Orteil
')+'
'+
G.doFunc('new game blurb','What is your name? ')+
G.field({style:'width:100%;',text:G.getName('ruler'),tooltip:'Enter your name here. Make it something memorable!',oninput:function(val){G.setName('ruler',val);}})+
''+
(G.resets>0?('You have '+B(G.resets)+' ascension'+(G.resets==1?'':'s')+' behind you. '):'')+
'You choose to start somewhere...
'+
/*G.button({style:'display:block;width:100%;',tooltip:'Start your civilization!',text:'Well okay then',onclick:function(e){var names=G.names;G.dialogue.forceClose();G.NewGameConfirm();G.names=names;}})+*/
G.button({style:'width:33%;min-width:75px;box-shadow:0px 0px 1px 1px #963;',/*style:'display:block;width:100%;',*/tooltip:'Start your civilization in a harsh terrain with scarce natural resources.',text:'Awful',onclick:function(e){G.startingType=1;var names=G.names;G.dialogue.forceClose();G.NewGameConfirm();G.names=names;}})+
G.button({style:'width:33%;min-width:75px;box-shadow:0px 0px 1px 1px #693;',/*style:'display:block;width:100%;',*/tooltip:'Start your civilization in a welcoming terrain full of natural resources.',text:'Pleasant',onclick:function(e){G.startingType=0;var names=G.names;G.dialogue.forceClose();G.NewGameConfirm();G.names=names;}})+
G.button({style:'width:33%;min-width:75px;box-shadow:0px 0px 1px 1px #666;',/*style:'display:block;width:100%;',*/tooltip:'Start your civilization in a random place on the map. Who knows how your people will fare in these strange lands!',text:'Random',onclick:function(e){G.startingType=2;var names=G.names;G.dialogue.forceClose();G.NewGameConfirm();G.names=names;}})+
'
';
},'noClose');
}
G.NewGameConfirm=function()
{
//the player has selected a starting location; launch the game proper
//G.Reset();
G.sequence='main';
G.T=0;
G.rememberAchievs=true;
for (var i in G.savedAchievs)
{
//reload achievements
if (G.modsByName[i] && G.modsByName[i].achievs)
{
for (var ii in G.savedAchievs[i])
{
if (G.modsByName[i].achievs[ii]) G.modsByName[i].achievs[ii].won=G.savedAchievs[i][ii];
}
}
}
//init everything
G.createMaps();
for (var i in G.res)
{
G.res[i].amount=G.res[i].startWith;
}
for (var i in G.tech)
{
if (G.tech[i].startWith) G.gainTech(G.tech[i]);
}
for (var i in G.trait)
{
if (G.trait[i].startWith) G.gainTrait(G.trait[i]);
}
for (var i in G.policy)
{
if (G.policy[i].startWith) G.gainPolicy(G.policy[i]);
}
for (var i in G.res)
{
G.res[i].tick(G.res[i],G.tick);
}
G.runUnitReqs();
G.runPolicyReqs();
G.applyAchievEffects('new');
G.updateEverything();
G.createTopInterface();
G.createDebugMenu();
for (var i in G.unit)
{
if (G.unit[i].startWith) {G.buyUnitByName(G.unit[i].name,G.unit[i].startWith);}
}
l('blackBackground').style.opacity=0;
G.setSetting('forcePaused',0);
G.setSetting('paused',0);
G.setSetting('fast',0);
G.animIntro=true;
G.introDur=G.fps*3;
G.doFunc('new game');
G.Message({type:'important',text:'If this is your first time playing, you may want to consult some quick '+G.button({text:'Getting started',tooltip:'Read a few tips on how to make it past the stone age.',onclick:function(){G.dialogue.popup(function(div){
return '
'+
'
A few tips on how to not die horribly :
'+
'
'+
'
Mouse over these buttons for more explanations!
'+
'
early on, focus most of your workers on food gathering
'+
'
assign a few spare workers as dreamers, in order to get some Insight which you can use to research technologies
'+
'
check the territory tab and click your starting location; if you\'ve got very few sources of food or water, you might want to restart the game
'+
'
don\'t bother researching fishing or hunting if none of your tiles have animals or fish!
'+
'
enabling elder/child work policies can be useful if you need extra workers, but may prove detrimental to your people\'s health
'+
'
if things get too hectic, you can pause the game and take your time
'+
'
this is an early alpha, so you don\'t have to worry about meeting other civilizations just yet
'+
'
sometimes things just go wrong; don\'t lose hope, you can always start over!
'+
G.button({text:'Load mods',classes:'frameless',onclick:function(){
G.dialogue.close();
var mods=G.newModsStr;
mods=mods.split('\n');
var mods2=[];
for (var i in mods)
{
mods[i]=mods[i].trim()
if (mods[i].length>0) mods2.push(mods[i]);
}
G.NewGame(false,mods2);
}})+
G.dialogue.getCloseButton('Cancel')+
'
';
});
}
G.GameOver=function()
{
if (G.on)
{
G.on=false;
G.doFunc('game over');
}
}
/*=====================================================================================
SOME INTERFACE STUFF
=======================================================================================*/
G.createTopInterface=function()
{
var str=''+
'
-
'+
''+
'
0
'+
G.button({id:'pauseButton',
text:'',
tooltip:'Time will be stopped. Generates fast ticks.',
onclick:function(){G.setSetting('paused',1);}
})+
G.button({id:'playButton',
text:'',
tooltip:'Time will pass by normally - 1 day every second.',
onclick:function(){G.setSetting('paused',0);G.setSetting('fast',0);}
})+
G.button({id:'fastButton',
text:'',
tooltip:'Time will go by about 30 times faster - 1 month every second. Uses up fast ticks. May lower browser performance while active.',
onclick:function(){if (G.fastTicks>0) {G.setSetting('paused',0);G.setSetting('fast',1);}}
})+
'';
l('topInterface').innerHTML=str;
G.addTooltip(l('date'),function(){return '
Date
This is the current date in your civilization. One day elapses every second, and 300 days make up a year.
This is how many ingame days you can run at fast speed.
You gain a fast tick for every second you\'re paused or offline.
You also gain fast ticks everytime you research a technology.
You currently have '+BT(G.fastTicks)+' of game time saved up, which will execute in '+BT(G.fastTicks/30)+' at fast speed, advancing your civilization by '+G.BT(G.fastTicks)+'.
';},{offY:-8});
l('fastTicks').onclick=function(e)
{
if (G.getSetting('debug'))
{
//debug : gain fast ticks
G.fastTicks+=10*G.getBuyAmount();
G.fastTicks=Math.max(0,G.fastTicks);
}
};
G.addCallbacks();
G.updateSpeedButtons();
}
G.updateSpeedButtons=function()
{
var div=l('pauseButton');
if (div)
{
var speed=1;
if (G.getSetting('fast')) speed=2;
if (G.getSetting('paused') || G.getSetting('forcePaused')) speed=0;
if (speed==0) {if (G.getSetting('animations')) {triggerAnim(l('pauseButton'),'plop');} l('pauseButton').classList.add('on');l('playButton').classList.remove('on');l('fastButton').classList.remove('on');}
else if (speed==1) {if (G.getSetting('animations')) {triggerAnim(l('playButton'),'plop');} l('pauseButton').classList.remove('on');l('playButton').classList.add('on');l('fastButton').classList.remove('on');}
else if (speed==2) {if (G.getSetting('animations')) {triggerAnim(l('fastButton'),'plop');} l('pauseButton').classList.remove('on');l('playButton').classList.remove('on');l('fastButton').classList.add('on');}
}
}
G.createDebugMenu=function()
{
var str=''+
'
'+
G.button({text:'New game',tooltip:'Instantly start a new game.',onclick:function(){G.T=0;G.NewGameWithSameMods();}})+
G.button({text:'Load',tooltip:'Reload the save.',onclick:function(){G.T=0;G.Load();}})+
G.button({text:'Clear',tooltip:'Wipe save data.',onclick:function(){G.Clear();}})+
' '+
G.button({text:'ALMIGHTY',tooltip:'Unlock every tech, trait and policy.',onclick:function(){
for (var i in G.tech)
{
if (!G.techsOwnedNames.includes(G.tech[i].name)) G.gainTech(G.tech[i]);
}
for (var i in G.trait)
{
if (!G.traitsOwnedNames.includes(G.trait[i].name)) G.gainTrait(G.trait[i]);
}
for (var i in G.policy)
{
G.gainPolicy(G.policy[i]);
}
G.shouldRunReqs=true;
G.middleText('- You are almighty! -');
}})+
G.writeSettingButton({id:'showAllRes',name:'showAllRes',text:'Show resources',tooltip:'Toggle whether all resources should be visible.'})+
G.writeSettingButton({id:'tieredDisplay',name:'tieredDisplay',text:'Show tiers',tooltip:'Toggle whether technologies should display in tiers instead of in the order they were researched. When in that mode, click a tech to highlight its ancestors and descendants.'})+
' '+
G.button({text:'Reveal map',tooltip:'Explore the whole map instantly.',onclick:function(){G.revealMap(G.currentMap);}})+
G.textWithTooltip('?','
This is the debug menu. Please debug responsibly. Further debug abilities while this mode is active :
click resources to add/remove some (keyboard shortcuts work the same way they do for purchasing units)
ctrl-click a tech, trait or policy to remove it (may have strange, buggy effects)
click the Fast ticks display to get more fast ticks
always see tech costs and requirements
gain access to debug robot units
edit the map
','infoButton')+
'
';
l('debug').innerHTML=str;
G.addCallbacks();
}
G.Cheat=function()
{
if (!G.getSetting('debug'))
{G.setSetting('debug',1);G.middleText('- Debug mode activated -');G.Message({type:'important',text:'Debug mode activated.'});return 'Debug mode activated.';}
else {G.setSetting('debug',0);G.middleText('- Debug mode disabled -');G.Message({type:'important',text:'Debug mode disabled.'});return 'Debug mode disabled.';}
}
G.RuinTheFun=G.Cheat;
G.Debug=G.Cheat;
//some neat functions i wish i came up with earlier
G.buttonsN=0;
G.button=function(obj)
{
//returns a string for a new button; creates a callback that must be applied after the html has been created, with G.addCallbacks()
//obj can have text, tooltip (text that shows on hover), onclick (function executed when button is clicked), classes (CSS classes added to the button), id (force button to have that id)
var id=obj.id||('button-'+G.buttonsN);
var str='
'+(obj.text||'-')+'
';
if (obj.onclick || obj.tooltip || obj.tooltipFunc)
{
G.pushCallback(function(id,obj){return function(){
if (l(id))
{
if (obj.tooltip) G.addTooltip(l(id),function(){return obj.tooltip;},{offY:-8});
else if (obj.tooltipFunc) G.addTooltip(l(id),obj.tooltipFunc,{offY:-8});
if (obj.onclick) l(id).onclick=obj.onclick;
}
}}(id,obj));
}
G.buttonsN++;
return str;
}
G.fieldN=0;
G.field=function(obj)
{
//returns a string for a new text input field; creates a callback that must be applied after the html has been created, with G.addCallbacks()
//obj can have text (the default value), min and max (character length limits), tooltip (text that shows on hover), oninput (function executed when text is entered in the field), onclick (function executed when field is clicked), classes (CSS classes added)
var id=G.fieldN;
if (!obj.textarea) var str='';
else var str='';
if (obj.onclick || obj.tooltip || obj.oninput || obj.select)
{
G.pushCallback(function(id,obj){return function(){
if (obj.tooltip) G.addTooltip(l('field-'+id),function(){return obj.tooltip;},{offY:-8});
if (obj.onclick) l('field-'+id).onclick=obj.onclick;
if (obj.oninput) l('field-'+id).oninput=function(e){obj.oninput(e.target.value);};
if (obj.select) l('field-'+id).select();
}}(id,obj));
}
G.fieldN++;
return str;
}
G.textarea=function(obj)
{
//returns a string for a new text input area; much the same as G.field
obj.textarea=true;
return G.field(obj);
}
G.arbitraryCallback=function(func)
{
G.pushCallback(func);
}
G.textN=0;
G.textE=0;
G.textWithTooltip=function(text,tooltip,classes)
{
//returns a string for a span of text with a tooltip; creates a callback that must be applied after the html has been created, with G.addCallbacks()
var id=G.textN;
var str=''+text+'';
G.pushCallback(function(id,tooltip){return function(){
G.addTooltip(l('textspan-'+id),function(){return tooltip;},{offY:-8});
}}(id,tooltip));
G.textN++;
return str;
}
G.clickableText=function(text,func,classes)
{
//returns a string for a span of text that triggers a function on click; creates a callback that must be applied after the html has been created, with G.addCallbacks()
var id=G.textN;
var str=''+text+'';
G.pushCallback(function(id,func){return function(div){
l('clickabletextspan-'+id).onclick=func;
}}(id,func));
G.textN++;
return str;
}
G.deleteSelfUpdatingTexts=function()
{
var divs=document.getElementsByClassName('updatabletextspan');
for (var i=0;i'+func()+'';
G.pushCallback(function(id,func){return function(){
G.updateTextTimer(id,func);
}}(id,func));
G.textN++;
return str;
}
/*=====================================================================================
NAMES
=======================================================================================*/
//stuff that can be set by the player like ruler name, country name and so on
G.names=[];
G.getName=function(name,fallback){if (!G.names[name]) return fallback; else return G.names[name];}
G.setName=function(name,val,fallback){if (!val) {val=fallback;}G.names[name]=val.replaceAll('<','<').replaceAll('>','>');}
G.getSafeName=function(name,fallback){if (!G.names[name]) return fallback; else return '"'+G.names[name].replaceAll('"','"')+'"';}//okay for saving
G.setSafeName=function(name,val,fallback){if (!val) {val=fallback;}G.names[name]=val.replaceAll('<','<').replaceAll('>','>');}//okay for loading
/*=====================================================================================
TABS
=======================================================================================*/
G.tabs=
[
//div : which div to empty+hide or display when tab is toggled
//update : which system's update to call when toggling on
{name:'Production',id:'unit',update:'unit',desc:'Recruit units and create buildings.'},
{name:'Territory',id:'land',update:'land',showMap:true,desc:'View the world map, inspect explored territory and see your natural resources.'},
//{name:'Diplomacy',id:'diplo',showMap:true,desc:'View and interact with other civilizations; conduct trade and send armies.'},//later
{name:'Policies',id:'policy',update:'policy',desc:'Use your influence to enact policies that change the way your civilization functions.'},
{name:'Traits',id:'trait',update:'trait',desc:'View traits and edit your civilization\'s properties.'},
{name:'Research',id:'tech',update:'tech',desc:'Purchase new technologies that improve your civilization and unlock new units.'},
{name:'Settings',id:'settings',popup:true,addClass:'right',desc:'Change the game\'s settings.'},
{name:'Update log',id:'updates',popup:true,addClass:'right',desc:'View the game\'s version history and other information.'},
{name:'Legacy',id:'legacy',popup:true,addClass:'right',desc:'View your legacy stats and achievements.'}
];
for (var i=0;i';
str+='';
str+='';
for (var i in G.tabs)
{G.tabs[i].div=G.tabs[i].id+'Div';str+='';}
l('sections').innerHTML=str;
G.buildMapDisplay();
var str='';
for (var i in G.tabs)
{str+='
'+G.tabs[i].name+'
';}
l('sectionTabs').innerHTML=str;
for (var i in G.tabs)
{
G.tabs[i].l=l('tab-'+G.tabs[i].id);
G.tabs[i].l.onclick=function(tab){return function(){G.setTab(tab);};}(G.tabs[i]);
if (G.tabs[i].desc) G.addTooltip(G.tabs[i].l,function(tab){return function(){return tab.desc;};}(G.tabs[i]),{offY:-8});
}
G.setTab(G.tabs[0]);
}
G.setTab=function(tab)
{
if (tab.popup)
{
if (G.getSetting('animations')) triggerAnim(tab.l,'plop');
G.dialogue.popup(G.tabPopup[tab.id],'bigDialogue',tab.l);
}
else
{
G.tab=tab;
G.settingsByName['tab'].value=G.tab.I;
for (var i in G.tabs)
{
var me=G.tabs[i];
if (me.id!=tab.id)//close other tabs
{
me.l.classList.remove('on');
me.l.classList.remove('bgLight');
me.l.classList.add('bgMid');
if (me.div) {l(me.div).style.display='none';l(me.div).innerHTML='';}
}
else//update focused tab
{
me.l.classList.add('on');
me.l.classList.remove('bgMid');
me.l.classList.add('bgLight');
if (me.div) l(me.div).style.display='block';
if (me.update) G.update[me.update]();
if (G.getSetting('animations')) triggerAnim(me.l,'plop');
}
}
if (tab.showMap) G.showMap();
else G.hideMap();
G.particlesReset();
}
}
/*=====================================================================================
UPDATE LOG
=======================================================================================*/
G.updateLogPreface='
NeverEnding Legacy is a game by Orteil and Opti. It is currently in early alpha, may feature strange and exotic bugs, and may be updated at any time. (Please don\'t get too attached to your saves.)
'+
'
Updates will most likely only affect your game after you ascend or reset to a new legacy.
'+
'
The long term
'+
'
The idea is to keep releasing incremental updates that add new technological eras or expand on existing features. This may take a while.
';
G.updateLog=UPDATELOG;
G.tabPopup['updates']=function()
{
var str='';
str+=G.updateLogPreface;
str+='
Update log :
';
var n=G.updateLog.length;
for (var i=n-1;i>=0;i--)
{
var me=G.updateLog[i];
str+='
'+me.date+' : '+me.title+'
';
var len=me.text.length;
for (var ii=0;ii'+me.text[ii]+'';
}
}
str='
About
'+str+'
';
str+='
'+
G.dialogue.getCloseButton()+
'
';
return str;
}
/*=====================================================================================
SETTINGS
=======================================================================================*/
/*
example use :
str=G.writeSettingButton({id:'myButton',name:'name-of-the-setting-to-affect',text:'Text that will be on the button',tooltip:'Tooltip to display on button hover',value:(value this button sets the setting to, not required for toggle settings),siblings:[list of ids of other linked buttons as strings (like 'myButton2','myButton3'...) which will be toggled off when this button is toggled on]});
don't forget to use G.addCallbacks() once the str has been added to the html
*/
G.settings=[
{name:'mapEditMode',type:'int',def:0,onChange:function(){
G.editMode=(G.getSetting('mapEditMode'));
G.mapEditWithLand=0;
var div=l('tileEditButton');
if (div && G.editMode==2)
{
div.style.display='block';
if (G.getSetting('animations')) triggerAnim(div,'plop');
if (G.land[G.mapEditWithLand])
{
div.style.background=G.getLandIconBG(G.land[G.mapEditWithLand]);
div.style.backgroundPosition=G.getLandIconBGpos(G.land[G.mapEditWithLand]);
}
}
else if (div) div.style.display='none';
}},//what are we doing to the map?
{name:'paused',type:'toggle',def:0,onChange:function(){G.updateSpeedButtons();}},//is the game currently paused?
{name:'fast',type:'toggle',def:1,onChange:function(){G.updateSpeedButtons();}},//is the game currently on fast speed?
{name:'forcePaused',type:'toggle',def:0,onChange:function(){G.updateSpeedButtons();}},//force pause when on
{name:'tab',type:'int',def:0,onChange:function(){}},//current tab
{name:'showLeads',type:'int',def:0,onChange:function(){}},//show what any given tech or trait will lead to (kinda cheaty/cumbersome)
{name:'pauseOnMenus',type:'toggle',def:1,onChange:function(){}},//pause when in menus
{name:'atmosphere',type:'toggle',def:1,onChange:function(){}},//show atmospheric messages
{name:'particles',type:'toggle',def:1,onChange:function(){}},//show particles
{name:'animations',type:'toggle',def:1,onChange:function(){if (G.getSetting('animations')) G.wrapl.classList.add('animationsOn'); else G.wrapl.classList.remove('animationsOn');}},//show animations ("plops" and blue squares)
{name:'filters',type:'toggle',def:1,onChange:function(){if (G.getSetting('filters')) G.wrapl.classList.add('filtersOn'); else G.wrapl.classList.remove('filtersOn');}},//use CSS filters
{name:'fpsgraph',type:'toggle',def:1,onChange:function(){if (G.getSetting('fpsgraph')) {G.fpsGraph.style.display='block';l('fpsCounter').style.display='block';} else {G.fpsGraph.style.display='none';l('fpsCounter').style.display='none';}}},//show fps graph
{name:'debug',type:'toggle',def:0,onChange:function(){if (G.getSetting('debug')) G.wrapl.classList.add('debugOn'); else G.wrapl.classList.remove('debugOn');}},//cheaty debug mode
{name:'showAllRes',type:'toggle',def:0,onChange:function(){}},//see all resources
{name:'autosave',type:'toggle',def:1,onChange:function(){}},//game will save every minute
{name:'buyAny',type:'toggle',def:0,onChange:function(){}},//when bulk-buying, buy any amount up to the demanded amount instead of cancelling if we can't buy the demanded amount
{name:'tieredDisplay',type:'toggle',def:0,onChange:function(){if (l('techDiv')) G.update['tech']();}},//techs will be displayed as tiers instead of in the order they were researched
{name:'buyAmount',type:'int',def:1,onChange:function(){G.updateBuyAmount();}},//how many units we create/remove at once
];
G.settingsByName=[];
for (var i in G.settings){G.settingsByName[G.settings[i].name]=G.settings[i];}
G.getSetting=function(name){return G.settingsByName[name].value;}
G.setSetting=function(name,value)
{
var me=G.settingsByName[name];
me.value=value;
if (me.onChange) me.onChange();
}
G.resetSettings=function()
{
for (var i in G.settings)
{
G.settings[i].value=G.settings[i].def;
if (G.settings[i].onChange) G.settings[i].onChange();
}
}
G.writeSettingButton=function(obj)
{
G.pushCallback(function(obj){return function(){
var div=l('settingButton-'+obj.id);
if (div)
{
var me=G.settingsByName[obj.name];
var valueMatches=(!(typeof obj.value==='undefined') && me.value==obj.value);
var on=false;
if (me.type=='toggle' && me.value==true) on=true;
else if (valueMatches) on=true;
div.innerHTML=obj.text||me.name;
if (on) div.classList.add('on');
div.onclick=function(div,name,value,siblings){return function(){G.clickSettingButton(div,name,value,siblings);}}(div,obj.name,obj.value,obj.siblings);
if (obj.tooltip) G.addTooltip(div,function(str){return function(){return str;};}(obj.tooltip),{offY:-8});
}
}}(obj));
return '';
}
G.clickSettingButton=function(div,name,value,siblings)
{
var me=G.settingsByName[name];
var newValue=value;
if (me.type=='toggle') newValue=!me.value;
if (!(typeof newValue==='undefined'))
{
G.setSetting(name,newValue);
}
if (div)
{
var valueMatches=(!(typeof value==='undefined') && me.value==value);
var on=false;
if (me.type=='toggle' && me.value==true) on=true;
else if (valueMatches) on=true;
if (on) div.classList.add('on'); else div.classList.remove('on');
if (siblings)
{
for (var i in siblings)
{
if (('settingButton-'+siblings[i])!=div.id)
{l('settingButton-'+siblings[i]).classList.remove('on');}
}
}
}
}
G.tabPopup['settings']=function()
{
var str='';
str+='
Settings
';
str+='
'+
(G.sequence=='main'?
(
'
Save file
'+
'
'+
G.button({text:'View loaded mods',tooltip:'Check which mods are currently active on this game.',onclick:function(){G.dialogue.popup(function(div){
var str='';
str='
Loaded mods
You can change mods when you start a new game.
';
for (var i in G.mods)
{
var mod=G.mods[i];
str+='
'+G.textWithTooltip(mod.name,'
'+mod.name+'
'+(mod.author?('
by '+mod.author+'
'):'')+'
URL : '+mod.url+'
'+(mod.desc?(mod.desc):'')+'
')+'
';
}
str+='
'+
'
'+
G.dialogue.getCloseButton()+
'
';
return str;
});}})+
' '+
G.button({text:'Wipe save',tooltip:'Clear your save completely, removing your current game and any achievements and game data. Cannot be undone!',style:'box-shadow:0px 0px 2px 1px #f00;',onclick:function(){G.dialogue.popup(function(div){
return '
Are you really sure you want to delete your save file?
'+G.button({text:'Yes!',onclick:function(){G.Clear();G.middleText('- Save wiped -');G.dialogue.close();}})+G.button({text:'No!',onclick:function(){G.dialogue.close();}})+'
';
});}})+
'
'+
G.button({text:'New game',tooltip:'Abandon your current game and start a new one.',onclick:function(){G.dialogue.popup(function(div){
return '
Are you sure you want to start a new game? You will have to start over (but you will keep your stats and achievements).
';
});}})+
G.button({text:'Save game',tooltip:'Save your current game. You can also save at any time with ctrl+S.',onclick:function(){G.Save();}})+
' '+
G.button({text:'Load from file',tooltip:'Load a game from an external save file.',onclick:function(){}})+
G.button({text:'Save to file',tooltip:'Save the game to an external save file. Use this to keep backups of your save on your computer.',onclick:function(){G.FileSave();}})+
'
'
)
:'')+
'
Gameplay
'+
G.writeSettingButton({id:'pauseOnMenus',name:'pauseOnMenus',text:'Pause in menus',tooltip:'Time will stop while in a menu or prompt.'})+
G.writeSettingButton({id:'buyAny',name:'buyAny',text:'Buy any amount',tooltip:'When this is on, bulk-buying a unit (by shift-clicking it) will buy as many as you can, up to 50; if this is off, it will only bulk-buy if you can buy all 50 at once.'})+
G.writeSettingButton({id:'atmosphere',name:'atmosphere',text:'Show atmospheric messages',tooltip:'Turn ambience flavor text in messages on/off.'})+
G.writeSettingButton({id:'autosave',name:'autosave',text:'Autosave',tooltip:'Turn autosaving every 60 seconds on/off.'})+
'
Graphics
'+
'
Turning these off may improve performance.
'+
G.writeSettingButton({id:'particles',name:'particles',text:'Particles',tooltip:'Turn resource particles on/off.'})+
G.writeSettingButton({id:'animations',name:'animations',text:'Animations',tooltip:'Turn interface animations on/off.'})+
G.writeSettingButton({id:'filters',name:'filters',text:'CSS filters',tooltip:'Turn fancy CSS filters on/off. Includes effects such as icon shadows, blurring and brightness adjustments.'})+
'
Misc.
'+
G.writeSettingButton({id:'fpsgraph',name:'fpsgraph',text:'Show fps',tooltip:'Display the frames per second graph.'})+
'
';
str+='
'+
G.dialogue.getCloseButton()+
'
';
return str;
}
/*=====================================================================================
USEFUL STUFF
=======================================================================================*/
G.checkReq=function(req)
{
//run through a list of requirements and return true if all match
var success=true;
for (var i in req)
{
var found=false;
if (G.getDict(i).type=='policy')
{
var policy=G.getDict(i);
if (policy.visible && policy.mode.id==req[i]) {}
else return false;
//else {success=false;}
}
else
{
if (G.has(i)) found=true;
if (found && req[i]==true) {}
else if (!found && req[i]==false) {}
else return false;
//else {success=false;}
}
}
return success;
}
G.has=function(what)
{
//return true if we have at least 1 of the tech, trait or unit
var me=G.getDict(what);
var type=me.type;
if (type=='tech' && G.techsOwnedNames.includes(what)) return true;
else if (type=='trait' && G.traitsOwnedNames.includes(what)) return true;
else if (type=='unit' && G.unitsOwnedNames.includes(what)) return true;
//NOTE : actually returns true for units if they're visible on the production screen at all
return false;
}
G.debugInfo=function(me)
{
return '
'
}
//the icon functions are a bit of a redundant mess honestly
G.getIconClasses=function(me,allowWide)
{
//returns some CSS classes
var str='';
if (me.wideIcon && allowWide) str+=' wide3';
else str+=' wide1';
return str;
}
G.getIconStr=function(me,id,classes,allowWide)
{
//returns a DOM string
var icon=G.getIconUsedBy(me,allowWide);
return '';
}
G.getIconUsedBy=function(me,allowWide)
{
//returns a style string for something's icon
var icon=me.icon||[0,0];
if (allowWide && me.wideIcon) icon=me.wideIcon;
if (me.getIcon) icon=me.getIcon(me);
return G.getIcon(icon);
}
G.getIcon=function(icon,split)
{
//returns a style string
/*examples for the icon parameter :
[1,2] - returns the icon at 1,2 in the default spritesheet
[1,2,3,4] - returns the icon at 1,2, overlaid on top of the icon at 3,4 in the default spritesheet
[1,2,"mySheet"] - returns the icon at 1,2 in the custom spritesheet with id "mySheet"
[1,2,"mySheet",3,4] - returns the icon at 1,2 in the custom spritesheet with id "mySheet" overlaid on top of the icon at 3,4 in the default spritesheet
*/
var bg=[];
var bgP=[];
var bit=[];
for (var i=0;i2) return 'background-position:'+(-icon[0]*24*G.iconScale)+'px '+(-icon[1]*24*G.iconScale)+'px,'+(-icon[2]*24*G.iconScale)+'px '+(-icon[3]*24*G.iconScale)+'px;';
return 'background-position:'+(-icon[0]*24*G.iconScale)+'px '+(-icon[1]*24*G.iconScale)+'px;';*/
}
G.getFreeformIcon=function(x,y,w,h)
{
//returns a style string
return 'background-position:'+(-x*G.iconScale)+'px '+(-y*G.iconScale)+'px;width:'+(w)+'px;height:'+(h)+'px;';
}
G.setIcon=function(div,icon)
{
//updates an existing DOM element's icon
div.style.cssText=G.getIcon(icon);
if (icon.length>2) div.classList.add('double'); else div.classList.remove('double');
}
G.getArbitraryIcon=function(icon,clipped,id)
{
return '
';
}
//dictionary : everything is stored in here by name
//handy for getting something without knowing its type
//sends a warning when trying to declare something with a duplicate name
G.dict=[];
G.setDict=function(name,what)
{
if (G.dict[name]) {console.log('WARNING : there is already something with the id "'+name+'".');return false;}
else {G.dict[name]=what;return true;}
}
G.getDict=function(name)
{
if (!G.dict[name]) ERROR('Nothing exists with the name '+name+'.');
else if (G.dict[name].type=='res') return G.resolveRes(G.dict[name]); else return G.dict[name];
}
G.getRawDict=function(name)
{
if (!G.dict[name]) ERROR('Nothing exists with the name '+name+'.');
else return G.dict[name];
}
G.resolveRes=function(res){if (res.replacement) return G.resolveRes(G.dict[res.replacement]); else return res;}
G.setTile=function(x,y,what)
{
G.currentMap.tiles[x][y].land=G.land[what];
}
G.getSmallThing=function(what,text)
{
return '
'+G.getIconStr(what)+'
'+(text=='*PLURAL*'?(what.displayName+'s'):(text||what.displayName||'!'))+''
}
G.getBrokenSmallThing=function(what,text)
{
return ''+(text=='*PLURAL*'?(what+'s'):(text||what))+''
}
G.parseFunc=function(str)
{
str=str.substring(1,str.length-1);
var parts=str.split(',');
var keyword=parts[0];
parts.shift();
var val=parts.join(',');
var exact=false;
if (keyword.charAt(0)=='#')
{
exact=true;
keyword=keyword.substring(1,keyword.length);
}
//str='['+keyword+' (not defined yet)]';
if (exact && G.getRawDict(keyword)) str=G.getSmallThing(G.getRawDict(keyword),val);
else if (!exact && G.getDict(keyword)) str=G.getSmallThing(G.getDict(keyword),val);
else str=G.getBrokenSmallThing(keyword,val);
return str;
}
G.parse=function(what)
{
/*
Syntax :
-[fruit] will display the fruit resource in bold with a small icon (also works for any other element registered in the dictionary)
-[fruit]s will do the same, but add an "s" at the end
-[fruit,Apples] will display the fruit resource but the name will be replaced with "Apples"
-[#fruit] will display the fruit resource, even if it has a replacement which should be displayed instead
-// will create a new paragraph
-@ will create a bullet list point
-<> will create a full-width divider
*/
var str='
';
return str;
}
G.getCostString=function(costs,verbose,neutral,mult)
{
//returns a string that displays resource costs with icons and amount; the amounts will be red if our current resources don't match them, unless neutral is set to true; only the amount will be displayed unless verbose is true, in which case the amount and the resource name will be displayed; costs will be multiplied by mult if specified
var costsStr=[];
mult=mult||1;
for (var i in costs)
{
var thing=G.getDict(i);
var text=B(costs[i]*mult)+(verbose?(' '+thing.displayName):'');
if (thing.amount';
costsStr.push(G.getSmallThing(thing,text));
}
return costsStr.join(', ');
}
G.getUseString=function(costs,verbose,neutral,mult)
{
//same as above, but takes into account the unused amount of a usable resource instead of its total amount
var costsStr=[];
mult=mult||1;
for (var i in costs)
{
var thing=G.getDict(i);
var text=B(costs[i]*mult)+(verbose?(' '+thing.displayName):'');
if ((thing.amount-thing.used)';
costsStr.push(G.getSmallThing(thing,text));
}
return costsStr.join(', ');
}
G.getLimitString=function(costs,verbose,neutral,amount)
{
//same as above, but for limits
var costsStr=[];
amount=amount||1;
for (var i in costs)
{
var thing=G.getDict(i);
var text='1 per '+B(costs[i])+(verbose?(' '+thing.displayName):'');
if (((thing.amount+costs[i])/amount)<=costs[i] && !neutral) text=''+text+'';
costsStr.push(G.getSmallThing(thing,text));
}
return costsStr.join(', ');
}
G.updateEverything=function()
{
for (var i in G.update)
{
G.update[i]();
}
G.updateMapDisplay();
}
//callbacks system : basically we have functions that return HTML but also add a callback to the callbacks array; after the HTML has been added to the DOM we call G.addCallbacks() to apply all the pending callbacks - this lets us centralize HTML and callbacks in one function
G.Callbacks=[];
G.addCallbacks=function()
{
var len=G.Callbacks.length;
for (var i=0;iCan\'t do that when paused!');
}
G.BT=function(value)//value is in game days
{
//give a Beautified Time string for the given value
value=Math.max(Math.ceil(value,0));
var years=Math.floor(value/300);
value-=years*300;
var days=Math.floor(value);
var bits=[];
if (years) bits.push(B(years)+' year'+(years==1?'':'s'));
if (days || bits.length==0) bits.push(B(days)+' day'+(days==1?'':'s'));
return bits.join(', ');
}
/*=====================================================================================
PARTICLES
=======================================================================================*/
G.particlesInit=function()
{
G.particles=[];
G.particlesI=0;
G.particlesN=100;
var str='';
for (var i=0;i';
}
l('particlesAnchor').innerHTML=str;
for (var i=0;iG.h-102 || obj.y<26)) return 0;//cull if on black interface
var me=G.particles[G.particlesI];
me.x=0;
me.y=0;
me.lm=0;
me.icon=[0,0];
me.type=0;
for (var i in obj) {me[i]=obj[i];}
me.on=true;
me.l=0;
if (me.type==0)
{
me.x+=Math.random()*32-16;
me.xd=Math.random()*4-2;
me.yd=-(Math.random()*2+1);
me.lm=me.lm||30;
//me.icon=choose(G.res).icon;
me.el.style.transform='translate('+(me.x-12)+'px,'+(me.y-12)+'px)';
var iconStr=G.getIcon(me.icon,true);
me.el.style.background=iconStr[0];
me.el.style.backgroundPosition=iconStr[1];
//me.el.style.backgroundPosition=(-me.icon[0]*24*G.iconScale)+'px '+(-me.icon[1]*24*G.iconScale)+'px';
me.el.style.display='block';
}
G.particlesI++;
if (G.particlesI>=G.particlesN) G.particlesI=0;
}
G.logic['particles']=function()
{
for (var i=0;ime.lm) {me.on=false;me.el.style.display='none';}
}
}
}
G.draw['particles']=function()
{
for (var i=0;i'+text+'';
//l('middleText').innerHTML='
'+text+'
';
triggerAnim(l('middleText'),'slowFadeOut');
if (slow) l('middleText').style.animationDuration='5s';
else l('middleText').style.animationDuration='1.5s';
}
/*=====================================================================================
MESSAGES
=======================================================================================*/
G.messages=[];
G.maxMessages=50;
G.Message=function(obj)
{
//syntax :
//G.Message({type:'important',text:'This is a message.'});
//.type is optional
var me={};
me.type='normal';
for (var i in obj) {me[i]=obj[i];}
var scrolled=!(Math.abs(G.messagesWrapl.scrollTop-(G.messagesWrapl.scrollHeight-G.messagesWrapl.offsetHeight))<3);//is the message list not scrolled at the bottom? (if yes, don't update the scroll - the player probably manually scrolled it)
me.date=G.year*300+G.day;
var text=me.text||me.textFunc(me.args);
var mergeWith=0;
if (me.mergeId)
{
//this is a system where similar messages merge together if they're within 100 days of each other, in order to reduce spam
//simply declare a .mergeId to activate merging on this message with others like it
//syntax :
//var cakes=10;G.Message({type:'important',mergeId:'newCakes',textFunc:function(args){return 'We\'ve baked '+args.n+' new cakes.';},args:{n:cakes}});
//numeric arguments will be added to the old ones unless .replaceOnly is true
for (var i in G.messages)
{
var other=G.messages[i];
if (other.id==me.mergeId && me.date-other.date<100) mergeWith=other;
}
me.id=me.mergeId;
}
if (mergeWith)
{
me.date=other.date;
if (me.replaceOnly)
{
for (var i in me.args)
{mergeWith.args[i]=me.args[i];}
}
else
{
for (var i in me.args)
{
if (!isNaN(parseFloat(me.args[i]))) mergeWith.args[i]+=me.args[i];
else mergeWith.args[i]=me.args[i];
}
}
text=me.textFunc(mergeWith.args);
}
var str='
';
if (mergeWith) mergeWith.l.innerHTML=str;
else
{
var div=document.createElement('div');
div.innerHTML=str;
div.className='message popInVertical '+(me.type).replaceAll(' ','Message ')+'Message';
G.messagesl.appendChild(div);
me.l=div;
G.messages.push(me);
if (G.messages.length>G.maxMessages)
{
var el=G.messagesl.firstChild;
for (var i in G.messages)
{
if (G.messages[i].l==el)
{
G.messages.splice(i,1);
break;
}
}
G.messagesl.removeChild(el);
//G.messages.pop();
//G.messagesl.removeChild(G.messagesl.firstChild);
}
if (!scrolled) G.messagesWrapl.scrollTop=G.messagesWrapl.scrollHeight-G.messagesWrapl.offsetHeight;
}
G.addCallbacks();
}
G.initMessages=function()
{
G.messages=[];
G.messagesl=l('messagesList');
G.messagesWrapl=l('messages');
G.messagesl.innerHTML='';
}
G.updateMessages=function()
{
}
/*=====================================================================================
LANGUAGE
A system for translation into pseudo-languages.
This system takes in a language object, composed of word starts, middles, jointers and ends, and an input text, and outputs that text "translated" into the language.
The translation should always return the same output for the same word.
=======================================================================================*/
//http://symbolcodes.tlt.psu.edu/web/codehtml.html
G.languages={
'primitive':{
name:'Primitive',
starts:['g','gr','gn','m','r','b','br','k','kr','z','h','d','th','thr','ob','ok','ork','ak','ark'],
mids:['a','a','a','a','a','a','o','o','o','o','o','o','i','i','e','e','u','u','y',/**/'oo','oh','aa','ah','ü','üü','ö','öö'],
joints:['nk','z','r','rb','rh','d','m','b','h','n','nd','mb','k','kt','lk','st','k\'t','g\'h'],
ends:['k','k','k','k','g','g','g','r','r','r','ko','nko','nka','mbo','mba','rk','nk','do','dia','kko','tta','tto','tia','t','th','b','l','ll','n','m','x','rx','rg'],
},
'english':{
name:'Brittanoid',
starts:[/*common*/'d','m','n','b','g','l','p','f','v','w','d','m','n','b','g','l','p','f','v','w','d','m','n','b','g','l','p','f','v','w',/**/'th','tr','thr','gr','cr','cl','br','bl','fl','fr','ar','or','wr','h','sc','sh','ch','wh','wh','wh','dr','st','str','squ','pl','pr','y'],
mids:[/*common*/'a','a','a','a','a','o','o','o','o','i','i','i','i','e','e','e','e','e',/**/'oo','ee','ea','io','ie','ei','iou','u','au','ai','ou','y'],
joints:['l','t','cr','ct','rm','tr','s','r','rs','pt','g','gg','b','h','ll','ls','th','gn','nc','ns','nd','rst','v','lv','ght','ghb','rb','bd','ncl','bg','lt','st','qu','rt','lb','gl','ff','fr','fl','mb','x'],
ends:['k','ck','s','ss','sk','m','n','nt','nk','nks','ng','ng','ng','ngs','le','ne','me','de','t','tt','ll','rp','p','r','re','d','w','l','ble','nkle','ttle','ggle','the','te','ve','gh','cks','tch','rch','nch','rse','sh','rt','rst','rsty','rty','rm','rf','pt','nny','se','ce','ge','nce','nge','ngth','rk','key','ky','sy','ry','ty','ly','py','lly','ff','t\'s','\'s','x','zz'],
},
'french':{
name:'Frankoid',
starts:['b','j','g','d','h','p','m','n','ad','ab','fr','fl','ch','f','c','ph','qu','gr','tr','l','gl','dr','cl','cr','br','bl','pr','pl','t','r','s','sc','l\'','s\'','qu\'','él','ét','étr'],
mids:['a','a','a','o','o','i','i','e','e','e','u','u','ai','au','ou','oi','ui','ie','à','è','é','ê','â','û','ô'],
joints:['l','t','c','cr','cc','ss','ct','tr','rs','rt','ff','fr','fl','pt','pht','s','r','g','ll','gn','nc','ns','nd','ls','dm','mb','mbl','md','mpt','ng','mm','nn','v','lv','rb','bd','lt','lb','st','mb','mbr','qu','ç'],
ends:['nt','nd','nte','nde','m','n','le','ne','me','de','rti','rtie','r','t','te','té','sé','tte','tre','che','cru','gru','gt','que','sque','n','rd','rde','rt','rte','s','se','fe','re','phe','d','l','gne','tion','bli','pi','pie','rme','ble','tions','lier','lière','bles','teau','telle','peau','pelle','meau','melle','ste','nse','nce','rs','ng','nge','x','z'],
},
'japanese':{
name:'Nipponoid',
starts:['ak','al','as','ik','is','its','ot','ok','k','s','t','n','h','m','w','d','p','j','ch','z','r','sh','g','ts'],
mids:[/*common*/'a','a','a','o','o','o','u','u','i','i','e','e','a','a','a','o','o','o','u','u','i','i','e','e',/**/'in','ou','ai','ao','ii','ei','yo','ya','yu','aa'],
joints:['t','k','s','b','n','m','g','z','sh','j','p','d','h','kk','ch','ts','ht'],
ends:['ko','ka','ki','ku','ke','mo','ma','mi','mu','me','no','na','ni','nu','ne','to','ta','ti','tu','te','ro','ra','ri','ru','re','jo','ja','ji','ju','je','do','da','di','du','de','tso','tsa','tsi','tsu','tse','n','n','n','n','wa','sai','chi','jio'],
},
'grecoroman':{
name:'Grecoromanoid',
starts:['m','n','x','g','gn','p','tr','t','gr','ad','ap','agr','atr','ant','ambr','arthr','pr','cl','chl','kl','st','sp','sk','skl','skr','ov','om','omb','onth','on','v','l','k','h','d','s','int','inc','in','fr','gl','pt','pht','aut','aud','ur','ult','exp','extr','ext'],
mids:['a','e','i','o','u','y','io','ia','iu','ae','eu'],
joints:['th','l','ll','nt','t','thr','tr','st','sk','skl','skr','gr','ngr','ntr','nth','v','g','gg','c','cc','k','s','x','d','cl','ct','kl','r','fr','pt','pht','mb','mbr'],
ends:[/*common*/'n','s','m','th','d','n','s','m','th','sma','rma','d','n','s','m','th','d','n','s','m','th','d',/**/'x','na','nus','nis','sa','sia','sus','sis','ta','tia','tius','tis','ga','gia','gius','gis','la','lia','lius','lis'],
},
};
G.translate=function(input,languages,seed)
{
var starts=[];
var mids=[];
var joints=[];
var ends=[];
var seed=seed||'0';
if (!languages) {languages=[];for (var i in G.languages) {languages.push(i);}}
for (var i in languages)
{
var language=G.languages[languages[i]];
starts.push(language.starts);
mids.push(language.mids);
joints.push(language.joints);
ends.push(language.ends);
}
var output='';
input=decodeEntities(input);
input+=' ';
var len=input.length;
var word='';
var endWord=false;
for (var i=0;i0)
{
//if we reached the end of a word, process it; chop it into chunks of 4 letters and translate each chunk
var len2=Math.ceil(word.length/4);
var bits=[];
var balance=false;
//if (len2%3==2) balance=true;//if the last chunk is only 1 letter, add 1 to it and remove 1 from the first chunk
for (var ii=0;ii2 && Math.random()<0.5) bit=choose(start)+choose(mid)+choose(end);//longish singles
else if (ii==0 && ii==len2-1) bit=choose([choose(mid),choose(start)+choose(mid),choose(mid)+choose(end)]);//singles
else if (ii==0) bit=choose(start)+choose(mid);//first part
else if (ii==len2-1) bit=choose(end);//end
else bit=choose(joint)+choose(mid);//middles
bit=decodeEntities(bit);
if (bits[ii].charAt(0).toUpperCase()==bits[ii].charAt(0)) bit=bit.charAt(0).toUpperCase()+bit.slice(1);
output+=bit;
}
word='';
}
if (endWord) {output+=thisChar;word='';endWord=false;}
}
output=output.slice(0,-1);
return output;
}
G.getRandomString=function(syllables,maxSyllables)
{
if (!maxSyllables) var maxSyllables=syllables;
syllables=Math.floor(Math.random()*(maxSyllables-syllables)+syllables);
var vow=['a','e','i','o','u','y'];
var cons=['b','c','d','f','g','h','j','k','l','m','n','p','q','r','s','t','v','w','x','z'];
var str='';
for (var i=0;i');
}
}
/*=====================================================================================
RESOURCES
=======================================================================================*/
G.res=[];
G.resByName=[];
G.getRes=function(name){if (!G.resByName[name]) ERROR('No resource exists with the name '+name+'.'); else return G.resolveRes(G.resByName[name]);}
G.getRawRes=function(name){if (!G.resByName[name]) ERROR('No resource exists with the name '+name+'.'); else return G.resByName[name];}
G.resCategories=[];
G.resInstances=[];//all resources displayed are actually instances of resources; this lets us have a resource be displayed multiple times (for instance in the regular display and in a pinned list) - note : these instances are not actually saved and are recreated with every filter change; amounts are part of the resource itself, not its instances
G.resN=0;//incrementer
G.Res=function(obj)
{
this.type='res';
this.amount=0;
this.used=0;//only used by some special resources (houses occupied, workers busy...); will only be handled and saved if the resource has .displayUsed=true
this.mult=1;//gain multiplier; all gains of this resource are multiplied by this; updated every tick
this.displayedAmount=0;//used to tick up the displayed number
this.displayedUsedAmount=0;//used to tick up the displayed number
this.startWith=0;
this.gained=0;//gained this tick
this.lost=0;//lost this tick
this.gainedBy=[];//filled by unit names and other processes that create this resource; emptied after every tick
this.lostBy=[];//filled by unit names and other processes that use up this resource; emptied after every tick
this.meta=false;//does this resource have subparts?
this.partOf=false;//is this resource a subpart of another resource? (a resource cannot be a subresource AND have subresources of its own)
this.subRes=[];//subresources if this is a meta-resource; handled automatically
this.tick=function(){};
this.getMult=function(){return 1;};
this.getDisplayAmount=function(){
if (this.displayUsed) return B(this.displayedUsedAmount)+'/'+B(this.displayedAmount);
else return B(this.displayedAmount);
};
this.category='';
this.icon=[0,0];
this.visible=false;//a resource will only be displayed if you've had some of the resource at some point (you can set .visible to force it to start visible; you can also set .hidden to override .visible)
for (var i in obj) this[i]=obj[i];
this.id=G.res.length;
if (!this.displayName) this.displayName=cap(this.name);
G.res.push(this);
G.resByName[this.name]=this;
G.setDict(this.name,this);
this.mod=G.context;
}
G.lose=function(what,amount,context)
{
if (amount<0) {return G.gain(what,-amount,context);}
//remove some amount from a resource; return how much we did manage to remove
var me=G.getRes(what);
if (me.replacement) me=G.getRes(me.replacement);
var removed=0;
if (amount>0)
{
if (me.meta)
{
var resAmount=0;
for (var i in me.subRes) {resAmount+=G.resolveRes(me.subRes[i]).amount;}
if (resAmount>0)
{
for (var i in me.subRes) {if (G.resolveRes(me.subRes[i]).amount>0) {G.lose(me.subRes[i].name,/*Math.round*/((G.resolveRes(me.subRes[i]).amount/resAmount)*amount),context);}}
}
removed+=Math.min(amount,resAmount);
}
else
{
var oldAmount=me.amount;
me.amount-=amount;
if (!me.fractional) me.amount=Math.max(0,randomFloor(me.amount));
removed=oldAmount-me.amount;
if (context && me.turnToByContext && me.turnToByContext[context])
{
for (var i in me.turnToByContext[context]) {G.gain(i,me.turnToByContext[context][i]*removed,(me.name==i?'-':me.displayName));}
}
if (context!='-') me.lost+=removed;
if (context && context!='-' && !me.lostBy.includes(context)) me.lostBy.push(context);
if (me.partOf)
{
var meta=G.getRes(me.partOf);
if (context!='-') meta.lost+=removed;
if (context && context!='-' && !meta.lostBy.includes(context)) meta.lostBy.push(context);
}
}
}
return removed;
}
G.gain=function(what,amount,context)
{
//add some amount to a resource; do not use on meta-resources
if (amount<0) {return G.lose(what,-amount,context);}
var me=G.getRes(what);
if (me.replacement) me=G.getRes(me.replacement);
if (amount>0)
{
if (me.meta)
{
return 0;
}
else
{
var oldAmount=me.amount;
me.amount+=amount*me.mult;
if (!me.fractional) me.amount=randomFloor(me.amount);
if (context!='-') me.gained+=me.amount-oldAmount;
if (context && context!='-' && !me.gainedBy.includes(context)) me.gainedBy.push(context);
if (me.partOf)
{
var meta=G.getRes(me.partOf);
if (context!='-') meta.gained+=me.amount-oldAmount;
if (context && context!='-' && !meta.gainedBy.includes(context)) meta.gainedBy.push(context);
}
}
}
}
G.getAmount=function(what)
{
//add some amount to a resource; do not use on meta-resources
var me=G.getRes(what);
if (me.replacement) me=G.getRes(me.replacement);
return me.amount;
}
G.testCost=function(costs,mult)
{
//can we afford the specified amount
var success=true;
for (var i in costs)
{
var cost=costs[i]*mult;
if (cost>0)
{
var res=G.getRes(i);
if (res.meta)
{
var resAmount=0;
for (var ii in res.subRes) {resAmount+=res.subRes[ii].amount;}
if (resAmount0)
{
var res=G.getRes(i);
if (res.meta)
{
var resAmount=0;
for (var ii in res.subRes) {resAmount+=res.subRes[ii].amount;}
if (n==-1) n=resAmount/cost; else n=Math.min(n,resAmount/cost);
}
else
{
if (n==-1) n=res.amount/cost; else n=Math.min(n,res.amount/cost);
}
}
}
n=Math.floor(n);
return n;
}
G.testUse=function(uses,mult)
{
//can we afford the specified amount
var success=true;
for (var i in uses)
{
var use=uses[i]*mult;
if (use>0)
{
var res=G.getRes(i);
var free=(res.amount-res.used);
if (res.used>res.amount) free=0;
if (free