in development
A Pen by Lovro Selic on CodePen.
in development
A Pen by Lovro Selic on CodePen.
<div id="preload" class="hidden"></div> | |
<div class="win" id="setup"> | |
<div id="load"> | |
</div> | |
<!--<a href="/Games/" title="Return to games' index"><img alt="Anxys" class="fr logo" src="/Images/Eyes.png" width="20" height="20"></a>--> | |
<div id="SC"></div> | |
<h1 id="title"></h1> | |
<p>text</p> | |
Hero's name: <input type = "text" id = "HeroName" value = "HERO" maxlength="10"/> | |
<p id="buttons"> | |
<input type='button' id='toggleHelp' value='Show/Hide Instructions'> | |
<input type='button' id='toggleAbout' value='About'> | |
</p> | |
<div id="help" class="section"> | |
<fieldset> | |
<legend> | |
Instructions: | |
</legend> | |
<p><strong>KEYS:</strong></p> | |
<p>Use cursor keys to move.</p> | |
<p>CTRL ... cast magic.</p> | |
<p>H ... use healing potion</p> | |
<p>M ... use mana potion</p> | |
<p>A, D ... move scroll selection cursor</p> | |
<p>TAB ... level up</p> | |
<p>ENTER ... cast selected scroll</p> | |
<p><strong>SCROLLS:</strong></p> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/Light.png" alt="Light" class="fl pic" title="Light"> | |
<p style="position: relative; top: 24px">Magic lamp</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/DrainMana.png" alt="DrainMana" class="fl pic" title="DrainMana"> | |
<p style="position: relative; top: 24px">Drain Mana: drains mana from all creatures in the area. Also yours!</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/Map.png" alt="Map" class="fl pic" title="Map"> | |
<p style="position: relative; top: 24px">Map: reveals the map of the exit area</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/BoostWeapon.png" alt="BoostWeapon" class="fl pic" title="BoostWeapon"> | |
<p style="position: relative; top: 24px">Increase the damage of your sword for the duration of the fight.</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/BoostArmor.png" alt="BoostArmor" class="fl pic" title="BoostArmor"> | |
<p style="position: relative; top: 24px">Increase your armor for the duration of the fight.</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/DestroyArmor.png" alt="DestroyArmor" class="fl pic" title="DestroyArmor"> | |
<p style="position: relative; top: 24px">Decrease your opponent's armor for the duration of the fight.</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/DestroyWeapon.png" alt="DestroyWeapon" class="fl pic" title="DestroyWeapon"> | |
<p style="position: relative; top: 24px">Decrease the damage of your opponent's sword for the duration of the fight.</p> | |
<p class="cb"></p> | |
</div> | |
<div> | |
<image src="https://www.c00lsch00l.eu/Games/AA/Invisibility.png" alt="Invisibility" class="fl pic" title="Invisibility"> | |
<p style="position: relative; top: 24px">Invisibility: I will let you figure this one by yourself..</p> | |
<p class="cb"></p> | |
</div> | |
</fieldset> | |
</div> | |
<div id="about" class="section"> | |
<fieldset> | |
<legend> | |
About: | |
</legend> | |
<image src="https://www.c00lsch00l.eu/Images/SoF.png" alt="Sword of Fargoal" class="fl pic" title="Sword of Fargoal"> | |
<p> 'Deep Down Into the Darkness' was inspired by C64 classic <a href="https://www.c64-wiki.com/wiki/Sword_of_Fargoal" target="_blank">Sword of Fargoal</a> from 1983, which was itself influenced by 1980 Unix game <a href="https://en.wikipedia.org/wiki/Rogue_(video_game)" | |
target="_blank">Rogue</a>.</p> | |
</fieldset> | |
</div> | |
<p class="version cb" id="version"></p> | |
</div> | |
<div id="game" class="winTrans"></div> | |
<div id="bottom" class="cb" style="margin-top: 720px"></div> | |
<div id="temp" class="hidden"></div> | |
<div id="temp2" class="hidden"></div> |
//console.clear(); | |
///////////////////////////////////////////////// | |
/* | |
to do: | |
MONSTER:Don't melee attack if mana available | |
Statistics on death | |
TEST. BALANCE | |
more scrolls, | |
new scroll ideas: | |
- | |
monster strategies | |
known bugs: | |
-can heal dead hero on fight form | |
*/ | |
//////////////////////////////////////////////////// | |
var DEBUG = { | |
frameCount: 0, | |
fog: false, | |
coord: false, | |
level: false, | |
viewMonsters: true, | |
addScrolls: function() { | |
let scrolls = [ | |
//{ type: "Light", use: "explore" }, | |
//{ type: "Invisibility", use: "explore" }, | |
//{ type: "Map", use: "explore" }, | |
//{ type: "DrainMana", use: "explore" }, | |
//{ type: "Cripple", use: "explore" }, | |
//{ type: "BoostWeapon", use: "fight" }, | |
//{ type: "BoostArmor", use: "fight" }, | |
//{ type: "DestroyArmor", use: "fight" }, | |
//{ type: "DestroyWeapon", use: "fight" } | |
]; | |
for (let q = 0, QL = scrolls.length; q < QL; q++) { | |
let selected = scrolls[q]; | |
let scroll = new Scroll(new Grid(0, 0), selected.type, selected.use); | |
HERO.scrolls.add(scroll); | |
//HERO.scrolls.add(scroll); | |
//HERO.scrolls.add(scroll); | |
} | |
//add potion2 | |
//let temp = new Potion("health", null); | |
//temp.exe(); | |
} | |
}; | |
var INI = { | |
LAST_LEVEL: 2, | |
GBpLVL: 10, | |
CpLVL: 10, | |
GBpDE: 2, | |
CpCRD: 5, | |
HPpLVL: 10, | |
MPpCRD: 9, | |
WpLVL: 2, | |
CHpLVL: 6, | |
LMPpLVL: 3, | |
SCRpLVL: 6, | |
Health_INC: 0.4, | |
EXP_KEY: 50, | |
MINI_PIX: 4, | |
TEMPLE_TIMEOUT: 10000, | |
EXP: 400, | |
MAGIC_EXP: 50, | |
EXP_FACTOR: 1.2, | |
MAGIC_EXP_FACTOR: 1.4, | |
PTS_LVL: 3, | |
LVL_HEALTH: 5, | |
LVL_MANA: 7, | |
MAGIC_FAIL: 1.5, | |
MAGIC_POWER_COST: 2, | |
ORB_MAX_RANGE: 10, | |
ENEMY_COMMON: 3, | |
ENEMY_START: 1, | |
ENEMY_KEY_ADD: 2, | |
ENEMY_END_ADD: 2, | |
ENEMY_TEMPLE: 1, | |
ENEMY_CORRIDOR: 25, | |
TRIGGER_WAKE: 11, | |
TRIGGER_VISION: 9, | |
SHOOT_TIMEOUT: 3000, | |
INVISIBILITY_TIME: 20, | |
MANA_DRAIN_RANGE: 10, | |
MAP_RADIUS: 5, | |
INITIATIVE_BONUS: 5, | |
ATTACk_OFFSET: -1, | |
AGILITY_TURN: 20, | |
FIGHT_PANEL_WIDTH: 200, | |
FLEE_AGILITY_DELTA: 5, | |
POINTS_ON_START: 4, | |
NEMESIS_RESPAWN: 900, | |
//NEMESIS_RESPAWN: 30, | |
STALK_DISTANCE: 3 | |
}; | |
var PRG = { | |
VERSION: "0.27.13.dev", | |
CSS: "color: #80f709", | |
NAME: "Deep Down Into the Darkness", | |
YEAR: 2019, | |
INIT: function() { | |
console.log("%c****************************", PRG.CSS); | |
console.log( | |
`%c${PRG.NAME} ${PRG.VERSION} by Lovro Selic, (c) C00lSch00l ${ | |
PRG.YEAR | |
} on ˘${navigator.userAgent}`, | |
PRG.CSS | |
); | |
$("#title").html(PRG.NAME); | |
$("#version").html( | |
PRG.NAME + | |
" V" + | |
PRG.VERSION + | |
" <span style='font-size:14px'>©</span> C00lSch00l 2019" | |
); | |
$("input#toggleAbout").val("About " + PRG.NAME); | |
$("#about fieldset legend").append(" " + PRG.NAME + " "); | |
ENGINE.readyCall = GAME.setup; | |
ENGINE.init(); | |
}, | |
setup: function() { | |
$("#toggleHelp").click(function() { | |
$("#help").toggle(400); | |
}); | |
$("#toggleAbout").click(function() { | |
$("#about").toggle(400); | |
}); | |
}, | |
start: function() { | |
console.log(`%c${PRG.NAME} started.`, PRG.CSS); | |
$("#startGame").addClass("hidden"); | |
var disableKeys = ["enter", "space", "tab", "back"]; | |
for (let key in disableKeys) ENGINE.disableKey(key); | |
//add boxes | |
if (!GAME.restarted) { | |
console.log("%cAdding boxes, setting ENGINE ....", PRG.CSS); | |
ENGINE.gameWIDTH = 768; | |
ENGINE.sideWIDTH = 1024 - ENGINE.gameWIDTH; | |
ENGINE.gameHEIGHT = 768; | |
ENGINE.titleHEIGHT = 80; | |
ENGINE.titleWIDTH = 1024; | |
ENGINE.bottomHEIGHT = 40; | |
ENGINE.bottomWIDTH = 1024; | |
ENGINE.checkProximity = false; | |
ENGINE.checkIntersection = false; | |
ENGINE.setCollisionsafe(49); | |
$("#bottom").css( | |
"margin-top", | |
ENGINE.gameHEIGHT + ENGINE.titleHEIGHT + ENGINE.bottomHEIGHT | |
); | |
$(ENGINE.gameWindowId).width(ENGINE.gameWIDTH + ENGINE.sideWIDTH + 4); | |
ENGINE.addBOX( | |
"TITLE", | |
ENGINE.titleWIDTH, | |
ENGINE.titleHEIGHT, | |
["title"], | |
null | |
); | |
ENGINE.addBOX( | |
"ROOM", | |
ENGINE.gameWIDTH, | |
ENGINE.gameHEIGHT, | |
[ | |
"background", | |
"coordview", | |
"grave", | |
"animation", | |
"actors", | |
"orbs", | |
"explosion", | |
"fogview", | |
"text" | |
], | |
"side" | |
); | |
ENGINE.addBOX( | |
"SIDE", | |
ENGINE.sideWIDTH, | |
ENGINE.gameHEIGHT, | |
["sideback", "status", "map", "time"], | |
"fside" | |
); | |
ENGINE.addBOX( | |
"DOWN", | |
ENGINE.bottomWIDTH, | |
ENGINE.bottomHEIGHT, | |
["bottom"], | |
null | |
); | |
ENGINE.addBOX( | |
"LEVEL", | |
ENGINE.gameWIDTH, | |
ENGINE.gameHEIGHT, | |
["floor", "wall", "config", "fog", "coord"], | |
null | |
); | |
if (!DEBUG.level) $("#LEVEL").addClass("hidden"); | |
} | |
GAME.start(); | |
} | |
}; | |
class Gold { | |
constructor(value, grid) { | |
this.name = "Gold"; | |
this.value = value; | |
this.grid = grid; | |
this.static = false; | |
if (value === 100) { | |
this.sprite = SPRITE.Gold; | |
} else this.sprite = SPRITE.Coin; | |
} | |
exe() { | |
HERO.gold += this.value; | |
TEXTPOOL.pool.push( | |
new TextSprite(this.value, GRID.gridToCoord(this.grid), "#DAA520") | |
); | |
TITLE.change(); | |
} | |
} | |
class Potion { | |
constructor(type, grid) { | |
this.name = "Potion"; | |
this.type = type; | |
this.grid = grid; | |
this.static = false; | |
switch (type) { | |
case "health": | |
this.sprite = SPRITE.RedPotion; | |
this.exe = () => { | |
HERO.redPotion++; | |
TITLE.change(); | |
}; | |
break; | |
case "magic": | |
this.sprite = SPRITE.BluePotion; | |
this.exe = () => { | |
HERO.bluePotion++; | |
TITLE.change(); | |
}; | |
break; | |
} | |
} | |
} | |
class Chest { | |
constructor(grid) { | |
this.name = "Chest"; | |
this.grid = grid; | |
this.sprite = SPRITE.Chest; | |
this.static = true; | |
let option = RND(1, 5); | |
switch (option) { | |
case 1: | |
case 2: | |
this.contains = new Gold(100, this.grid); | |
break; | |
case 3: | |
case 4: | |
let type = ["health", "magic"]; | |
this.contains = new Potion(type.chooseRandom(), this.grid); | |
break; | |
case 5: | |
let boosts = ["health", "mana", "weapon", "armor", "magic"]; | |
this.contains = new Boost(boosts.chooseRandom(), this.grid); | |
break; | |
} | |
} | |
exe() { | |
MAP[GAME.level].DUNGEON.chests.push(this.contains); | |
} | |
} | |
class Boost { | |
constructor(type, grid) { | |
this.name = "Boost"; | |
this.type = type; | |
this.grid = grid; | |
this.static = false; | |
switch (type) { | |
case "health": | |
this.sprite = SPRITE.Heart; | |
this.exe = () => { | |
HERO.maxHealth += INI.LVL_HEALTH; | |
HERO.health = HERO.maxHealth; | |
TITLE.change(); | |
}; | |
break; | |
case "mana": | |
this.sprite = SPRITE.Mana; | |
this.exe = () => { | |
HERO.maxMana += INI.LVL_MANA; | |
HERO.mana = HERO.maxMana; | |
TITLE.change(); | |
}; | |
break; | |
case "weapon": | |
this.sprite = SPRITE.Sword; | |
this.exe = () => { | |
HERO.weapon++; | |
TITLE.change(); | |
}; | |
break; | |
case "armor": | |
this.sprite = SPRITE.Shield; | |
this.exe = () => { | |
HERO.armor++; | |
TITLE.change(); | |
}; | |
break; | |
case "magic": | |
this.sprite = SPRITE.Magic; | |
this.exe = () => { | |
HERO.magic++; | |
//HERO.magic += 2; | |
TITLE.change(); | |
}; | |
break; | |
case "agility": | |
this.sprite = SPRITE.Agility; | |
this.exe = () => { | |
HERO.agility++; | |
TITLE.change(); | |
}; | |
break; | |
} | |
} | |
} | |
class Lamp { | |
constructor(grid) { | |
this.grid = grid; | |
this.sprite = SPRITE.Lamp; | |
this.value = 99; | |
this.start = null; | |
this.now = null; | |
this.delta = null; | |
} | |
exe() { | |
if (!HERO.lamp) { | |
HERO.visibility = 2; | |
HERO.lamp = this; | |
HERO.inventory.add(SPRITE.Lamp); | |
TITLE.change(); | |
this.switchOn(); | |
} else { | |
HERO.lamp.value += this.value; | |
} | |
} | |
update() { | |
this.now = performance.now(); | |
this.delta = Math.round((this.now - this.start) / 1000); | |
if (this.delta >= this.value) this.switchOff(); | |
} | |
switchOn() { | |
this.start = performance.now(); | |
} | |
switchOff() { | |
HERO.visibility = 1; | |
HERO.lamp = false; | |
HERO.inventory.delete(SPRITE.Lamp); | |
TITLE.change(); | |
} | |
} | |
class Scroll { | |
constructor(grid, type, use) { | |
this.name = "Scroll"; | |
this.type = type; | |
this.id = this.name + this.type; | |
this.sprite = SPRITE["SCR_" + type] || SPRITE.Scroll; | |
this.grid = grid; | |
this.static = false; | |
this.use = use; | |
} | |
exe() { | |
HERO.scrolls.add(this); | |
} | |
action() { | |
let POOL; | |
switch (this.type) { | |
case "Cripple": | |
console.log("action cripple"); | |
POOL = MAP[GAME.level].DUNGEON.ENEMY; | |
for (let q = 0, PL = POOL.length; q < PL; q++) { | |
let enemy = POOL[q]; | |
let distance = HERO.MoveState.endGrid.distanceDiagonal( | |
enemy.MoveState.endGrid | |
); | |
if (distance <= INI.MANA_DRAIN_RANGE) { | |
enemy.speed = 1; | |
enemy.agility = drain(enemy.agility); | |
} | |
} | |
break; | |
case "Map": | |
let grid; | |
if (MAP[GAME.level].DUNGEON.mapAnchors.length) { | |
grid = MAP[GAME.level].DUNGEON.mapAnchors.shift(); | |
} else grid = MAP[GAME.level].DUNGEON.getAnyGrid(); | |
MINIMAP.unveil(grid); | |
TITLE.change(); | |
break; | |
case "Light": | |
let lamp = new Lamp(new Grid(0, 0)); | |
lamp.exe(); | |
break; | |
case "Invisibility": | |
HERO.invisible(); | |
break; | |
case "DrainMana": | |
HERO.mana = 0; | |
TITLE.change(); | |
POOL = MAP[GAME.level].DUNGEON.ENEMY; | |
for (let q = 0, PL = POOL.length; q < PL; q++) { | |
let enemy = POOL[q]; | |
let distance = HERO.MoveState.endGrid.distanceDiagonal( | |
enemy.MoveState.endGrid | |
); | |
if (distance <= INI.MANA_DRAIN_RANGE) { | |
enemy.mana = 0; | |
} | |
} | |
break; | |
case "BoostWeapon": | |
$("#hero_sword").css({ color: "blue" }); | |
HERO.weapon = inflate(HERO.weapon); | |
CONSOLE.print( | |
`<span class="blue">${ | |
HERO.name | |
}</span> applied magical sharpening oil to the sword.` | |
); | |
break; | |
case "BoostArmor": | |
$("#hero_shield").css({ color: "blue" }); | |
HERO.armor = inflate(HERO.armor); | |
CONSOLE.print( | |
`<span class="blue">${ | |
HERO.name | |
}</span> applied magic protection oil to the armor.` | |
); | |
break; | |
case "DestroyArmor": | |
$("#enemy_armor").css({ color: "red" }); | |
GAME.TURN.enemy.armor = drain(GAME.TURN.enemy.armor); | |
CONSOLE.print( | |
`<span class="blue">${ | |
HERO.name | |
}</span> magically lowered <span class="red">${ | |
GAME.TURN.enemy.type.title | |
}'s</span> defense.` | |
); | |
break; | |
case "DestroyWeapon": | |
$("#enemy_weapon").css({ color: "red" }); | |
GAME.TURN.enemy.weapon = drain(GAME.TURN.enemy.weapon); | |
CONSOLE.print( | |
`<span class="blue">${ | |
HERO.name | |
}</span> magically drained <span class="red">${ | |
GAME.TURN.enemy.type.title | |
}'s</span> weapons.` | |
); | |
break; | |
default: | |
console.log("Scroll action ERROR!"); | |
break; | |
} | |
function drain(number) { | |
let N = RND(Math.floor(0.333 * number), Math.ceil(0.666 * number)); | |
if (N === number) N = number - 1; | |
return N; | |
} | |
function inflate(number) { | |
let N = RND(Math.floor(1.2 * number), Math.ceil(1.5 * number)); | |
if (N === number) N = number + 1; | |
return N; | |
} | |
} | |
} | |
var HERO = { | |
construct: function() { | |
//HERO.name = "HERO"; | |
HERO.name = HERO.getName(); | |
HERO.gold = 0; | |
//HERO.gold = 999; //DEBUG | |
HERO.redPotion = 0; | |
HERO.bluePotion = 0; | |
HERO.maxHealth = 8; | |
HERO.maxHealth = 58; //debug | |
HERO.health = HERO.maxHealth; | |
HERO.maxMana = 13; | |
HERO.maxMana = 999; //debug | |
HERO.mana = HERO.maxMana; | |
HERO.armor = 1; | |
HERO.armor = 6; //debug | |
HERO.weapon = 1; | |
HERO.weapon = 6; //debug | |
HERO.magic = 2; | |
HERO.magic = 20; //debug | |
HERO.agility = 1; | |
//HERO.agility = 25; //debug | |
HERO.magicResistance = 0; | |
HERO.inventory = new Set(); | |
HERO.scrolls = new Inventory(); | |
HERO.silverKey = false; | |
HERO.goldKey = false; | |
HERO.depth = 1; | |
//HERO.level = 1; | |
HERO.level = 0; | |
HERO.experience = 0; | |
//HERO.experience = 399; //debug | |
HERO.expBuffer = 0; | |
//HERO.expBuffer = 399; //debug | |
HERO.magicExpBuffer = 0; | |
//HERO.magicExpBuffer = 48; //debug | |
HERO.speed = 6; | |
HERO.visibility = 1; | |
HERO.dark = false; | |
HERO.dead = false; | |
HERO.cloak = false; | |
HERO.lamp = false; | |
HERO.points = 0; | |
HERO.canEnterTemple = true; | |
HERO.canLevelUp = false; | |
HERO.spriteClass = "Knight"; | |
HERO.asset = ASSET[HERO.spriteClass]; | |
HERO.actor = new ACTOR(HERO.spriteClass, 0, 0, "front", HERO.asset); | |
HERO.inFight = false; | |
HERO.maxDepth = 1; | |
}, | |
getName: function() { | |
console.log("getting Hero name"); | |
let name = $("#HeroName").val(); | |
return name.toLowerCase().capitalize(); | |
}, | |
invisible: function() { | |
HERO.dark = true; | |
HERO.cloak = new SimpleTimer(INI.INVISIBILITY_TIME, HERO.visible); | |
HERO.spriteClass = "KnightInvisible"; | |
HERO.setSpriteClass(HERO.spriteClass); | |
}, | |
visible: function() { | |
HERO.dark = false; | |
HERO.cloak = false; | |
HERO.spriteClass = "Knight"; | |
HERO.setSpriteClass(HERO.spriteClass); | |
}, | |
setSpriteClass: function(spriteClass) { | |
HERO.asset = ASSET[spriteClass]; | |
HERO.actor.class = spriteClass; | |
HERO.actor.asset = HERO.asset; | |
HERO.actor.resetIndexes(); | |
HERO.actor.animateMove(HERO.actor.orientation); | |
}, | |
init: function() { | |
GRID.gridToSprite(MAP[GAME.level].DUNGEON[GAME.location], HERO.actor); | |
HERO.MoveState = new MoveState( | |
MAP[GAME.level].DUNGEON[GAME.location], | |
DOWN | |
); | |
ENGINE.VIEWPORT.check(HERO.actor); | |
ENGINE.VIEWPORT.alignTo(HERO.actor); | |
}, | |
draw: function() { | |
if (HERO.dead) return; | |
ENGINE.spriteDraw( | |
"actors", | |
HERO.actor.vx, | |
HERO.actor.vy, | |
HERO.actor.sprite() | |
); | |
ENGINE.layersToClear.add("actors"); | |
}, | |
move: function() { | |
if (HERO.dead) return; | |
if (HERO.MoveState.moving) { | |
GRID.translateMove(HERO, true, HeroOnFinish); | |
} | |
function HeroOnFinish() { | |
TITLE.change(); | |
HERO.updView(); | |
} | |
}, | |
changeDirection: function(dir) { | |
if (HERO.MoveState.moving) return; | |
let x = HERO.MoveState.endGrid.x + dir.x; | |
let y = HERO.MoveState.endGrid.y + dir.y; | |
if (!GRID.isBlock(x, y)) { | |
HERO.MoveState.next(dir); | |
} | |
}, | |
interactLists: function() { | |
let LIST = ["boosts", "chests", "gold", "potions", "lamps", "scrolls"]; | |
for (let outer = 0, LN = LIST.length; outer < LN; outer++) { | |
let LL = MAP[GAME.level].DUNGEON[LIST[outer]].length; | |
for (let list_index = 0; list_index < LL; list_index++) { | |
let element = MAP[GAME.level].DUNGEON[LIST[outer]][list_index]; | |
let hit = GRID.collision(HERO, element.grid); | |
if (hit) { | |
//console.log(hit, element); | |
if (element.static) HERO.MoveState.reverse(); | |
element.exe(); | |
ENGINE.VIEWPORT.changed = true; | |
MAP[GAME.level].DUNGEON[LIST[outer]].splice(list_index, 1); | |
GAME.PAINT.config(); | |
return; | |
} | |
} | |
} | |
}, | |
interactStatic: function() { | |
let LIST = [ | |
"door", | |
"gate", | |
"entrance", | |
"exit", | |
"goldKey", | |
"silverKey", | |
"temple" | |
]; | |
let element; | |
for (let q = 0, LN = LIST.length; q < LN; q++) { | |
element = check(LIST[q]); | |
if (element !== null) break; | |
} | |
if (element === null) return; | |
switch (element) { | |
case "door": | |
if (HERO.silverKey) { | |
HERO.silverKey = false; | |
HERO.incExp(getKeyExp()); | |
SpritePOOL.pool.push( | |
new PartSprite( | |
GRID.gridToCoord(MAP[GAME.level].DUNGEON.door), | |
SPRITE.Door, | |
SPRITE.Door.height, | |
1 | |
) | |
); | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.door) | |
] = 1; | |
MAP[GAME.level].DUNGEON.door = null; | |
MAP[GAME.level].DUNGEON.setObstacles( | |
MAP[GAME.level].DUNGEON.door, | |
MAP[GAME.level].DUNGEON.gate | |
); | |
HERO.inventory.delete(SPRITE.silverKey); | |
GAME.PAINT.config(); | |
TITLE.change(); | |
} else { | |
HERO.MoveState.reverse(); | |
} | |
break; | |
case "gate": | |
if (HERO.goldKey) { | |
HERO.goldKey = false; | |
HERO.incExp(getKeyExp()); | |
SpritePOOL.pool.push( | |
new PartSprite( | |
GRID.gridToCoord(MAP[GAME.level].DUNGEON.gate), | |
SPRITE.Gate, | |
SPRITE.Gate.height, | |
1 | |
) | |
); | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.gate) | |
] = 1; | |
MAP[GAME.level].DUNGEON.gate = null; | |
MAP[GAME.level].DUNGEON.setObstacles( | |
MAP[GAME.level].DUNGEON.door, | |
MAP[GAME.level].DUNGEON.gate | |
); | |
HERO.inventory.delete(SPRITE.goldKey); | |
GAME.PAINT.config(); | |
TITLE.change(); | |
} else { | |
HERO.MoveState.reverse(); | |
} | |
break; | |
case "entrance": | |
if (GAME.level > 1) { | |
console.log("Going up!"); | |
if (ENGINE.GAME.keymap[ENGINE.KEY.map.tab]) { | |
HERO.usingStairs(-1); | |
} | |
} | |
break; | |
case "exit": | |
console.log("Going down"); | |
if (ENGINE.GAME.keymap[ENGINE.KEY.map.tab]) { | |
HERO.usingStairs(1); | |
} | |
break; | |
case "goldKey": | |
case "silverKey": | |
HERO[element] = true; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON[element]) | |
] = 1; | |
MAP[GAME.level].DUNGEON[element] = null; | |
GAME.PAINT.config(); | |
HERO.incExp(getKeyExp()); | |
HERO.inventory.add(SPRITE[element]); | |
TITLE.change(); | |
break; | |
case "temple": | |
if (HERO.canEnterTemple) GAME.visitTemple(); | |
console.log("Temple"); | |
break; | |
} | |
function check(instance) { | |
let hit = GRID.collision(HERO, MAP[GAME.level].DUNGEON[instance]); | |
if (hit) { | |
return instance; | |
} else return null; | |
} | |
function getKeyExp() { | |
let exp = INI.EXP_KEY; | |
for (let i = 1; i <= GAME.level - 1; i++) { | |
exp *= INI.EXP_FACTOR; | |
} | |
console.log("GATE, KEY exp awarded", exp); | |
return round10(exp); | |
} | |
}, | |
useManaPotion: function() { | |
if (HERO.mana === HERO.maxMana) return; | |
if (HERO.bluePotion > 0) { | |
HERO.bluePotion--; | |
HERO.incMana(); | |
} | |
}, | |
useHealingPotion: function() { | |
if (HERO.health === HERO.maxHealth) return 0; | |
if (HERO.redPotion > 0) { | |
HERO.redPotion--; | |
return HERO.heal(); | |
} | |
}, | |
heal: function() { | |
let addHealth = Math.round(INI.Health_INC * HERO.maxHealth); | |
let realHealing = Math.min(addHealth, HERO.maxHealth - HERO.health); | |
HERO.health += addHealth; | |
let above = GRID.gridToCoord(HERO.MoveState.homeGrid.add(UP)).add( | |
new Vector(0, 24) | |
); | |
TEXTPOOL.pool.push( | |
new TextSprite(("+" + addHealth).toString(), above, "#DD0000", 50) | |
); | |
HERO.health = Math.min(HERO.health, HERO.maxHealth); | |
TITLE.change(); | |
return realHealing; | |
}, | |
incMana: function() { | |
let above = GRID.gridToCoord(HERO.MoveState.homeGrid.add(UP)).add( | |
new Vector(0, 24) | |
); | |
let addMana = Math.round(INI.Health_INC * HERO.maxMana); | |
HERO.mana += addMana; | |
TEXTPOOL.pool.push( | |
new TextSprite(("+" + addMana).toString(), above, "#0000EE", 50) | |
); | |
HERO.mana = Math.min(HERO.mana, HERO.maxMana); | |
TITLE.change(); | |
}, | |
decMana: function(dec) { | |
HERO.mana -= dec; | |
if (HERO.mana <= 0) HERO.mana = 0; | |
TITLE.change(); | |
}, | |
updView: function() { | |
ENGINE.VIEWPORT.changed = true; | |
TITLE.change(); | |
let x = HERO.MoveState.endGrid.x - 1; | |
let y = HERO.MoveState.endGrid.y - 1; | |
let left = x * ENGINE.INI.GRIDPIX; | |
let top = y * ENGINE.INI.GRIDPIX; | |
ENGINE.cutManyGrids(LAYER.fog, new Point(left, top), 3); | |
for (let q = x; q < x + 3; q++) { | |
for (let w = y; w < y + 3; w++) { | |
let temp = new Grid(q, w); | |
MINIMAP.maps[GAME.level].map[GRID.gridToIndex(temp)] &= 127; | |
MINIMAP.maps[GAME.level].map[GRID.gridToIndex(temp)] |= 64; //set fog info | |
} | |
} | |
if (HERO.visibility !== 2) return; | |
for (let q = 0; q < ENGINE.directions.length; q++) { | |
let test = HERO.MoveState.endGrid.add(ENGINE.directions[q]); | |
if (!GRID.gridIsBlock(test)) { | |
test = test.add(ENGINE.directions[q]); | |
ENGINE.cutGrid(LAYER.fog, GRID.gridToCoord(test)); | |
MINIMAP.maps[GAME.level].map[GRID.gridToIndex(test)] &= 127; | |
MINIMAP.maps[GAME.level].map[GRID.gridToIndex(test)] |= 64; //set fog info | |
} | |
} | |
var vectors = [new Vector(1, 1), new Vector(1, 0), new Vector(0, 1)]; | |
for (let q = 0; q < ENGINE.corners.length; q++) { | |
let test = HERO.MoveState.endGrid.add(ENGINE.corners[q]); | |
if (!GRID.gridIsBlock(test)) { | |
for (let w = 0; w < vectors.length; w++) { | |
let tVector = ENGINE.corners[q].mul(vectors[w]); | |
let lightGrid = HERO.MoveState.endGrid.add(tVector); | |
ENGINE.cutGrid(LAYER.fog, GRID.gridToCoord(lightGrid)); | |
MINIMAP.maps[GAME.level].map[GRID.gridToIndex(lightGrid)] &= 127; | |
MINIMAP.maps[GAME.level].map[GRID.gridToIndex(lightGrid)] |= 64; //set fog info | |
} | |
} | |
} | |
}, | |
manage: function() { | |
HERO.move(); | |
if (HERO.lamp) HERO.lamp.update(); | |
if (HERO.cloak) HERO.cloak.update(); | |
HERO.interactLists(); | |
HERO.interactStatic(); | |
HERO.collisionEnemy(); | |
}, | |
incExp: function(exp) { | |
HERO.experience += exp; | |
HERO.expBuffer += exp; | |
if (HERO.expBuffer >= GAME.EXP) { | |
HERO.expBuffer -= GAME.EXP; | |
HERO.canLevelUp = true; | |
GAME.EXP *= INI.EXP_FACTOR; | |
GAME.EXP = Math.round(GAME.EXP); | |
console.log("NEW EXP limit", GAME.EXP); | |
} | |
}, | |
castMagic: function() { | |
const dir = HERO.MoveState.dir; | |
const cost = HERO.magic + INI.MAGIC_POWER_COST; | |
if (cost > HERO.mana) { | |
//sound failed magic | |
return; | |
} | |
if (success() && dir !== null) { | |
const power = setPower(); | |
HERO.mana -= cost; | |
TITLE.change(); | |
//sound casting magic | |
ORBS.pool.push( | |
new Orb(HERO.MoveState.homeGrid, dir, power, HERO.magic, "friendly") | |
); | |
} else { | |
HERO.decMana(1); | |
//sound failed magic | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(HERO.actor.x, HERO.actor.y, "Fizzle_", 10) | |
); | |
} | |
function success() { | |
let prop = Math.round(HERO.magic / (HERO.magic + INI.MAGIC_FAIL) * 100); | |
return probable(prop); | |
} | |
function setPower() { | |
let bottom = Math.round(HERO.magic * 0.8); | |
if (bottom === 0) bottom = 1; | |
const power = RND(bottom, Math.round(HERO.magic * 1.2)); | |
return power; | |
} | |
}, | |
collisionEnemy: function() { | |
let POOL = MAP[GAME.level].DUNGEON.ENEMY; | |
for (let PL = POOL.length, q = PL - 1; q >= 0; q--) { | |
let enemy = POOL[q]; | |
if (!enemy.visible) continue; | |
//let hitGrid = GRID.spriteToSpriteCollision(HERO, enemy); | |
let hitShape = ENGINE.collision(HERO.actor, enemy.actor); | |
//research hitGrid || hitShape, hitGrid && hitShape | |
if (hitShape) { | |
GAME.fight(enemy, true, q); | |
break; | |
} | |
} | |
}, | |
turn: function(enemy) { | |
if (!GAME.TURN.fight_active) return; | |
let damage = GAME.TURN.damage(HERO, enemy); | |
if (damage > 0) { | |
CONSOLE.print( | |
`<span class="blue">${ | |
HERO.name | |
}</span> hits and makes <span class="orange">${damage}</span> damage.` | |
); | |
enemy.health -= damage; | |
if (enemy.health <= 0) { | |
enemy.die(); | |
CONSOLE.print( | |
`<span class="red">${enemy.type.title}</span> was killed.` | |
); | |
CONSOLE.print( | |
`<span class="blue">${HERO.name} gets ${enemy.type.exp} XP.</span>` | |
); | |
GAME.TURN.fight_active = false; | |
MAP[GAME.level].DUNGEON.ENEMY.splice(GAME.TURN.enemyIndex, 1); | |
GAME.CLICK.endFight(); | |
LOG[enemy.type.title].kills++; | |
} | |
enemy.health = Math.max(0, enemy.health); | |
GAME.fightRefresh(enemy); | |
} else { | |
CONSOLE.print(`<span class="blue">${HERO.name}</span> misses.`); | |
} | |
}, | |
death: function() { | |
console.log(`%cHERO died`, PRG.CSS); | |
HERO.dead = true; | |
HERO.dark = true; | |
ENGINE.spriteDraw("grave", HERO.actor.vx, HERO.actor.vy, SPRITE.Grave); | |
ENGINE.TEXT.setFS(60); | |
ENGINE.TEXT.centeredText("THE END", 300); | |
//Game still runs!! | |
GAME.over(); | |
}, | |
usingStairs: function(delta) { | |
GAME.prepareLevel = GAME.level + delta; | |
switch (delta) { | |
case 1: | |
GAME.location = "entrance"; | |
HERO.maxDepth = Math.max(HERO.maxDepth, GAME.prepareLevel); | |
break; | |
case -1: | |
GAME.location = "exit"; | |
break; | |
} | |
console.log( | |
delta, | |
"HERO using the stairs to level", | |
GAME.prepareLevel, | |
GAME.location | |
); | |
console.log("GAME waitin for all processes to complete"); | |
ENGINE.GAME.ANIMATION.STACK.push(GAME.coolDown); | |
ENGINE.GAME.ANIMATION.stop(); | |
} | |
}; | |
class Orb { | |
constructor(grid, dir, power, range, type) { | |
this.grid = Grid.toClass(grid); | |
this.type = type; | |
this.power = power; | |
this.speed = 16; | |
this.range = Math.min(range, INI.ORB_MAX_RANGE); | |
let id; | |
if (type === "friendly") { | |
id = "MagicOrb"; | |
} else id = "RedMagic"; | |
this.actor = new ACTOR(id, 0, 0, "linear", ASSET[id]); | |
GRID.gridToSprite(this.grid, this.actor); | |
this.alignToViewport(); | |
this.MoveState = new MoveState(this.grid, dir); | |
this.MoveState.next(dir); | |
} | |
alignToViewport() { | |
ENGINE.VIEWPORT.alignTo(this.actor); | |
} | |
draw() { | |
ENGINE.spriteDraw( | |
"orbs", | |
this.actor.vx, | |
this.actor.vy, | |
this.actor.sprite() | |
); | |
} | |
fizzle() { | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(this.actor.x, this.actor.y, "Fizzle_", 10) | |
); | |
} | |
} | |
var ORBS = { | |
pool: [], | |
draw: function() { | |
let OPL = ORBS.pool.length; | |
if (OPL === 0) return; | |
ENGINE.layersToClear.add("orbs"); | |
for (let q = OPL - 1; q >= 0; q--) { | |
ORBS.pool[q].draw(); | |
} | |
}, | |
manage: function() { | |
ORBS.move(); | |
ORBS.collide(); | |
}, | |
move: function() { | |
let OPL = ORBS.pool.length; | |
if (OPL === 0) return; | |
for (let q = OPL - 1; q >= 0; q--) { | |
let orb = ORBS.pool[q]; | |
if (orb.range === 0) { | |
orb.fizzle(); | |
ORBS.pool.splice(q, 1); | |
continue; | |
} | |
if (orb.MoveState.moving) { | |
GRID.translateMove(orb); | |
} else { | |
orb.MoveState.next(orb.MoveState.dir); | |
orb.range--; | |
} | |
} | |
}, | |
collide: function() { | |
//to background | |
let OPL = ORBS.pool.length; | |
if (OPL === 0) return; | |
for (let q = OPL - 1; q >= 0; q--) { | |
let orb = ORBS.pool[q]; | |
if (GRID.gridIsBlock(orb.MoveState.homeGrid)) { | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "ShipExp", 8) | |
); | |
ORBS.pool.splice(q, 1); | |
continue; //next orb | |
} | |
} | |
//to enemy | |
OPL = ORBS.pool.length; | |
if (OPL === 0) return; | |
for (let q = OPL - 1; q >= 0; q--) { | |
let orb = ORBS.pool[q]; | |
if (orb.type === "friendly") { | |
let ENML = MAP[GAME.level].DUNGEON.ENEMY.length; | |
for (let w = ENML - 1; w >= 0; w--) { | |
let enemy = MAP[GAME.level].DUNGEON.ENEMY[w]; | |
//let hit = GRID.spriteToSpriteCollision(orb, enemy); | |
let hit = ENGINE.collision(orb.actor, enemy.actor); | |
if (hit) { | |
// | |
HERO.magicExpBuffer++; | |
//console.log("HERO.magicExpBuffer", HERO.magicExpBuffer); | |
if (HERO.magicExpBuffer >= GAME.MAGIC_EXP) { | |
HERO.magicExpBuffer -= GAME.MAGIC_EXP; | |
GAME.MAGIC_EXP *= INI.MAGIC_EXP_FACTOR; | |
GAME.MAGIC_EXP = round10(GAME.MAGIC_EXP); | |
HERO.magic++; | |
console.log( | |
"magic increased due to succesful use, new EXP limit", | |
GAME.MAGIC_EXP | |
); | |
TITLE.change(); | |
} | |
// | |
let damage = orb.power - enemy.type.magicResistance; | |
enemy.health -= damage; | |
damage = Math.max(damage, 0); | |
let above = GRID.gridToCoord(enemy.MoveState.homeGrid.add(UP)).add( | |
new Vector(0, 24) | |
); | |
if (damage === 0) { | |
TEXTPOOL.pool.push( | |
new TextSprite("Resisted".toString(), above, "#AAA", 50) | |
); | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "Fizzle_", 10) | |
); | |
} else if (enemy.health > 0) { | |
TEXTPOOL.pool.push( | |
new TextSprite(("-" + damage).toString(), above, "#FF0000", 50) | |
); | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "AlienExp", 6) | |
); | |
} else { | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "ShipExp", 8) | |
); | |
enemy.die(); | |
MAP[GAME.level].DUNGEON.ENEMY.splice(w, 1); | |
} | |
ORBS.pool.splice(q, 1); | |
break; | |
} | |
} | |
} | |
} | |
//to HERO | |
OPL = ORBS.pool.length; | |
if (OPL === 0) return; | |
for (let q = OPL - 1; q >= 0; q--) { | |
let orb = ORBS.pool[q]; | |
if (orb.type !== "friendly") { | |
//let hit = GRID.spriteToSpriteCollision(orb, HERO); | |
let hit = ENGINE.collision(orb.actor, HERO.actor); | |
if (hit) { | |
let resistance = Math.floor(HERO.magic / 3); | |
let damage = orb.power - resistance; | |
HERO.health -= damage; | |
HERO.health = Math.max(HERO.health, 0); | |
TITLE.change(); | |
damage = Math.max(damage, 0); | |
let above = GRID.gridToCoord(HERO.MoveState.homeGrid.add(UP)).add( | |
new Vector(0, 22) | |
); | |
if (damage === 0) { | |
TEXTPOOL.pool.push( | |
new TextSprite("Resisted".toString(), above, "#AAA", 50) | |
); | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "Fizzle_", 10) | |
); | |
} else if (HERO.health > 0) { | |
TEXTPOOL.pool.push( | |
new TextSprite(("-" + damage).toString(), above, "#FF0022", 50) | |
); | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "AlienExp", 6) | |
); | |
} else { | |
EXPLOSIONS.pool.push( | |
new AnimationSPRITE(orb.actor.x, orb.actor.y, "ShipExp", 8) | |
); | |
console.log("HERO died from Orb collision"); | |
HERO.death(); | |
} | |
ORBS.pool.splice(q, 1); | |
} | |
} | |
} | |
} | |
}; | |
var ENEMY = { | |
draw: function() { | |
let POOL = MAP[GAME.level].DUNGEON.ENEMY; | |
for (let q = 0, PL = POOL.length; q < PL; q++) { | |
let enemy = POOL[q]; | |
ENGINE.VIEWPORT.alignTo(enemy.actor); //pazi, v translateMove se ponovi!! samo za DEV, lahko isključiš ko riše samo visible | |
if (enemy.visible || DEBUG.viewMonsters) { | |
ENGINE.spriteDraw( | |
"actors", | |
enemy.actor.vx, | |
enemy.actor.vy, | |
enemy.actor.sprite() | |
); | |
} | |
ENGINE.layersToClear.add("actors"); | |
} | |
}, | |
manage: function() { | |
//DEBUG.frameCount++; | |
//console.log("**************** REVIEW cycle", DEBUG.frameCount); | |
//if (DEBUG.frameCount > 12) GAME.abort(); | |
let POOL = MAP[GAME.level].DUNGEON.ENEMY; | |
//let t1 = performance.now(); | |
for (let PL = POOL.length, q = PL - 1; q >= 0; q--) { | |
let enemy = POOL[q]; | |
let distance = HERO.MoveState.endGrid.distanceDiagonal( | |
enemy.MoveState.endGrid | |
); | |
if (enemy.MoveState.moving) { | |
if (checkFreedom(q)) GRID.translateMove(enemy); | |
if (distance <= INI.TRIGGER_VISION) enemy.visible = enemy.isVisible(); | |
continue; | |
} | |
let node; | |
let path = []; | |
if (enemy.awake) { | |
//AWAKE | |
//should go to sleep? | |
if (distance > INI.TRIGGER_WAKE && enemy.strategy !== "hunt") { | |
//console.log(enemy, "goes to sleep"); | |
enemy.sleep(); | |
continue; | |
} | |
//go to sleep if path too long | |
node = GRID.findDungeonPath( | |
enemy.MoveState.endGrid, | |
HERO.MoveState.endGrid, | |
MAP[GAME.level].DUNGEON.obstacles, | |
GAME.DISTANCE_WAKE | |
); | |
//hunters should be switched to wander in order to sleep! | |
if (node && node.dist !== 0 && node.priority > INI.TRIGGER_WAKE && enemy.strategy !== "hunt") { | |
enemy.sleep(); | |
continue; | |
} | |
//sleep, awake evaluation end | |
//check vision, line of sight | |
//if in visible range, check if truly visible | |
if (distance <= INI.TRIGGER_VISION) enemy.visible = enemy.isVisible(); | |
//end checking vision | |
//not moving | |
//SHOOT | |
if (!HERO.dark && enemy.magic > 0) { | |
//has ability to shoot | |
const cost = enemy.magic + INI.MAGIC_POWER_COST; | |
if (cost <= enemy.mana && distance <= enemy.magic) { | |
//has enough mana and range | |
if (distance <= INI.STALK_DISTANCE && enemy.visible && !enemy.canShoot){ | |
//stalk while on cool down, or else shoot | |
enemy.strategy = "stalk"; | |
} else enemy.strategy = "hunt"; | |
if (enemy.canShoot) { | |
let direction = enemy.MoveState.endGrid.absDirection( | |
HERO.MoveState.endGrid | |
); | |
if (direction.isOrto() || direction.isDiagonal()) { | |
//has proper direction | |
if ( | |
GRID.vision(enemy.MoveState.endGrid, HERO.MoveState.endGrid) | |
) { | |
enemy.mana -= cost; | |
ORBS.pool.push( | |
new Orb( | |
enemy.MoveState.homeGrid, | |
direction, | |
setPower(enemy), | |
enemy.magic, | |
"deadly" | |
) | |
); | |
enemy.casted(); | |
continue; | |
} | |
} | |
} | |
} else { | |
//not enough mana to cast | |
//change strategy --> hunt | |
enemy.strategy = "hunt"; | |
} | |
} | |
//SHOOT END | |
//has stack? | |
if (enemy.dirStack.length > 0) { | |
// has stack, but let's check first if it should hunt | |
if (!HERO.dark && distance <= enemy.type.triggers.hunt) { | |
//switch to hunt and clear stack | |
enemy.strategy = "hunt"; | |
enemy.dirStack.clear(); | |
continue; | |
} else { | |
//yes, move from stack | |
enemy.makeMove(); | |
continue; | |
} | |
} else { | |
//no, stack is empty | |
//set direction stack from strategy | |
switch (enemy.strategy) { | |
case "wander": | |
//check first for status change | |
if (!HERO.dark && distance <= enemy.type.triggers.hunt) { | |
enemy.strategy = "hunt"; | |
break; | |
} | |
//finished checking for status | |
path = GRID.AI.wanderer.hunt(enemy.MoveState).return; | |
break; | |
case "hunt": | |
//check first for status change | |
if (HERO.dark || distance >= enemy.type.triggers.wander) { | |
enemy.strategy = "wander"; | |
break; | |
} | |
//finished checking for status | |
node = GRID.findDungeonPath( | |
enemy.MoveState.endGrid, | |
HERO.MoveState.endGrid, | |
MAP[GAME.level].DUNGEON.obstacles | |
); | |
//debug | |
if (enemy.type.name === "Wizard"){ | |
console.log(q, enemy, "hunter-->", node); | |
} | |
//debug end | |
if (node === null) { | |
GAME.fight(enemy, false, q); | |
continue; | |
} else { | |
path = node.stack; | |
path.length = 1; | |
} | |
//end finding path | |
break; | |
case "stalk": | |
//console.log("Stalking!"); | |
path = GRID.AI.keepTheDistance.hunt(enemy.MoveState, HERO.MoveState.endGrid, INI.STALK_DISTANCE).return | |
//console.log("Stalking!", path, path.length, path[0]); | |
break; | |
case "goto": | |
//console.log("goto"); | |
break; | |
case "guard": | |
break; | |
case "seek": | |
break; | |
case "flee": | |
break; | |
default: | |
console.log("monster strategy ERROR"); | |
break; | |
} | |
//end of switch, if not continue(d) break points to here: | |
if (path && path.length > 0) { | |
enemy.dirStack = path; | |
enemy.makeMove(); | |
continue; | |
} else { | |
continue; | |
} | |
} | |
//stack end | |
} else { | |
//HYBERNATING | |
//should wake? | |
if (distance <= INI.TRIGGER_WAKE) { | |
//console.log("check if it should wake", enemy); | |
node = GRID.findDungeonPath( | |
enemy.MoveState.endGrid, | |
HERO.MoveState.endGrid, | |
MAP[GAME.level].DUNGEON.obstacles, | |
GAME.DISTANCE_WAKE | |
); | |
if (node.status === "NoSolution") continue; | |
if ( | |
node === null || | |
(node.dist === 0 && node.priority <= INI.TRIGGER_WAKE) | |
) | |
enemy.wake(); | |
} | |
continue; | |
} | |
} | |
//GAME.abort() | |
//let t2 = performance.now() - t1; | |
//console.log("spent", t2, "ms"); | |
function checkFreedom(q) { | |
for (let W = 0; W < q; W++) { | |
if (MAP[GAME.level].DUNGEON.ENEMY[W].awake) { | |
if ( | |
GRID.same( | |
MAP[GAME.level].DUNGEON.ENEMY[q].MoveState.endGrid, | |
MAP[GAME.level].DUNGEON.ENEMY[W].MoveState.endGrid | |
) | |
) | |
return false; | |
} | |
} | |
return true; | |
} | |
function setPower(enemy) { | |
let bottom = Math.round(enemy.type.magic * 0.8); | |
if (bottom === 0) bottom = 1; | |
const power = RND(bottom, Math.round(enemy.type.magic * 1.2)); | |
return power; | |
} | |
} | |
}; | |
var GAME = { | |
CSS: "color: #0F0", | |
abort: function() { | |
ENGINE.GAME.stopAnimation = true; | |
console.error("..... aborting GAME, DEBUG info:"); | |
console.log("scrolls", HERO.scrolls); | |
}, | |
start: function() { | |
GAME.DISTANCE_WAKE = Math.floor(2 * Math.sqrt((INI.TRIGGER_WAKE ** 2) / 2)); | |
console.log("GAME.DISTANCE_WAKE", GAME.DISTANCE_WAKE); | |
ENGINE.GAME.start(); //INIT game loop | |
ENGINE.KEY.on(); // keymapping active | |
CreateDungeon.init(); | |
GAME.prepareForRestart(); //everything required for safe restart | |
GAME.level = 1; | |
GAME.prepareLevel = null; | |
GAME.location = "entrance"; | |
GAME.score = 0; | |
ENGINE.INI.ANIMATION_INTERVAL = 16; | |
HERO.construct(); | |
ENGINE.GAME.ANIMATION.waitThen(GAME.levelStart, 2); | |
GAME.EXP = INI.EXP; | |
GAME.MAGIC_EXP = INI.MAGIC_EXP; | |
GAME.time = new Timer(); | |
}, | |
prepareForRestart: function() { | |
console.log("preparing game for start or safe restart ..."); | |
//everything required for safe restart | |
ENGINE.GAME.ANIMATION.stop(); | |
//clear layers | |
ENGINE.clearLayer("text"); | |
ENGINE.clearLayer("animation"); | |
ENGINE.clearLayer("grave"); | |
ENGINE.clearLayer("actors"); | |
}, | |
coolDown: function() { | |
ENGINE.GAME.ANIMATION.stop(); | |
console.log("%c ...all processes completed", PRG.CSS); | |
GAME.nextLevel(); | |
}, | |
spawnNemesis: function() { | |
console.log("GAME.spawnNemesis"); | |
CreateDungeon.spawnNemesis(GAME.level); | |
//just one for debug purpose! uncomment line below!! | |
GAME.levelTime = new SimpleTimer(INI.NEMESIS_RESPAWN, GAME.spawnNemesis); | |
//GAME.levelTime = new SimpleTimer(9999, GAME.spawnNemesis); | |
}, | |
levelExecute: function() { | |
console.log(`%cLevel ${GAME.level} executes ...`, GAME.CSS); | |
ENGINE.VIEWPORT.reset(); | |
HERO.init(); | |
EXPLOSIONS.pool.clear(); | |
GAME.firstFrameDraw(GAME.level); | |
GAME.levelTime = new SimpleTimer(INI.NEMESIS_RESPAWN, GAME.spawnNemesis); | |
if (HERO.level === 0) { | |
GAME.levelUp(INI.POINTS_ON_START); | |
} else GAME.levelContinue(); | |
}, | |
levelContinue: function() { | |
console.log("LEVEL", GAME.level, "continues ..."); | |
ENGINE.GAME.ANIMATION.STACK.push(GAME.run); | |
ENGINE.GAME.ANIMATION.queue(); | |
}, | |
levelStart: function() { | |
console.log(`%cStarting level ${GAME.level}`, GAME.CSS); | |
GAME.initLevel(GAME.level); | |
GAME.levelExecute(); | |
}, | |
nextLevel: function() { | |
GAME.level = GAME.prepareLevel; | |
console.log("creating next level: ", GAME.level); | |
if (GAME.level > INI.LAST_LEVEL) { | |
console.log("Game have been won or last level has been played."); | |
//add end game stuff | |
//TITLE.gameWon(); | |
//ENGINE.GAME.ANIMATION.stop(); | |
//GAME.endAnimationStart(); | |
//GAME.end(); | |
} else { | |
console.log("Starting next level:", GAME.level); | |
ENGINE.GAME.ANIMATION.waitThen(GAME.levelStart, 2); | |
} | |
}, | |
levelEnd: function() { | |
console.log("level", GAME.level, "ended."); | |
GAME.levelCompleted = true; | |
ENGINE.GAME.ANIMATION.STACK.push( | |
ENGINE.KEY.waitFor.bind(null, GAME.nextLevel) | |
); | |
TITLE.endLevel(); | |
ENGINE.GAME.ANIMATION.stop(); | |
}, | |
initLevel: function(level) { | |
if (!MAP[level].dungeonExist) { | |
CreateDungeon.create(level); | |
MAP[level].pw = MAP[level].width * ENGINE.INI.GRIDPIX; | |
MAP[level].ph = MAP[level].height * ENGINE.INI.GRIDPIX; | |
} else MAP[level].returning = true; | |
//MAP[level].pw = MAP[level].width * ENGINE.INI.GRIDPIX; | |
//MAP[level].ph = MAP[level].height * ENGINE.INI.GRIDPIX; | |
ENGINE.VIEWPORT.setMax({ x: MAP[level].pw, y: MAP[level].ph }); | |
MINIMAP.create(level); | |
//DEBUG | |
//let grid = MAP[GAME.level].DUNGEON.entrance.add(UP) | |
//MAP[level].DUNGEON.scrolls.push(new Scroll(grid, "Light", "explore")); | |
//grid = MAP[GAME.level].DUNGEON.entrance.add(DOWN) | |
//MAP[level].DUNGEON.scrolls.push(new Scroll(grid, "Light", "explore")); | |
DEBUG.addScrolls(); | |
// | |
}, | |
updateVieport: function() { | |
if (!ENGINE.VIEWPORT.changed) return; | |
// do required repaints | |
ENGINE.VIEWPORT.change("floor", "background"); | |
ENGINE.VIEWPORT.change("config", "background"); | |
ENGINE.clearLayer("fogview"); | |
ENGINE.VIEWPORT.change("fog", "fogview"); | |
if (DEBUG.coord) { | |
ENGINE.clearLayer("coordview"); | |
ENGINE.VIEWPORT.change("coord", "coordview"); | |
} | |
// | |
ENGINE.VIEWPORT.changed = false; | |
}, | |
frameDraw: function() { | |
ENGINE.clearLayerStack(); | |
GAME.updateVieport(); | |
TEXTPOOL.draw("animation"); | |
SpritePOOL.draw("animation"); | |
EXPLOSIONS.draw(); | |
HERO.draw(); | |
ORBS.draw(); | |
TITLE.time(); | |
TITLE.status(); | |
ENEMY.draw(); | |
}, | |
firstFrameDraw: function(level) { | |
ENGINE.resizeBOX("LEVEL", MAP[level].pw, MAP[level].ph); | |
GRID.repaint( | |
MAP[level].grid, | |
TEXTURE[MAP[level].floor], | |
TEXTURE[MAP[level].background] | |
); | |
ENGINE.flattenLayers("wall", "floor"); | |
GAME.PAINT.config(); | |
if (DEBUG.fog) { | |
ENGINE.fill(LAYER.fog, TEXTURE.Fog); | |
GAME.adjustFogToMap(); | |
} | |
if (DEBUG.coord) GAME.PAINT.coord(); | |
ENGINE.VIEWPORT.changed = true; | |
HERO.updView(); | |
GAME.updateVieport(); | |
TITLE.main(); | |
TITLE.time(); | |
TITLE.status(); | |
ENGINE.clearLayer("actors"); | |
ENGINE.clearLayer("explosion"); | |
}, | |
adjustFogToMap: function() { | |
console.log("Adjusting fog from MINIMAP"); | |
for (let x = 0; x < MAP[GAME.level].width; x++) { | |
for (let y = 0; y < MAP[GAME.level].height; y++) { | |
let grid = new Grid(x, y); | |
if (MINIMAP.maps[GAME.level].map[GRID.gridToIndex(grid)] & 64) { | |
ENGINE.cutGrid(LAYER.fog, GRID.gridToCoord(grid)); | |
} | |
} | |
} | |
}, | |
run: function() { | |
//let t1 = performance.now(); | |
GAME.frameCount++; | |
//GAME.run() template | |
if (ENGINE.GAME.stopAnimation) return; | |
//do all game loop stuff here | |
GAME.respond(); | |
ORBS.manage(); | |
ENEMY.manage(); | |
HERO.manage(); | |
GAME.levelTime.update(); | |
// | |
GAME.frameDraw(); | |
//let t2 = performance.now() - t1; | |
//console.log("Frame", t2); | |
//if (GAME.frameCount > 100) GAME.abort(); | |
}, | |
respond: function() { | |
//GAME.respond() template | |
if (HERO.dead) return; | |
var map = ENGINE.GAME.keymap; | |
//fall throught section | |
if (map[ENGINE.KEY.map.F9]) { | |
//GAME.abort(); | |
//teleport to temple | |
/* | |
console.log("teleporting to temple room"); | |
GRID.teleportToGrid(HERO, MAP[GAME.level].DUNGEON.temple, true); | |
HERO.updView(); | |
*/ | |
//cheats | |
/* | |
console.log("CHEATING"); | |
HERO.armor += 10; | |
HERO.weapon += 10; | |
HERO.agility +=10; | |
HERO.heal(); | |
HERO.incMana(); | |
TITLE.change(); | |
*/ | |
//teleport to exit and purge enemies | |
console.log("teleport to exit"); | |
//MAP[GAME.level].DUNGEON.ENEMY.clear(); | |
GRID.teleportToGrid(HERO, MAP[GAME.level].DUNGEON.exit.add(UP), true); | |
HERO.updView(); | |
} | |
if (map[ENGINE.KEY.map.F8]) { | |
console.log("instaDeath"); | |
HERO.death(); | |
} | |
if (map[ENGINE.KEY.map.tab]) { | |
if (!HERO.canLevelUp) return; | |
GAME.levelUp(INI.PTS_LVL); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.tab] = false; //NO repeat | |
} | |
if (map[ENGINE.KEY.map.A]) { | |
TITLE.stack.scrollIndex--; | |
TITLE.stack.scrollIndex = Math.max(0, TITLE.stack.scrollIndex); | |
TITLE.change(); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.A] = false; | |
} | |
if (map[ENGINE.KEY.map.D]) { | |
TITLE.stack.scrollIndex++; | |
TITLE.stack.scrollIndex = Math.min( | |
HERO.scrolls.size() - 1, | |
TITLE.stack.scrollIndex | |
); | |
TITLE.change(); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.D] = false; | |
} | |
if (map[ENGINE.KEY.map.H]) { | |
HERO.useHealingPotion(); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.H] = false; //NO repeat | |
} | |
if (map[ENGINE.KEY.map.M]) { | |
HERO.useManaPotion(); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.M] = false; //NO repeat | |
} | |
if (map[ENGINE.KEY.map.ctrl]) { | |
HERO.castMagic(); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.ctrl] = false; //NO repeat | |
} | |
if (map[ENGINE.KEY.map.space]) { | |
console.log("SPACE"); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.space] = false; //NO repeat | |
} | |
if (map[ENGINE.KEY.map.enter]) { | |
if (HERO.scrolls.size() === 0) return; | |
let scroll = HERO.scrolls.remove(TITLE.stack.scrollIndex); | |
scroll.action(); | |
TITLE.change(); | |
ENGINE.GAME.keymap[ENGINE.KEY.map.enter] = false; //NO repeat | |
} | |
//single key section | |
if (map[ENGINE.KEY.map.left]) { | |
HERO.changeDirection(LEFT); | |
return; | |
} | |
if (map[ENGINE.KEY.map.right]) { | |
HERO.changeDirection(RIGHT); | |
return; | |
} | |
if (map[ENGINE.KEY.map.up]) { | |
HERO.changeDirection(UP); | |
return; | |
} | |
if (map[ENGINE.KEY.map.down]) { | |
HERO.changeDirection(DOWN); | |
return; | |
} | |
return; | |
}, | |
setup: function() { | |
console.log("%cGAME SETUP started", PRG.CSS); | |
}, | |
end: function() { | |
console.log("GAME ENDED"); | |
GAME.checkScore(); | |
}, | |
PAINT: { | |
coord: function(layer = "coord") { | |
ENGINE.clearLayer(layer); | |
for (let x = 0; x < MAP[GAME.level].width; x++) { | |
for (let y = 0; y < MAP[GAME.level].height; y++) { | |
if (!GRID.isBlock(x, y)) { | |
let point = GRID.gridToCoord(new Grid(x, y)); | |
let text = `${x},${y}`; | |
GRID.paintText(point, text, layer); | |
} | |
} | |
} | |
}, | |
config: function() { | |
ENGINE.VIEWPORT.changed = true; | |
ENGINE.clearLayer("config"); | |
let layer = "config"; | |
ENGINE.spriteToGrid( | |
layer, | |
MAP[GAME.level].DUNGEON.entrance, | |
SPRITE.Entrance | |
); | |
ENGINE.spriteToGrid(layer, MAP[GAME.level].DUNGEON.exit, SPRITE.Exit); | |
ENGINE.spriteToGrid(layer, MAP[GAME.level].DUNGEON.temple, SPRITE.temple); | |
if (MAP[GAME.level].DUNGEON.gate) | |
ENGINE.spriteToGrid(layer, MAP[GAME.level].DUNGEON.gate, SPRITE.Gate); | |
if (MAP[GAME.level].DUNGEON.door) | |
ENGINE.spriteToGrid(layer, MAP[GAME.level].DUNGEON.door, SPRITE.Door); | |
if (MAP[GAME.level].DUNGEON.goldKey) | |
ENGINE.spriteToGrid( | |
layer, | |
MAP[GAME.level].DUNGEON.goldKey, | |
SPRITE.goldKey | |
); | |
if (MAP[GAME.level].DUNGEON.silverKey) | |
ENGINE.spriteToGrid( | |
layer, | |
MAP[GAME.level].DUNGEON.silverKey, | |
SPRITE.silverKey | |
); | |
GAME.PAINT.gold(layer); | |
GAME.PAINT.lamp(layer); | |
GAME.PAINT.potion(layer); | |
GAME.PAINT.boost(layer); | |
GAME.PAINT.chest(layer); | |
GAME.PAINT.scroll(layer); | |
}, | |
gold: function(layer) { | |
for (let q = 0, GL = MAP[GAME.level].DUNGEON.gold.length; q < GL; q++) { | |
let gold = MAP[GAME.level].DUNGEON.gold[q]; | |
ENGINE.spriteToGrid(layer, gold.grid, gold.sprite); | |
} | |
}, | |
lamp: function(layer) { | |
for (let q = 0, GL = MAP[GAME.level].DUNGEON.lamps.length; q < GL; q++) { | |
let lamp = MAP[GAME.level].DUNGEON.lamps[q]; | |
ENGINE.spriteToGrid(layer, lamp.grid, lamp.sprite); | |
} | |
}, | |
potion: function(layer) { | |
for ( | |
let q = 0, GL = MAP[GAME.level].DUNGEON.potions.length; | |
q < GL; | |
q++ | |
) { | |
let potion = MAP[GAME.level].DUNGEON.potions[q]; | |
ENGINE.spriteToGrid(layer, potion.grid, potion.sprite); | |
} | |
}, | |
boost: function(layer) { | |
for (let q = 0, GL = MAP[GAME.level].DUNGEON.boosts.length; q < GL; q++) { | |
let boost = MAP[GAME.level].DUNGEON.boosts[q]; | |
ENGINE.spriteToGrid(layer, boost.grid, boost.sprite); | |
} | |
}, | |
chest: function(layer) { | |
for (let q = 0, GL = MAP[GAME.level].DUNGEON.chests.length; q < GL; q++) { | |
let chest = MAP[GAME.level].DUNGEON.chests[q]; | |
ENGINE.spriteToGrid(layer, chest.grid, chest.sprite); | |
} | |
}, | |
scroll: function(layer) { | |
for ( | |
let q = 0, GL = MAP[GAME.level].DUNGEON.scrolls.length; | |
q < GL; | |
q++ | |
) { | |
let scroll = MAP[GAME.level].DUNGEON.scrolls[q]; | |
ENGINE.spriteToGrid(layer, scroll.grid, scroll.sprite); | |
} | |
} | |
}, | |
CLICK: { | |
sacrificeGold: function() { | |
HERO.gold -= 1000; | |
HERO.points += 1; | |
GAME.templeRefresh(); | |
}, | |
manageTemple: function() { | |
GAME.CLICK.decPoint(this); | |
GAME.templeRefresh(); | |
}, | |
manageCharacter: function() { | |
GAME.CLICK.decPoint(this); | |
GAME.heroRefresh(); | |
}, | |
decPoint: function(that) { | |
HERO.points -= 1; | |
let id = that.id; | |
id = id.substr(id.lastIndexOf("_") + 1); | |
if (id === "maxHealth") { | |
HERO[id] += INI.LVL_HEALTH; | |
} else if (id === "maxMana") { | |
HERO[id] += INI.LVL_MANA; | |
} else HERO[id]++; | |
}, | |
endFight: function() { | |
$("#form_continue").prop("disabled", false); | |
$("#form_make_turn").prop("disabled", true); | |
$("#form_flee").prop("disabled", true); | |
}, | |
usePotion: function() { | |
let healed = HERO.useHealingPotion(); | |
//console.log("using red potion for", healed, "HP"); | |
GAME.fightRefresh(); | |
if (healed === 0) { | |
CONSOLE.print( | |
`<span class="blue">${HERO.name}</span> has full health already.` | |
); | |
} else { | |
CONSOLE.print(` | |
<span class="blue">${HERO.name}</span> heals ${healed} points. | |
`); | |
} | |
}, | |
useScroll: function() { | |
const regex = /_([a-zA-Z]+)/; | |
let type = regex.exec(this.id)[1]; | |
let temp = new Scroll(null, type, null); | |
temp.action(); | |
HERO.scrolls.remove(HERO.scrolls.find("type", type)); | |
GAME.fightRefresh(); | |
} | |
}, | |
levelUp: function(points) { | |
console.log("%cCharacter screen", PRG.CSS); | |
ENGINE.GAME.ANIMATION.stop(); | |
HERO.canLevelUp = false; | |
let x = ENGINE.gameWIDTH / 4; | |
let y = ENGINE.gameHEIGHT / 4; | |
let w = ENGINE.gameWIDTH / 2 + 16; | |
let h = ENGINE.gameHEIGHT / 2 - 50; | |
HERO.level++; | |
HERO.points = points; | |
HERO.maxHealth += INI.LVL_HEALTH; | |
HERO.maxMana += INI.LVL_MANA; | |
const temp = character(); | |
function character() { | |
const temp = new Form(HERO.name, x, y, w, h, FORM_WEDGE.HERO); | |
FORM_WEDGE.hero(); | |
GAME.heroRefresh(); | |
return temp; | |
} | |
}, | |
visitTemple: function() { | |
console.log("%cEntered temple", PRG.CSS); | |
ENGINE.GAME.ANIMATION.stop(); | |
let x = ENGINE.gameWIDTH / 4; | |
let y = ENGINE.gameHEIGHT / 4; | |
let w = ENGINE.gameWIDTH / 2 + 16; | |
let h = ENGINE.gameHEIGHT / 2 + 32; | |
const temp = temple(); | |
function temple() { | |
const temp = new Form("The Temple", x, y, w, h, FORM_WEDGE.TEMPLE); | |
FORM_WEDGE.temple(); | |
GAME.templeRefresh(); | |
return temp; | |
} | |
}, | |
heroRefresh: function() { | |
if (HERO.points > 0) { | |
$("#form_done").prop("disabled", true); | |
$(".skill").prop("disabled", false); | |
} else { | |
$("#form_done").prop("disabled", false); | |
$(".skill").prop("disabled", true); | |
} | |
$("#hero_points").html(HERO.points); | |
$("#hero_sword").html(HERO.weapon.toString().padStart(2, "0")); | |
$("#hero_shield").html(HERO.armor.toString().padStart(2, "0")); | |
$("#hero_agility").html(HERO.agility.toString().padStart(2, "0")); | |
$("#hero_magic").html(HERO.magic.toString().padStart(2, "0")); | |
HERO.health = HERO.maxHealth; | |
HERO.mana = HERO.maxMana; | |
TITLE.change(); | |
TITLE.status(); | |
}, | |
templeRefresh: function() { | |
$("#hero_gold").html(HERO.gold); | |
if (HERO.gold < 1000) { | |
$("#form_sacrifice_gold").prop("disabled", true); | |
} else $("#form_sacrifice_gold").prop("disabled", false); | |
if (HERO.points === 0) { | |
$(".skill").prop("disabled", true); | |
} else $(".skill").prop("disabled", false); | |
$("#hero_points").html(HERO.points); | |
$("#hero_vitality").html(HERO.maxHealth.toString().padStart(2, "0")); | |
$("#hero_mana").html(HERO.maxMana.toString().padStart(2, "0")); | |
$("#hero_sword").html(HERO.weapon.toString().padStart(2, "0")); | |
$("#hero_shield").html(HERO.armor.toString().padStart(2, "0")); | |
$("#hero_agility").html(HERO.agility.toString().padStart(2, "0")); | |
$("#hero_magic").html(HERO.magic.toString().padStart(2, "0")); | |
HERO.health = HERO.maxHealth; | |
HERO.mana = HERO.maxMana; | |
TITLE.change(); | |
TITLE.status(); | |
}, | |
fightRefresh: function() { | |
let enemy = GAME.TURN.enemy; | |
$("#hero_sword").html(HERO.weapon.toString().padStart(2, "0")); | |
$("#hero_shield").html(HERO.armor.toString().padStart(2, "0")); | |
$("#hero_agility").html(HERO.agility.toString().padStart(2, "0")); | |
if (LOG[enemy.type.title].kills >= 1) { | |
$("#enemy_agility").html(enemy.agility.toString().padStart(2, "0")); | |
} else $("#enemy_agility").html("??"); | |
if (LOG[enemy.type.title].kills >= 2) { | |
$("#enemy_armor").html(enemy.armor.toString().padStart(2, "0")); | |
} else $("#enemy_armor").html("??"); | |
if (LOG[enemy.type.title].kills >= 3) { | |
$("#enemy_weapon").html(enemy.weapon.toString().padStart(2, "0")); | |
} else $("#enemy_weapon").html("??"); | |
ENGINE.clearLayer("hero_health"); | |
ENGINE.clearLayer("enemy_health"); | |
ENGINE.statusBar( | |
LAYER.hero_health, | |
0, | |
0, | |
INI.FIGHT_PANEL_WIDTH / 2 - 10, | |
20, | |
HERO.health, | |
HERO.maxHealth, | |
"red" | |
); | |
ENGINE.statusBar( | |
LAYER.enemy_health, | |
0, | |
0, | |
INI.FIGHT_PANEL_WIDTH / 2 - 10, | |
20, | |
enemy.health, | |
enemy.maxHealth, | |
"orange" | |
); | |
$("#count_redPotion").html(HERO.redPotion.toString().padStart(2, "0")); | |
if (HERO.redPotion > 0) { | |
$("#redPotion").prop("disabled", false); | |
} else { | |
$("#redPotion").prop("disabled", true); | |
//delete if none | |
$("#redPotion").remove(); | |
$("#count_redPotion").remove(); | |
} | |
scrollPanel(); | |
TITLE.forceChange(); | |
return; | |
function scrollPanel() { | |
let scrolls = [ | |
"BoostWeapon", | |
"BoostArmor", | |
"DestroyWeapon", | |
"DestroyArmor" | |
]; | |
scrolls.forEach(scroll => { | |
let count = HERO.scrolls.getCount("type", scroll); | |
$(`#count_${scroll}`).html(count.toString().padStart(2, "0")); | |
if (count === 0) { | |
$(`#SCR_${scroll}`).prop("disabled", true); | |
//delete those not present | |
$(`#SCR_${scroll}`).remove(); | |
$(`#count_${scroll}`).remove(); | |
} else $(`#SCR_${scroll}`).prop("disabled", false); | |
}); | |
} | |
}, | |
leaveTemple: function() { | |
console.log("leaving temple ..."); | |
HERO.canEnterTemple = false; | |
$("#FORM").remove(); | |
setTimeout(() => { | |
console.log("HERO.canEnterTemple"); | |
HERO.canEnterTemple = true; | |
}, INI.TEMPLE_TIMEOUT); | |
GAME.levelContinue(); | |
}, | |
endFight: function() { | |
console.log("fight ends ..."); | |
$("#FORM").remove(); | |
HERO.inFight = false; | |
GAME.TURN.fight_active = false; | |
HERO.weapon = GAME.TURN.HERO_weapon; | |
HERO.armor = GAME.TURN.HERO_armor; | |
GAME.levelContinue(); | |
}, | |
fleeFight: function() { | |
let enemy = GAME.TURN.enemy; | |
CONSOLE.print(`<span class="blue">${HERO.name}</span> tries to flee ...`); | |
let delta = HERO.agility - enemy.agility; | |
const top = 2 * INI.FLEE_AGILITY_DELTA; | |
let chance; | |
if (delta > top) { | |
chance = top; | |
} else { | |
chance = RND(-top + delta, top); | |
} | |
let selectedDir = safeSpot(); //bugfix for dead ends | |
if ( | |
chance > 0 && | |
GAME.TURN.agility_accumulator > INI.AGILITY_TURN * -1 && | |
selectedDir !== null | |
) { | |
CONSOLE.print(` ... and succeeds.`); | |
//let selectedDir = safeSpot(); | |
HERO.MoveState.dir = selectedDir; | |
GRID.blockMove(HERO, true); | |
HERO.updView(); | |
GAME.CLICK.endFight(); | |
} else { | |
CONSOLE.print(` ... and fails ...`); | |
GAME.TURN.agility_accumulator += chance; | |
enemy.turn(); | |
GAME.fightRefresh(); //bugfix | |
} | |
function safeSpot() { | |
let obstacles = MAP[GAME.level].DUNGEON.obstacles.clone(); | |
obstacles.push(enemy.MoveState.homeGrid); | |
let dirs = GRID.getDirections(HERO.MoveState.startGrid, obstacles); | |
if (dirs.length === 0) return null; | |
let selectedDir; | |
let enemyDirIndex = enemy.MoveState.dir.isInAt(dirs); | |
if (enemyDirIndex >= 0) { | |
selectedDir = dirs[enemyDirIndex]; | |
} else { | |
selectedDir = dirs.chooseRandom(); | |
} | |
return selectedDir; | |
} | |
}, | |
charDone: function() { | |
console.log("levelUp done"); | |
$("#FORM").remove(); | |
GAME.levelContinue(); | |
}, | |
debug: function() {}, | |
fight: function(enemy, initiative, index) { | |
if (HERO.inFight) return; | |
if (HERO.dead) return; | |
if (!HERO.MoveState.moving) initiative = false; | |
if (initiative) { | |
initiative = 1; | |
} else initiative = 0; | |
HERO.inFight = true; | |
ENGINE.GAME.ANIMATION.stop(); | |
let x = ENGINE.gameWIDTH / 4; | |
let y = ENGINE.gameHEIGHT / 4 - 60; | |
let w = ENGINE.gameWIDTH / 2 + 16; | |
let h = ENGINE.gameHEIGHT / 2 + 32 + 168; | |
INI.FIGHT_PANEL_WIDTH = w; | |
GAME.TURN.enemy = enemy; | |
kills(); | |
fight(enemy); | |
CONSOLE.set("Console"); | |
GAME.TURN.agility_accumulator = 0; | |
GAME.TURN.counter = 1; | |
GAME.TURN.HERO_weapon = HERO.weapon; | |
GAME.TURN.HERO_armor = HERO.armor; | |
//GAME.TURN.enemy = enemy; | |
GAME.TURN.fight_active = true; | |
GAME.TURN.enemyIndex = index; | |
let delta = HERO.agility - enemy.agility; | |
if (delta + initiative * INI.INITIATIVE_BONUS >= 0) { | |
//hero starts | |
CONSOLE.print( | |
`<span class="blue">${HERO.name}</span> attacks <span class="red">${ | |
enemy.type.title | |
}</span>.` | |
); | |
} else { | |
//enemy attacks | |
CONSOLE.print( | |
`<span class="red">${ | |
enemy.type.title | |
}</span> attacks <span class="blue">${HERO.name}</span>.` | |
); | |
enemy.turn(enemy); | |
} | |
//GAME.fightRefresh(enemy); | |
GAME.fightRefresh(); | |
return; | |
function fight(enemy) { | |
const temp = new Form("Fight!", x, y, w, h, FORM_WEDGE.FIGHT); | |
FORM_WEDGE.fight(enemy); | |
GAME.fightRefresh(); | |
return temp; | |
} | |
function kills() { | |
if (LOG[enemy.type.title] === undefined) { | |
LOG[enemy.type.title] = new Log(); | |
} | |
return LOG[enemy.type.title].kills; | |
} | |
}, | |
turn: function() { | |
if (!GAME.TURN.fight_active) return; | |
GAME.TURN.setQuickResolve(); | |
let enemy = GAME.TURN.enemy; | |
GAME.TURN.counter++; | |
let delta = HERO.agility - enemy.type.agility; | |
GAME.TURN.agility_accumulator += delta; | |
//console.log("GAME.TURN.agility_accumulator", GAME.TURN.agility_accumulator); | |
if (GAME.TURN.agility_accumulator > INI.AGILITY_TURN * -1) { | |
//hero turn | |
HERO.turn(enemy); | |
} else { | |
//hero skips turn | |
GAME.TURN.agility_accumulator += INI.AGILITY_TURN; | |
CONSOLE.print( | |
`<span class="blue">${HERO.name}</span> fell behind and missed turn.` | |
); | |
} | |
if (!GAME.TURN.fight_active) return; | |
if (GAME.TURN.agility_accumulator < INI.AGILITY_TURN) { | |
enemy.turn(); | |
} else { | |
GAME.TURN.agility_accumulator -= INI.AGILITY_TURN; | |
CONSOLE.print( | |
`<span class="red">${ | |
enemy.type.title | |
}</span> fell behind and missed turn.` | |
); | |
} | |
GAME.fightRefresh(enemy); | |
TITLE.forceChange(); | |
if (GAME.TURN.fight_active && GAME.turn.quickResolve) { | |
return GAME.turn(); | |
} else return; | |
}, | |
TURN: { | |
counter: 0, | |
agility_accumulator: 0, | |
HERO_weapon: null, | |
HERO_armor: null, | |
fight_active: null, | |
enemy: null, | |
quickResolve: false, | |
setQuickResolve: function() { | |
if ($("#form_quick_resolve").prop("checked")) { | |
GAME.turn.quickResolve = true; | |
} else GAME.turn.quickResolve = false; | |
}, | |
enemyIndex: null, | |
damage: function(attacker, defender) { | |
if (attacker.weapon === 0) return 0; | |
//let delta = Math.max(attacker.weapon - defender.armor, 1); | |
//let damage = RND(INI.ATTACk_OFFSET, delta); | |
let delta = attacker.weapon - defender.armor; | |
let damage = RND( | |
Math.min(INI.ATTACk_OFFSET, Math.floor(delta / 2)), | |
Math.max(delta, 1) | |
); | |
return damage; | |
} | |
}, | |
over: function() { | |
//$("#Restart").remove(); | |
$("#buttons").prepend( | |
`<input type='button' id='Restart' value='Restart game'>` | |
); | |
$("#Restart").on("click", GAME.restartGame); | |
}, | |
restartGame: function() { | |
//$("#Restart").addClass("hidden"); | |
$("#Restart").remove(); | |
console.log("GAME RESTARTED"); | |
GAME.discardMaps(); | |
GAME.start(); | |
}, | |
discardMaps: function() { | |
console.log("discarding old maps"); | |
MINIMAP.maps = {}; | |
MAP.clear(); | |
LOG = {}; | |
} | |
}; | |
var MINIMAP = { | |
/* | |
1 - wall, path (0), | |
2 - door/gate | |
4 - key | |
8 - entrance, exit | |
16 - temple | |
32 - | |
64 - fog info | |
128 - fog | |
*/ | |
key: "#00F", | |
door: "#F00", | |
wall: "#8B4513", | |
path: "#000", | |
hero: "#0F0", | |
temple: "yellow", | |
exit: "#FFF", | |
maps: {}, | |
create: function(level) { | |
console.log("MINIMAP creation - level:", level); | |
if (MINIMAP.maps[GAME.level] === undefined) { | |
MINIMAP.maps[GAME.level] = {}; | |
MINIMAP.maps[GAME.level].buffer = new ArrayBuffer( | |
MAP[GAME.level].width * MAP[GAME.level].height | |
); | |
MINIMAP.maps[GAME.level].map = new Uint8Array( | |
MINIMAP.maps[GAME.level].buffer | |
); | |
let index; | |
for (let y = 0; y < MAP[GAME.level].height; y++) { | |
for (let x = 0; x < MAP[GAME.level].width; x++) { | |
index = x + y * MAP[GAME.level].width; | |
if (GRID.isBlock(x, y)) { | |
MINIMAP.maps[GAME.level].map[index] = 0b10000000; | |
} else MINIMAP.maps[GAME.level].map[index] = 0b10000001; | |
} | |
} | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.door) | |
] |= 2; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.gate) | |
] |= 2; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.silverKey) | |
] |= 4; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.goldKey) | |
] |= 4; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.entrance) | |
] |= 8; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.exit) | |
] |= 8; | |
MINIMAP.maps[GAME.level].map[ | |
GRID.gridToIndex(MAP[GAME.level].DUNGEON.temple) | |
] |= 16; | |
//reserve 64 for fog storage | |
} else console.log(`MINIMAP to ${GAME.level} already exists`); | |
//console.log("MINIMAP in MINIMAP", MINIMAP); | |
}, | |
draw: function(x, y, CTX) { | |
MINIMAP.maps[GAME.level].map.forEach((value, index) => { | |
if (value < 128) { | |
if (value & 16) { | |
CTX.fillStyle = MINIMAP.temple; | |
} else if (value & 8) { | |
CTX.fillStyle = MINIMAP.exit; | |
} else if (value & 4) { | |
CTX.fillStyle = MINIMAP.key; | |
} else if (value & 2) { | |
CTX.fillStyle = MINIMAP.door; | |
} else if (value & 1) { | |
CTX.fillStyle = MINIMAP.path; | |
} else { | |
CTX.fillStyle = MINIMAP.wall; | |
} | |
let grid = GRID.indexToGrid(index); | |
CTX.pixelAt( | |
x + grid.x * INI.MINI_PIX, | |
y + grid.y * INI.MINI_PIX, | |
INI.MINI_PIX | |
); | |
} | |
CTX.fillStyle = MINIMAP.hero; | |
CTX.pixelAt( | |
x + HERO.MoveState.homeGrid.x * INI.MINI_PIX, | |
y + HERO.MoveState.homeGrid.y * INI.MINI_PIX, | |
INI.MINI_PIX | |
); | |
}); | |
}, | |
unveil: function(grid, r = INI.MAP_RADIUS) { | |
let startX = Math.max(grid.x - r, 0); | |
let startY = Math.max(grid.y - r, 0); | |
let maxX = Math.min(MAP[GAME.level].DUNGEON.maxX + 1, startX + 2 * r); | |
let maxY = Math.min(MAP[GAME.level].DUNGEON.maxY + 1, startY + 2 * r); | |
for (let y = startY; y <= maxY; y++) { | |
for (let x = startX; x <= maxX; x++) { | |
let index = x + y * MAP[GAME.level].width; | |
MINIMAP.maps[GAME.level].map[index] &= 0b01111111; | |
} | |
} | |
} | |
}; | |
var TITLE = { | |
stack: { | |
scrollIndex: 0, | |
scrollInRow: 3, | |
scrollDelta: 72 | |
}, | |
change: function() { | |
TITLE.STATUS.changed = true; | |
}, | |
STATUS: { | |
changed: true, | |
layer: "status" | |
}, | |
main: function() { | |
TITLE.title(); | |
TITLE.bottom(); | |
TITLE.side(); | |
}, | |
title: function() { | |
var CTX = LAYER.title; | |
TITLE.background(); | |
var fs = 42; | |
CTX.font = fs + "px Arcade"; | |
CTX.textAlign = "center"; | |
var txt = CTX.measureText(PRG.NAME); | |
var x = ENGINE.titleWIDTH / 2; | |
var y = fs + 10; | |
var gx = x - txt.width / 2; | |
var gy = y - fs; | |
var grad = CTX.createLinearGradient(gx, gy + 10, gx, gy + fs); | |
grad.addColorStop("0", "#0F0"); | |
grad.addColorStop("0.1", "#0E0"); | |
grad.addColorStop("0.2", "#0D0"); | |
grad.addColorStop("0.3", "#0C0"); | |
grad.addColorStop("0.4", "#0B0"); | |
grad.addColorStop("0.5", "#0A0"); | |
grad.addColorStop("0.6", "#090"); | |
grad.addColorStop("0.7", "#080"); | |
grad.addColorStop("0.8", "#070"); | |
grad.addColorStop("0.9", "#060"); | |
grad.addColorStop("1", "#050"); | |
GAME.grad = grad; | |
CTX.fillStyle = grad; | |
CTX.shadowColor = "#040"; | |
CTX.shadowOffsetX = 2; | |
CTX.shadowOffsetY = 2; | |
CTX.shadowBlur = 3; | |
CTX.fillText(PRG.NAME, x, y); | |
}, | |
background: function() { | |
var CTX = LAYER.title; | |
CTX.fillStyle = "#000"; | |
CTX.roundRect( | |
0, | |
0, | |
ENGINE.titleWIDTH, | |
ENGINE.titleHEIGHT, | |
{ | |
upperLeft: 20, | |
upperRight: 20, | |
lowerLeft: 0, | |
lowerRight: 0 | |
}, | |
true, | |
true | |
); | |
}, | |
bottom: function() { | |
var CTX = LAYER.bottom; | |
CTX.fillStyle = "#000"; | |
CTX.roundRect( | |
0, | |
0, | |
ENGINE.bottomWIDTH, | |
ENGINE.bottomHEIGHT, | |
{ | |
upperLeft: 0, | |
upperRight: 0, | |
lowerLeft: 20, | |
lowerRight: 20 | |
}, | |
true, | |
true | |
); | |
CTX.textAlign = "center"; | |
var x = ENGINE.bottomWIDTH / 2; | |
var y = ENGINE.bottomHEIGHT / 2; | |
CTX.font = "10px Consolas"; | |
CTX.fillStyle = GAME.grad; | |
CTX.shadowOffsetX = 2; | |
CTX.shadowOffsetY = 2; | |
CTX.shadowBlur = 5; | |
CTX.shadowColor = "#040"; | |
CTX.fillText("Version " + PRG.VERSION + " by Lovro Selič", x, y); | |
}, | |
side() { | |
ENGINE.clearLayer("sideback"); | |
ENGINE.fillLayer("sideback", "#000"); | |
}, | |
forceChange: function() { | |
TITLE.change(); | |
TITLE.status(); | |
}, | |
status: function() { | |
if (!TITLE.STATUS.changed) return; | |
TITLE.STATUS.changed = false; | |
ENGINE.clearLayer("status"); | |
TITLE.hero(); | |
TITLE.time(); | |
TITLE.health(); | |
TITLE.gold(); | |
TITLE.inv(); | |
TITLE.skills(); | |
TITLE.scrolls(); | |
TITLE.map(); | |
}, | |
scrolls: function() { | |
TITLE.stack.scrollIndex = Math.min( | |
TITLE.stack.scrollIndex, | |
HERO.scrolls.size() - 1 | |
); | |
let scrollSpread = ENGINE.spreadAroundCenter( | |
TITLE.stack.scrollInRow, | |
ENGINE.sideWIDTH / 2 - 16, | |
TITLE.stack.scrollDelta | |
); | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
CTX.save(); | |
ENGINE.resetShadow(CTX); | |
let x = 16; | |
let fs = 16; | |
let y = TITLE.stack.y + 4; | |
let LN = HERO.scrolls.size(); | |
let start = Math.max( | |
0, | |
TITLE.stack.scrollIndex - TITLE.stack.scrollInRow + 1 | |
); | |
start = Math.min(start, LN - TITLE.stack.scrollInRow); | |
if (start < 0) start = 0; | |
let max = start + Math.min(TITLE.stack.scrollInRow, LN); | |
for (let q = start; q < max; q++) { | |
let scroll = HERO.scrolls.list[q]; | |
if (scroll.object.use === "explore") { | |
CTX.globalAlpha = 1; | |
CTX.strokeStyle = "#0F0"; | |
} else { | |
CTX.globalAlpha = 0.3; | |
CTX.strokeStyle = "#F00"; | |
} | |
x = scrollSpread.shift(); | |
ENGINE.draw(TITLE.STATUS.layer, x, y, scroll.object.sprite); | |
CTX.font = "10px Consolas"; | |
CTX.fillStyle = "#FFF"; | |
CTX.fillText( | |
scroll.count.toString().padStart(2, "0"), | |
x + 32, | |
y + 18 + 4 | |
); | |
if (q === TITLE.stack.scrollIndex) { | |
CTX.globalAlpha = 0.5; | |
CTX.lineWidth = "1"; | |
CTX.beginPath(); | |
CTX.rect(x - 14, y - 3, 60, 44); | |
CTX.closePath(); | |
CTX.stroke(); | |
} | |
} | |
CTX.globalAlpha = 1; | |
y += 2 * fs + 12; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
CTX.restore(); | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineBottom); | |
y += SPRITE.LineBottom.height + 4; | |
TITLE.stack.y = y; | |
}, | |
map: function() { | |
var CTX = LAYER.map; | |
ENGINE.clearLayer("map"); | |
let y = TITLE.stack.y + 14; | |
let x = 28; | |
CTX.strokeStyle = "#FFF"; | |
CTX.beginPath(); | |
CTX.rect(x - 1, y - 1, 202, 202); | |
CTX.closePath(); | |
CTX.stroke(); | |
CTX.fillStyle = "#444"; | |
CTX.fillRect(x, y, 200, 200); | |
MINIMAP.draw(x, y, CTX); | |
}, | |
statusBar: function(x, y, value, max, color) { | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
let h = 16; | |
let w = 160; | |
ENGINE.statusBar(CTX, x, y, w, h, value, max, color); | |
}, | |
healthBar: function(x, y) { | |
TITLE.statusBar(x, y, HERO.health, HERO.maxHealth, "#F00"); | |
}, | |
manaBar: function(x, y) { | |
TITLE.statusBar(x, y, HERO.mana, HERO.maxMana, "#00F"); | |
}, | |
inv: function() { | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
let fs = 16; | |
let y = TITLE.stack.y + fs + 4; | |
let x; | |
let NUM = 2; | |
let delta = 80; | |
let xS = ENGINE.spreadAroundCenter(NUM, ENGINE.sideWIDTH / 2, delta); | |
x = xS.shift(); | |
ENGINE.spriteDraw(TITLE.STATUS.layer, x, y, SPRITE.RedPotion); | |
x += SPRITE.RedPotion.width / 2 + 5; | |
CTX.fillText(HERO.redPotion, x, y + 6); | |
x = xS.shift(); | |
ENGINE.spriteDraw(TITLE.STATUS.layer, x, y, SPRITE.BluePotion); | |
x += SPRITE.RedPotion.width / 2 + 5; | |
CTX.fillText(HERO.bluePotion, x, y + 6); | |
y += fs + 4; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineBottom); | |
y += SPRITE.LineBottom.height; | |
TITLE.stack.y = y; | |
}, | |
gold: function() { | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
CTX.save(); | |
CTX.fillStyle = "#CFB53B"; | |
CTX.shadowColor = "#DAA520"; | |
let fs = 16; | |
let y = TITLE.stack.y + 1.5 * fs; | |
let x = 16; | |
CTX.fillText("Gold:", x, y); | |
CTX.fillText(HERO.gold.toString().padStart(6, "0"), TITLE.stack.tab, y); | |
CTX.restore(); | |
y += fs; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineTop); | |
y += SPRITE.LineBottom.height; | |
TITLE.stack.y = y; | |
}, | |
skills: function() { | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
let fs = 16; | |
let y = TITLE.stack.y + 1.5 * fs; | |
let x = 16; | |
let pad1 = 80; | |
let pad2 = 32; | |
CTX.fillText("Sword:", x, y); | |
x += pad1; | |
CTX.fillText(HERO.weapon.toString().padStart(2, "0"), x, y); | |
x += pad2; | |
CTX.fillText("Shield:", x, y); | |
x += pad1; | |
CTX.fillText(HERO.armor.toString().padStart(2, "0"), x, y); | |
y += 1.5 * fs; | |
x = 16; | |
CTX.fillText("Agility:", x, y); | |
x += pad1; | |
CTX.fillText(HERO.agility.toString().padStart(2, "0"), x, y); | |
x += pad2; | |
CTX.fillText("Magic:", x, y); | |
x += pad1; | |
CTX.fillText(HERO.magic.toString().padStart(2, "0"), x, y); | |
y += fs - 2; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineBottom); | |
y += SPRITE.LineBottom.height + 2; | |
TITLE.stack.y = y; | |
}, | |
health: function() { | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
let fs = 16; | |
let y = TITLE.stack.y + 1.5 * fs; | |
let x = 16; | |
CTX.font = fs + "px Times"; | |
CTX.fillStyle = "#AAA"; | |
CTX.shadowColor = "#666"; | |
CTX.shadowOffsetX = 1; | |
CTX.shadowOffsetY = 1; | |
CTX.shadowBlur = 1; | |
if (HERO.canLevelUp) { | |
CTX.fillStyle = "#0F0"; | |
CTX.shadowColor = "#0D0"; | |
} | |
CTX.fillText("Level:", x, y); | |
CTX.fillText(HERO.level.toString().padStart(2, "0"), 100, y); | |
y += 1.5 * fs; | |
CTX.fillText("Experience:", x, y); | |
CTX.fillText(HERO.experience.toString().padStart(6, "0"), 100, y); | |
if (HERO.canLevelUp) { | |
CTX.fillStyle = "#00F"; | |
CTX.shadowColor = "#00D"; | |
CTX.font = "10px Arial"; | |
CTX.fillText("Press TAB for Level Up!", x + 116, y - 26); | |
} | |
CTX.font = fs + "px Times"; | |
CTX.fillStyle = "#AAA"; | |
CTX.shadowColor = "#666"; | |
y += 1.5 * fs; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineBottom); | |
y += SPRITE.LineBottom.height + 1.5 * fs; | |
x = 16; | |
CTX.fillText("Health:", x, y); | |
var bx, by; | |
inc(); | |
TITLE.healthBar(bx, by); | |
x = 16; | |
y += 1.5 * fs; | |
CTX.fillText("Mana:", x, y); | |
inc(); | |
TITLE.manaBar(bx, by); | |
y += 1.5 * fs; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineBottom); | |
y += SPRITE.LineBottom.height; | |
TITLE.stack.y = y; | |
TITLE.stack.tab = bx; | |
function inc() { | |
const pad = 3; | |
bx = x + 64; | |
by = y - fs + pad; | |
} | |
}, | |
time: function() { | |
var CTX = LAYER["time"]; | |
ENGINE.clearLayer("time"); | |
let fs = 14; | |
CTX.font = fs + "px Times"; | |
CTX.fillStyle = "#0D0"; | |
CTX.shadowColor = "#0F0"; | |
CTX.shadowOffsetX = 0; | |
CTX.shadowOffsetY = 0; | |
CTX.shadowBlur = 0; | |
CTX.fillText(GAME.time.timeString(), 180, 28); | |
}, | |
hero: function() { | |
let x = 48; | |
let y = 20; | |
let fs = 14; | |
ENGINE.spriteDraw(TITLE.STATUS.layer, x, y, SPRITE.Knight_front_0); | |
var CTX = LAYER[TITLE.STATUS.layer]; | |
x = 102; | |
CTX.font = fs + "px Times"; | |
CTX.fillStyle = "#0A0"; | |
CTX.shadowColor = "#0F0"; | |
CTX.shadowOffsetX = 0; | |
CTX.shadowOffsetY = 0; | |
CTX.shadowBlur = 0; | |
CTX.fillText(HERO.name, x, y); | |
y += fs + 4; | |
CTX.fillText(`Depth: ${GAME.level.toString().padStart(2, "0")}`, x, y); | |
//reset shadow | |
CTX.fillStyle = "#AAA"; | |
CTX.shadowColor = "#666"; | |
// | |
y = 26 + 32; | |
x = (ENGINE.sideWIDTH - SPRITE.LineTop.width) / 2; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineTop); | |
let delta = 48; | |
let NUM = HERO.inventory.size; | |
let xS = ENGINE.spreadAroundCenter(NUM, ENGINE.sideWIDTH / 2, delta); | |
y += 32; | |
fs = 16; | |
HERO.inventory.forEach(sprite => { | |
ENGINE.spriteDraw(TITLE.STATUS.layer, xS.shift(), y, sprite); | |
}); | |
y += fs; | |
ENGINE.draw(TITLE.STATUS.layer, x, y, SPRITE.LineBottom); | |
y += SPRITE.LineBottom.height; | |
TITLE.stack.y = y; | |
} | |
}; | |
class Monster { | |
constructor(type, grid, go = false) { | |
this.enemy = true; | |
this.type = type; | |
this.grid = Grid.toClass(grid); | |
this.awake = false; | |
this.actor = new ACTOR( | |
this.type.name, | |
0, | |
0, | |
"front", | |
ASSET[this.type.name] | |
); | |
GRID.gridToSprite(this.grid, this.actor); | |
this.alignToViewport(); | |
this.MoveState = new MoveState(this.grid, DOWN); | |
this.dirStack = []; | |
this.strategy = this.type.strategy; | |
this.speed = this.type.speed; | |
this.visible = false; | |
this.health = this.type.health; | |
this.weapon = this.type.weapon; | |
this.armor = this.type.armor; | |
this.maxHealth = this.type.health; | |
this.mana = this.type.mana; | |
this.magic = this.type.magic; | |
this.agility = this.type.agility; | |
//this.casted(2000); | |
this.casted(0); | |
if (go) this.wake(); | |
/* | |
strategy: | |
wander, hunt | |
goto, guard (door, key) | |
seek | |
flee, wander | |
*/ | |
} | |
alignToViewport() { | |
ENGINE.VIEWPORT.alignTo(this.actor); | |
} | |
makeMove() { | |
this.MoveState.dir = this.dirStack.shift(); | |
this.MoveState.next(this.MoveState.dir); | |
} | |
wake() { | |
//console.log("waking", this); | |
this.awake = true; | |
} | |
sleep() { | |
this.awake = false; | |
this.hide(); | |
} | |
show() { | |
this.visible = true; | |
} | |
hide() { | |
this.visible = false; | |
} | |
isVisible() { | |
let targetGrid = this.MoveState.closerGrid(HERO.MoveState); | |
return GRID.vision(Grid.toClass(HERO.MoveState.homeGrid), targetGrid); | |
} | |
casted(add = 0) { | |
this.canShoot = false; | |
this.shootCoolDown(add); | |
} | |
shootCoolDown(add) { | |
setTimeout(() => (this.canShoot = true), INI.SHOOT_TIMEOUT + add); | |
} | |
drop(grid) { | |
let drop; | |
if (this.type.inventory) { | |
switch (this.type.inventory.type) { | |
case "potion": | |
drop = new Potion(this.type.inventory.value.chooseRandom(), grid); | |
MAP[GAME.level].DUNGEON.potions.push(drop); | |
break; | |
case "boost": | |
drop = new Boost(this.type.inventory.value.chooseRandom(), grid); | |
MAP[GAME.level].DUNGEON.boosts.push(drop); | |
break; | |
default: | |
console.log("Monster dropping", this.type.inventory, "ERROR"); | |
break; | |
} | |
} else { | |
let value = RND(Math.floor(this.type.gold / 2), this.type.gold); | |
drop = new Gold(value, grid); | |
MAP[GAME.level].DUNGEON.gold.push(drop); | |
} | |
ENGINE.VIEWPORT.changed = true; | |
GAME.PAINT.config(); | |
} | |
turn() { | |
if (!GAME.TURN.fight_active) return; | |
let damage = GAME.TURN.damage(this, HERO); | |
if (damage > 0) { | |
CONSOLE.print( | |
`<span class="red">${ | |
this.type.title | |
}</span> hits and makes <span class="red">${damage}</span> damage.` | |
); | |
HERO.health -= damage; | |
HERO.health = Math.max(HERO.health, 0); | |
if (HERO.health <= 0) { | |
console.log("HERO dies in fight"); | |
GAME.TURN.fight_active = false; | |
CONSOLE.print(`<span class="blue">${HERO.name} was killed.`); | |
GAME.CLICK.endFight(); | |
HERO.death(); | |
} | |
} else { | |
CONSOLE.print(`<span class="red">${this.type.title}</span> misses.`); | |
} | |
} | |
die() { | |
HERO.incExp(this.type.exp); | |
//HERO.experience += this.type.exp; | |
TITLE.change(); | |
this.drop(this.MoveState.homeGrid); | |
TEXTPOOL.pool.push( | |
new TextSprite( | |
(this.type.exp + "XP").toString(), | |
GRID.gridToCoord(this.MoveState.homeGrid), | |
"#00FF00", | |
100 | |
) | |
); | |
} | |
} | |
var LOG = {}; | |
class Log { | |
constructor() { | |
this.kills = 0; | |
} | |
} | |
///////////////// | |
$(function() { | |
PRG.INIT(); | |
PRG.setup(); | |
ENGINE.LOAD.preload(); | |
SCORE.init("SC", "DDID", 10, 2500); | |
SCORE.loadHS(); | |
SCORE.hiScore(); | |
BACKUP_MAP = $.extend(true, {}, MAP); | |
}); |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> | |
<script src="https://codepen.io/laughingskull/pen/MOJejy"></script> | |
<script src="https://codepen.io/laughingskull/pen/ooZWOB"></script> | |
<script src="https://codepen.io/laughingskull/pen/MOmNNE"></script> | |
<script src="https://codepen.io/laughingskull/pen/qwbKMK"></script> | |
<script src="https://codepen.io/laughingskull/pen/KEpzbV"></script> | |
<script src="https://codepen.io/laughingskull/pen/vMGjqe"></script> |
@font-face { | |
font-family: "Emulogic"; | |
src: url("https://www.c00lsch00l.eu/Fonts/emulogic.ttf"); | |
} | |
@font-face { | |
font-family: "Arcade"; | |
src: url("https://www.c00lsch00l.eu/Fonts/Arcade Classic.ttf"); | |
} | |
:root { | |
background-color: orange; | |
background: -webkit-linear-gradient(90deg, orange, #ffc04d); | |
background: -o-linear-gradient(90deg, orange, #ffc04d); | |
background: -moz-linear-gradient(90deg, orange, #ffc04d); | |
background: -ms-linear-gradient(90deg, orange, #ffc04d); | |
background: linear-gradient(90deg, orange, #ffc04d); | |
font-family: "Source Sans Pro", sans-serif; | |
} | |
h1, | |
h2 { | |
text-align: left; | |
font-size: 25px; | |
text-shadow: 1px 1px #666; | |
font-family: "Arcade"; | |
} | |
p { | |
color: #000; | |
font-size: 18px; | |
/*font-family: "Emulogic";*/ | |
} | |
hr { | |
border: 0; | |
height: 1px; | |
background-image: -webkit-linear-gradient( | |
left, | |
rgba(0, 0, 0, 0), | |
rgba(0, 0, 0, 0.75), | |
rgba(0, 0, 0, 0) | |
); | |
background-image: -moz-linear-gradient( | |
left, | |
rgba(0, 0, 0, 0), | |
rgba(0, 0, 0, 0.75), | |
rgba(0, 0, 0, 0) | |
); | |
background-image: -ms-linear-gradient( | |
left, | |
rgba(0, 0, 0, 0), | |
rgba(0, 0, 0, 0.75), | |
rgba(0, 0, 0, 0) | |
); | |
background-image: -o-linear-gradient( | |
left, | |
rgba(0, 0, 0, 0), | |
rgba(0, 0, 0, 0.75), | |
rgba(0, 0, 0, 0) | |
); | |
} | |
.center { | |
text-align: center; | |
} | |
.big { | |
font-size: 22px; | |
} | |
.fr { | |
float: right; | |
} | |
.fl { | |
float: left; | |
} | |
.cb { | |
clear: both; | |
} | |
.cr { | |
clear: right; | |
} | |
.cl { | |
clear: left; | |
} | |
.win { | |
width: 960px; | |
margin: auto; | |
margin-top: 20px; | |
padding: 20px 32px 32px; | |
background-color: #ffd78e; | |
background: -webkit-linear-gradient( | |
to right, | |
#ffd78e, | |
#ffd382, | |
#ffe5b4, | |
#ffd78e, | |
#ffd78e | |
); | |
background: -o-linear-gradient( | |
to right, | |
#ffd78e, | |
#ffd382, | |
#ffe5b4, | |
#ffd78e, | |
#ffd78e | |
); | |
background: -moz-linear-gradient( | |
to right, | |
#ffd78e, | |
#ffd382, | |
#ffe5b4, | |
#ffd78e, | |
#ffd78e | |
); | |
background: -ms-linear-gradient( | |
to right, | |
#ffd78e, | |
#ffd382, | |
#ffe5b4, | |
#ffd78e, | |
#ffd78e | |
); | |
background: linear-gradient( | |
to right, | |
#ffd78e, | |
#ffd382, | |
#ffe5b4, | |
#ffd78e, | |
#ffd78e | |
); | |
border-radius: 13px; | |
box-shadow: 5px 5px 20px 7px #e69400; | |
} | |
/**NEW**/ | |
.setup_container { | |
padding: 10px; | |
display: inline-block; | |
float: left; | |
margin: 12px; | |
} | |
input[type="button"] { | |
cursor: pointer; | |
} | |
input[type="text"] { | |
text-align: center; | |
} | |
.version { | |
font-family: Consolas; | |
font-size: 10px; | |
} | |
.hidden { | |
display: none; | |
} | |
.winTrans { | |
width: 960px; | |
margin: auto; | |
margin-top: 20px; | |
padding: 20px 32px 32px; | |
} | |
input[type="button"] { | |
-webkit-border-radius: 0px 5px 5px 5px; | |
border-radius: 0px 5px 5px 5px; | |
} | |
#startGame, #Restart { | |
font-weight: bold; | |
} | |
#setup2 input[type="text"] { | |
font-size: 12px; | |
font-family: Consolas; | |
} | |
.section { | |
display: none; | |
font-size: 12px; | |
font-family: Consolas; | |
} | |
.section fieldset { | |
border-radius: 7px; | |
width: 600px; | |
} | |
.section fieldset p { | |
font-size: 12px; | |
} | |
.layer { | |
position: absolute; | |
left: 0; | |
} | |
#hiscore { | |
border: 1px dotted #888; | |
border-radius: 13px; | |
padding: 20px; | |
box-shadow: 2px 3px 3px 3px #333; | |
font-family: "Courier New", Courier, monospace; | |
font-size: 16px; | |
white-space: pre; | |
width: 230px; | |
margin-top: 8px; | |
} | |
#SC { | |
float: right; | |
margin-left: 8px; | |
} | |
.gw { | |
float: left; | |
margin: 0px; | |
padding: 0px; | |
} | |
.gh { | |
float: none; | |
clear: both; | |
margin: 0px; | |
padding: 0px; | |
} | |
.bordered { | |
border: 1px solid black; | |
border-radius: 7px; | |
width: 240px; | |
height: 100px; | |
} | |
img.pic { | |
margin: 10px; | |
border: 1px solid #000; | |
padding: 2px; | |
max-width: 100%; | |
height: auto; | |
} | |
/*FORM*/ | |
.form { | |
padding-left: 8px; | |
padding-right: 8px; | |
} | |
.form input[type="button"] { | |
margin: 4px; | |
-webkit-border-radius: 5px 5px 5px 5px; | |
border-radius: 5px; | |
background-color: #999; | |
border: 1px solid #666; | |
color: #111; | |
box-shadow: 3px 3px 3px 0px #222; | |
cursor: pointer; | |
} | |
.form input[type="button"]:active { | |
background-color: #111; | |
border: 1px solid #666; | |
color: #999; | |
cursor: pointer; | |
} | |
.form input[type="button"]:disabled { | |
background-color: #333; | |
border: 1px solid #222; | |
box-shadow: 3px 3px 3px 0px #111; | |
color: #222; | |
cursor: not-allowed; | |
} | |
#FORM { | |
position: absolute; | |
border: 1px solid #645454; | |
border-radius: 7px; | |
background: #000; | |
z-index: 999; | |
} | |
#FORM h1 { | |
font-family: "Garamond"; | |
text-align: center; | |
font-size: 16px; | |
color: #999; | |
text-shadow: 1px 1px #666; | |
margin: 0px; | |
margin-top: 4px; | |
padding: 0px; | |
} | |
#FORM hr { | |
border: 0; | |
border-top: 1px solid #222; | |
} | |
#FORM img { | |
position: relative; | |
top: 4px; | |
} | |
.form span { | |
font-family: "Consolas"; | |
color: #777; | |
padding: 16px; | |
} | |
.fightWindow { | |
padding: 0px; | |
float: left; | |
width: 100; | |
} | |
.fightWindow p { | |
color: #0d0; | |
font-size: 14px; | |
text-align: center; | |
padding: 0px; | |
margin: 0px; | |
} | |
.fightWindow p.attr { | |
font-family: "Garamond"; | |
color: #999; | |
text-shadow: 1px 1px #666; | |
font-size: 16px; | |
text-align: left; | |
padding: 0px; | |
padding-left: 12px; | |
margin: 0px; | |
} | |
div .healthbar { | |
padding: 5px; | |
} | |
span.scroll_counter { | |
padding: 0px; | |
padding-right: 0px; | |
/*position: relative; | |
top: -10px;*/ | |
padding-right: 20px; | |
font-size: 14px; | |
} | |
.form input[type="image"] { | |
} | |
.form input[type="image"]:disabled { | |
cursor: not-allowed; | |
opacity: 0.4 | |
} | |
div#Console {} | |
div#Console p { | |
color: #0F0; | |
font-family: Consolas; | |
font-size: 11px; | |
padding-left: 8px; | |
margin: 0px; | |
} | |
.blue { | |
color: #00F !important; | |
padding: 0px !important; | |
} | |
.red { | |
color: #F00 !important; | |
padding: 0px !important; | |
} | |
.orange { | |
color: #FFA500 !important; | |
padding: 0px !important; | |
} | |
#scrollPanel { | |
clear: both; | |
overflow-x: scroll; | |
white-space: nowrap; | |
scroll-behavior: smooth; | |
scrollbar-width: thin; | |
scrollbar-color: #333 #000; | |
justify-content: safe center; | |
display: flex; | |
align-items: center; | |
height: 50px; | |
} | |
#scrollPanel input[type="image"]{ | |
padding-right: 0px; | |
flex-shrink: 0; | |
} |