diff --git a/Platform/Apple/tools/PackPartitions/src/org/badvision/A2PackPartitions.groovy b/Platform/Apple/tools/PackPartitions/src/org/badvision/A2PackPartitions.groovy index 9e786644..41d34cad 100644 --- a/Platform/Apple/tools/PackPartitions/src/org/badvision/A2PackPartitions.groovy +++ b/Platform/Apple/tools/PackPartitions/src/org/badvision/A2PackPartitions.groovy @@ -19,11 +19,13 @@ import java.nio.ByteBuffer import java.nio.channels.Channels import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.util.Calendar import java.util.zip.GZIPInputStream import java.util.LinkedHashMap import java.security.MessageDigest import javax.xml.bind.DatatypeConverter import groovy.json.JsonOutput +import groovy.util.Node /** * @@ -81,6 +83,7 @@ class A2PackPartitions def modules = [:] // module name to module.num, module.buf def bytecodes = [:] // module name to bytecode.num, bytecode.buf def fixups = [:] // module name to fixup.num, fixup.buf + def gameFlags = [:] // flag name to number def itemNameToFunc = [:] def playerNameToFunc = [:] @@ -773,6 +776,12 @@ class A2PackPartitions cache[key] = [hash:hash, data:buf] } + def updateEngineStamp(name, hash) + { + if (!cache.containsKey("engineStamp") || cache["engineStamp"].hash < hash) + cache["engineStamp"] = [hash:hash] + } + def grabEntireFromCache(kind, addTo, hash) { if (cache.containsKey(kind) && cache[kind].hash == hash) { @@ -1559,6 +1568,30 @@ class A2PackPartitions } } + /** + * Make a compact representation of a timestamp, useful as a version number + */ + def timestampToVersionNum(engineStamp, scenarioStamp) + { + Calendar cal = Calendar.getInstance() + cal.setTimeInMillis(engineStamp) + def year = cal.get(Calendar.YEAR) + def month = cal.get(Calendar.MONTH) + def day = cal.get(Calendar.DAY_OF_MONTH) + def hour = cal.get(Calendar.HOUR_OF_DAY) + + def yearCode = year % 10 + def monthCode = (month < 9) ? (char) (48+month+1) : + month == 9 ? 'o' : + month == 10 ? 'n' : + 'd' + def hourCode = (char) (97 + hour) // 'a'=0, 'b'=1, etc. + def engineCode = String.format("%d%c%02d%c", yearCode, monthCode, day, hourCode) + + def offset = (int) ((scenarioStamp - engineStamp) / (1000 * 60 * 60)) + return String.format("%s%s%d", engineCode, offset < 0 ? "-" : ".", Math.abs(offset)) + } + /** * Make an index listing the partition number wherein each map and portrait can be found. */ @@ -1566,6 +1599,12 @@ class A2PackPartitions { def tmp = ByteBuffer.allocate(5000) + // Start with the version number + def combinedVersion = timestampToVersionNum(cache["engineStamp"].hash, cache["scenarioStamp"].hash) + tmp.put((byte)(combinedVersion.length())) + combinedVersion.getBytes().each { b -> tmp.put((byte)b) } + + // Then output 2D maps, 3d maps, and portraits tmp.put((byte) maps2D.size()) maps2D.each { k, v -> tmp.put((byte) ((parseOrder(v.order) < 0) ? 255 : v.buf.partNum)) @@ -1584,6 +1623,8 @@ class A2PackPartitions name:"resourceIndex", buf:code["resourceIndex"].buf] part.chunks[["code", "resourceIndex"]] = chunk part.spaceUsed += calcChunkLen(chunk) + + return combinedVersion } def fillAllDisks() @@ -1637,7 +1678,7 @@ class A2PackPartitions assert allMaps.isEmpty : "All data must fit within $MAX_DISKS disks." // Add the special resource index to disk 1 - addResourceIndex(partChunks[0]) + def gameVersion = addResourceIndex(partChunks[0]) // And write out each disk partChunks.each { part -> @@ -1649,6 +1690,8 @@ class A2PackPartitions def spaceUsed = part.spaceUsed // use var to avoid gigantic assert fail msg assert spaceUsed == partFile.length() } + + println "Game version: V $gameVersion" } def writePartition(stream, partNum, chunks) @@ -1859,6 +1902,7 @@ class A2PackPartitions def uncompData = readBinary(inDir + "build/" + codeName + ".b") addToCache("code", code, codeName, hash, compress(uncompData)) + updateEngineStamp(codeName, hash) } def assembleCore(inDir) @@ -1878,8 +1922,10 @@ class A2PackPartitions def file = jitCopy( new File("build/tools/${name=="PRORWTS" ? "ProRWTS/PRORWTS2" : "PLASMA/src/PLVM02"}#4000")) hash = file.lastModified() - if (!grabFromCache("sysCode", sysCode, name, hash)) + if (!grabFromCache("sysCode", sysCode, name, hash)) { addToCache("sysCode", sysCode, name, hash, compress(readBinary(file.toString()))) + updateEngineStamp(name, hash) + } } else { hash = getLastDep(new File(inDir, "${name}.s")) @@ -1892,6 +1938,7 @@ class A2PackPartitions addToCache("sysCode", sysCode, name, hash, (name ==~ /loader|decomp/) ? [data:uncompData, len:uncompData.length, compressed:false] : compress(uncompData)) + updateEngineStamp(name, hash) } } @@ -1952,6 +1999,8 @@ class A2PackPartitions addToCache("modules", modules, moduleName, hash, module) addToCache("bytecodes", bytecodes, moduleName, hash, bytecode) addToCache("fixups", fixups, moduleName, hash, fixup) + if (!(moduleName ==~ /.*(gs|gen)_.*/ || codeDir ==~ /.*mapScript.*/)) + updateEngineStamp(moduleName, hash) } def readAllCode() @@ -1980,6 +2029,7 @@ class A2PackPartitions compileModule("gen_enemies", "src/plasma/") compileModule("gen_items", "src/plasma/") compileModule("gen_players", "src/plasma/") + compileModule("gen_flags", "src/plasma/") globalScripts.each { name, nArgs -> compileModule("gs_"+name, "src/plasma/") } @@ -2015,6 +2065,40 @@ class A2PackPartitions } } + /* + * Scan all the scripts looking for flags, and make a mapping of flag name to number. + */ + def numberGameFlags(data) + { + String name = data.name().toString() + if (name == "{outlaw}gameData") { + gameFlags = [] as Set // temporary, until we have them all + data.global.scripts.script.each { numberGameFlags(it) } + data.map.scripts.script.each { numberGameFlags(it) } + + // Now that we have them all, sort and assign numbers + def flagSet = gameFlags + gameFlags = [:] + flagSet.sort().each { flg -> gameFlags[flg] = gameFlags.size() } + } + else if (name == "{outlaw}block" && + (data.@type == "interaction_get_flag" || data.@type == "interaction_set_flag")) + { + def els = data.field + assert els.size() == 1 + def first = els[0] + assert first.@name == "NAME" + def flg = first.text().toLowerCase() + gameFlags << flg + } + else { + data.iterator().each { + if (it instanceof Node) + numberGameFlags(it) + } + } + } + def readCache() { File cacheFile = new File("build/world.cache") @@ -2127,11 +2211,14 @@ class A2PackPartitions addResourceDep("map", curMapName, toType, toName) } - def pack(xmlPath, dataIn) + def pack(xmlFile, dataIn) { // Save time by using cache of previous run readCache() + // Record scenario timestamp + cache["scenarioStamp"] = [hash: xmlFile.lastModified()] + // Record global script names recordGlobalScripts(dataIn) @@ -2242,6 +2329,9 @@ class A2PackPartitions // Number all the maps and record them with names numberMaps(dataIn) + // Assign a number to each game flag + numberGameFlags(dataIn) + // Form the translation from item name to function name (and ditto // for players) allItemFuncs(dataIn.global.sheets.sheet) @@ -2598,6 +2688,67 @@ end return parseDice(val) } + def genAllFlags() + { + // Make constants + new File("build/src/plasma/gen_flags.plh.new").withWriter { out -> + out.println("// Generated code - DO NOT MODIFY BY HAND\n") + out.println("const flags_nameForNumber = 0") + out.println("const flags_numberForName = 2") + out.println() + gameFlags.each { name, num -> + out.println("const GF_${humanNameToSymbol(name, true)} = $num") + } + out.println("const NUM_GAME_FLAGS = ${gameFlags.size()}") + } + replaceIfDiff("build/src/plasma/gen_flags.plh") + + // Generate code + new File("build/src/plasma/gen_flags.pla.new").withWriter { out -> + out.println("// Generated code - DO NOT MODIFY BY HAND") + out.println() + out.println("include \"gamelib.plh\"") + out.println("include \"globalDefs.plh\"") + out.println("include \"gen_flags.plh\"") + out.println() + out.println("predef _flags_nameForNumber(num)#1") + out.println("predef _flags_numberForName(name)#1") + out.println() + out.println("word[] funcTbl = @_flags_nameForNumber, @_flags_numberForName") + out.println() + gameFlags.each { name, num -> + out.println("byte[] SF_${humanNameToSymbol(name, true)} = ${escapeString(name.toUpperCase())}") + } + out.println() + gameFlags.each { name, num -> + if (num == 0) + out.print("word[] flagNames = ") + else + out.print("word = ") + out.println("@SF_${humanNameToSymbol(name, true)}") + } + out.println() + out.println("def _flags_nameForNumber(num)#1") + out.println(" if num >= 0 and num < NUM_GAME_FLAGS; return flagNames[num]; fin") + out.println(" return NULL") + out.println("end") + out.println() + out.println("def _flags_numberForName(name)#1") + out.println(" word num") + out.println(" for num = 0 to NUM_GAME_FLAGS-1") + out.println(" if streqi(flagNames[num], name); return num; fin") + out.println(" next") + out.println(" return -1") + out.println("end") + out.println() + + // Lastly, the outer module-level code + out.println("return @funcTbl") + out.println("done") + } + replaceIfDiff("build/src/plasma/gen_flags.pla") + } + def genWeapon(func, row, out) { out.println( @@ -2629,26 +2780,21 @@ end "${parseModifier(row, "bonus-value", "bonus-attribute")})") } - def genAmmo(func, row, out) - { - out.println( - " return makeStuff(" + - "${escapeString(parseStringAttr(row, "name"))}, " + - "${escapeString(parseStringAttr(row, "ammo-kind"))}, " + - "${parseWordAttr(row, "price")}, " + - "${parseWordAttr(row, "max")}, " + - "${parseWordAttr(row, "store-amount")}, " + - "${parseDiceAttr(row, "loot-amount")})") - } - def genItem(func, row, out) { - out.println( - " return makeItem(" + - "${escapeString(parseStringAttr(row, "name"))}, " + - "${parseWordAttr(row, "price")}, " + - "${parseModifier(row, "bonus-value", "bonus-attribute")}, " + - "${parseByteAttr(row, "number-of-uses")})") + def name = parseStringAttr(row, "name") + def price = parseWordAttr(row, "price") + def modifier = parseModifier(row, "bonus-value", "bonus-attribute") + def kind = parseStringAttr(row, "ammo-kind") + def count = parseWordAttr(row, "count") + def storeAmount = parseWordAttr(row, "store-amount") + def lootAmount = parseDiceAttr(row, "loot-amount") + + if ("$kind, $modifier, $count, $storeAmount, $lootAmount" != ", NULL, 0, 0, 0") + out.println(" return makeFancyItem(${escapeString(name)}, $price, " + + "${escapeString(kind)}, $modifier, $count, $storeAmount, $lootAmount)") + else + out.println(" return makePlainItem(${escapeString(name)}, $price)") } def genPlayer(func, row, out) @@ -2688,7 +2834,7 @@ end def itemFunc = itemNameToFunc[name] assert itemFunc : "Can't locate item '$name'" if (num > 1) - out.println(" addToList(@p=>p_items, setStuffCount(itemScripts()=>$itemFunc(), $num))") + out.println(" addToList(@p=>p_items, setItemCount(itemScripts()=>$itemFunc(), $num))") else out.println(" addToList(@p=>p_items, itemScripts()=>$itemFunc())") } @@ -2849,24 +2995,22 @@ def makeWeapon_pt2(p, attack0, attack1, attack2, weaponRange, combatText, single return p end -def makeStuff(name, kind, price, count, storeAmount, lootAmount) - word p; p = mmgr(HEAP_ALLOC, TYPE_STUFF) +def makePlainItem(name, price) + word p; p = mmgr(HEAP_ALLOC, TYPE_PLAIN_ITEM) p=>s_name = mmgr(HEAP_INTERN, name) - p=>s_itemKind = mmgr(HEAP_INTERN, kind) p=>w_price = price - p=>w_count = count - p=>w_storeAmount = storeAmount - p=>r_lootAmount = lootAmount return p end -def makeItem(name, price, modifier, maxUses) - word p; p = mmgr(HEAP_ALLOC, TYPE_ITEM) +def makeFancyItem(name, price, kind, modifiers, count, storeAmount, lootAmount) + word p; p = mmgr(HEAP_ALLOC, TYPE_FANCY_ITEM) p=>s_name = mmgr(HEAP_INTERN, name) p=>w_price = price - p=>p_modifiers = modifier - p->b_maxUses = maxUses - p->b_curUses = 0 + p=>s_itemKind = mmgr(HEAP_INTERN, kind) + p=>p_modifiers = modifiers + p=>w_count = count + p=>w_storeAmount = storeAmount + p=>r_lootAmount = lootAmount return p end @@ -2880,7 +3024,7 @@ end switch (typeName) { case "weapon": genWeapon(func, row, out); break case "armor": genArmor(func, row, out); break - case "ammo": genAmmo(func, row, out); break + case "ammo": genItem(func, row, out); break case "item": genItem(func, row, out); break default: assert false } @@ -2993,11 +3137,11 @@ def makePlayer_pt2(p, health, level, aiming, handToHand, dodging, gender)#1 return p end -def setStuffCount(p, ct)#1 - if p->t_type == TYPE_STUFF +def setItemCount(p, ct)#1 + if p->t_type == TYPE_FANCY_ITEM p->w_count = ct else - fatal(\"stuffct\") + fatal(\"itemct\") fin return p // for chaining end @@ -3088,7 +3232,7 @@ end return [name.trim(), animFrameNum, animFlags] } - def dataGen(xmlPath, dataIn) + def dataGen(xmlFile, dataIn) { // When generating code, we need to use Unix linebreaks since that's what // the PLASMA compiler expects to see. @@ -3141,6 +3285,9 @@ end // Before we can generate global script code, we need to identify and number all the maps. numberMaps(dataIn) + // Assign a number to each game flag + numberGameFlags(dataIn) + // Form the translation from item name to function name (and ditto // for players) allItemFuncs(dataIn.global.sheets.sheet) @@ -3152,6 +3299,9 @@ end genAllGlobalScripts(dataIn.global.scripts.script) curMapName = null + // Generate a mapping of flags, for debugging purposes. + genAllFlags() + // Translate enemies, weapons, etc. to code genAllItems(dataIn.global.sheets.sheet) genAllEnemies(dataIn.global.sheets.sheet.find { it?.@name.equalsIgnoreCase("enemies") }) @@ -3206,7 +3356,7 @@ end def createHddImage() { - println "Creating hdd image." + //println "Creating hdd image." // Copy the combined core executable to the output directory copyIfNewer(new File("build/src/core/build/LEGENDOS.SYSTEM.sys#2000"), @@ -3231,7 +3381,7 @@ end def createFloppyImages() { - println "Creating floppy images." + //println "Creating floppy images." // We'll be copying stuff from the hdd directory def hddDir = new File("build/root") @@ -3442,6 +3592,7 @@ end out << "include \"../plasma/gamelib.plh\"\n" out << "include \"../plasma/globalDefs.plh\"\n" out << "include \"../plasma/playtype.plh\"\n" + out << "include \"../plasma/gen_flags.plh\"\n" out << "include \"../plasma/gen_images.plh\"\n" out << "include \"../plasma/gen_items.plh\"\n" out << "include \"../plasma/gen_modules.plh\"\n" @@ -3929,14 +4080,16 @@ end def packGetFlag(blk) { - def name = getSingle(blk.field, 'NAME').text() - out << "getGameFlag(${escapeString(name)})" + def name = getSingle(blk.field, 'NAME').text().trim().toLowerCase() + assert gameFlags.containsKey(name) + out << "getGameFlag(GF_${humanNameToSymbol(name, true)})" } def packChangeFlag(blk) { - def name = getSingle(blk.field, 'NAME').text() - outIndented("setGameFlag(${escapeString(name)}, ${blk.@type == 'interaction_set_flag' ? 1 : 0})\n") + def name = getSingle(blk.field, 'NAME').text().trim().toLowerCase() + assert gameFlags.containsKey(name) + outIndented("setGameFlag(GF_${humanNameToSymbol(name, true)}, ${blk.@type == 'interaction_set_flag' ? 1 : 0})\n") } def isStringExpr(blk) @@ -4009,6 +4162,10 @@ end } } + def packLogicNegate(blk) { + out << "!("; packExpr(getSingle(blk.value, "BOOL").block[0]); out << ")" + } + def packMathArithmetic(blk) { def op = getSingle(blk.field, "OP").text() @@ -4078,6 +4235,9 @@ end case 'logic_operation': packLogicOperation(blk) break + case 'logic_negate': + packLogicNegate(blk) + break case 'variables_get': packVarGet(blk) break diff --git a/Platform/Apple/virtual/src/core/mem.s b/Platform/Apple/virtual/src/core/mem.s index 8e1dc136..0eab4edf 100644 --- a/Platform/Apple/virtual/src/core/mem.s +++ b/Platform/Apple/virtual/src/core/mem.s @@ -747,15 +747,8 @@ gcHash_chk: !zone rts .corrup jmp heapCorrupt -!if DEBUG = 0 { -debugOnly: - jsr inlineFatal : !text "DebugOnly",0 -} - ; Verify integrity of memory manager structures memCheck: !zone -!if DEBUG = 0 { jmp debugOnly } -!if DEBUG { jsr heapCheck ; heap check (if there is one) ldx #0 ; check main bank jsr .chk @@ -811,7 +804,6 @@ heapCheck: !zone cmp heapEndPg ; or >= than end of heap bcc .tscan ; fall through to heapCorrupt... -} ; if DEBUG heapCorrupt: +prWord pTmp @@ -1333,16 +1325,10 @@ aux_dispatch: !if SANITY_CHECK { saneStart: !zone { sta saneEnd+2 ; save cmd num for end-checking - pha - tya - pha - txa + cmp #ADVANCE_ANIMS + beq .skip pha jsr saneCheck - pla - tax - pla - tay +prChr 'M' lda isAuxCmd beq + @@ -1359,7 +1345,7 @@ saneStart: !zone { beq + +prY + pla - rts +.skip rts } saneCheck: !zone { @@ -1373,6 +1359,8 @@ saneCheck: !zone { saneEnd: !zone { pha lda #$11 ; self-modified earlier by saneStart + cmp #ADVANCE_ANIMS + beq .skip cmp #REQUEST_MEMORY beq .val cmp #QUEUE_LOAD @@ -1385,17 +1373,9 @@ saneEnd: !zone { bne .noval .val +prStr : !text "->",0 +prYX -.noval tya - pha - txa - pha - jsr saneCheck +.noval jsr saneCheck +prStr : !text "m.",0 - pla - tax - pla - tay - pla +.skip pla rts } } @@ -2787,6 +2767,7 @@ advSingleAnim: rts .dbgout +crout +waitKey + bit $c050 sta setAuxRd sta setAuxWr rts @@ -2828,12 +2809,14 @@ applyPatch: ; loop to skip patches until we find the right one - dec reqLen ; it starts at 1, which means first patch. beq + - ldy #1 + ldy #0 + lda (pSrc),y ; low byte of patch len + pha + iny lda (pSrc),y ; hi byte of patch len inx ; -> pSrc+1 jsr .ptradd ; skip by # pages in patch - dey - lda (pSrc),y ; low byte of patch len + pla ; get lo byte of len back jsr .srcadd ; skip pSrc past last partial page in patch jmp - + !if DEBUG = 2 { jsr .dbgC2 } diff --git a/Platform/Apple/virtual/src/plasma/combat.pla b/Platform/Apple/virtual/src/plasma/combat.pla index 8166d7bf..65f76dd6 100644 --- a/Platform/Apple/virtual/src/plasma/combat.pla +++ b/Platform/Apple/virtual/src/plasma/combat.pla @@ -132,7 +132,7 @@ def rollPlayerHit(pPlayer, pWeapon, pEnemy, sAction) roll = rollPercentileWithLuck(-(pPlayer->b_luck)) // luck can reduce roll = increase chance to hit if combatDebug; displayf2("Roll=%d, need <%d\n", roll, abs(roll < chance)); getUpperKey(); fin if roll >= chance - setPlural(0) + isPlural = FALSE displayf3("\n%s %s at %s but misses.\n", pPlayer=>s_name, sAction, pEnemy=>s_name) return FALSE fin @@ -143,7 +143,7 @@ end def rollEnemyDodge(pPlayer, pEnemy, sAction) // Enemy chance to dodge is taken from their chance to hit divided by 2 if (rand16() % 100) < (pEnemy->b_chanceToHit / 2) - setPlural(0) + isPlural = FALSE displayf3("\n%s %s at %s, ", pPlayer=>s_name, sAction, pEnemy=>s_name) displayf1("but %s dodges.\n", pEnemy=>s_name) return TRUE @@ -157,7 +157,7 @@ def damageEnemy(pPlayer, pEnemy, dmg, sAction)#0 displayf3("\nenemy health: %d-%d=%d\n", pEnemy=>w_health, dmg, pEnemy=>w_health-dmg) getUpperKey fin - setPlural(0) + isPlural = FALSE buildString(@addToString) printf3("\n%s %s %s ", pPlayer=>s_name, sAction, pEnemy=>s_name) printf1("for %d damage.", dmg) @@ -290,7 +290,7 @@ def reload(pl, pWeapon, echo)#0 word item byte orig, n - setPlural(FALSE) + isPlural = FALSE // If ammo type is null, it means weapon doesn't use ammo in traditional sense. if !pWeapon=>s_ammoKind @@ -301,7 +301,7 @@ def reload(pl, pWeapon, echo)#0 // Find matching ammo item = pl=>p_items while item - if item->t_type == TYPE_STUFF + if item->t_type == TYPE_FANCY_ITEM if streqi(item=>s_itemKind, pWeapon=>s_ammoKind); break; fin fin item = item=>p_nextObj @@ -366,7 +366,7 @@ def displayOpponents()#0 fin first = FALSE count = countListFiltered(p=>p_enemies, p_nextObj, @canFight) - setPlural(count <> 1) + isPlural = count <> 1 if (p=>p_enemies=>r_groupSize == 0) displayf2("%s at %d'", p=>p_enemies=>s_name, p->b_enemyGroupRange) else @@ -678,7 +678,7 @@ def enemyCombatTurn(pe)#1 pl = randomFromListFiltered(global=>p_players, p_nextObj, @canFight) if !pl; return FALSE; fin - setPlural(FALSE) + isPlural = FALSE displayf3("\n%s %s %s ", pe=>s_name, pe=>s_attackText, pl=>s_name) // Roll to hit @@ -851,7 +851,7 @@ def addItem(pl, pItem)#1 word pComp pComp = scanForNamedObj(pl=>p_items, pItem=>s_name) if pComp - if pItem->t_type == TYPE_STUFF + if pItem->t_type == TYPE_FANCY_ITEM pComp=>w_count = min(30000, pComp=>w_count + pItem=>w_count) return TRUE else @@ -882,16 +882,17 @@ def collectLootAndXP()#2 mmgr(FINISH_LOAD, 0) itemFunc = randomFromArray(pItemsModule()=>items_forLootCode(enemy=>s_lootCode)) pItem = itemFunc() - if pItem->t_type == TYPE_STUFF + if pItem->t_type == TYPE_FANCY_ITEM pItem=>w_count = rollDiceWithLuck(pItem=>r_lootAmount, global=>p_players->b_luck) fin if addItem(global=>p_players, pItem) - if pItem->t_type == TYPE_STUFF and pItem=>w_count > 1 - setPlural(TRUE) - displayf2("You find %d %s! ", pItem=>w_count, pItem=>s_name) + displayStr("You find ") + if pItem->t_type == TYPE_FANCY_ITEM and pItem=>w_count > 1 + isPlural = TRUE + displayf2("%d %s! ", pItem=>w_count, pItem=>s_name) else - setPlural(FALSE) - displayf2("You find %s%s! ", anOrA(pItem=>s_name), pItem=>s_name) + isPlural = FALSE + displayf2("%s%s! ", anOrA(pItem=>s_name), pItem=>s_name) fin fin lootedItem = TRUE @@ -908,6 +909,11 @@ end def startCombat(mapCode)#1 word p, p2, n, s + // Pre-load some global funcs + mmgr(QUEUE_LOAD, GS_COMBAT_INTRO<<8 | RES_TYPE_MODULE) + mmgr(QUEUE_LOAD, GS_COMBAT_PROMPT<<8 | RES_TYPE_MODULE) + mmgr(QUEUE_LOAD, GS_ENEMY_INTRO<<8 | RES_TYPE_MODULE) + // Setup isFleeing = FALSE combatDebug = FALSE @@ -915,6 +921,7 @@ def startCombat(mapCode)#1 // Display portrait of first group setPortrait(global=>p_enemyGroups=>p_enemies->b_image) + mmgr(FINISH_LOAD, 0) // Clear keyboard stobe, because while wandering the map, the player may have // queued up movement keys, which are made obsolete by the surprise of combat. @@ -932,7 +939,7 @@ def startCombat(mapCode)#1 p = global=>p_enemyGroups while p n = countList(p=>p_enemies) - setPlural(n <> 1) + isPlural = n <> 1 s = callGlobalFunc(GS_ENEMY_INTRO, 0, 0, 0) displayf2(s, n, p=>p_enemies=>s_name) p = p=>p_nextObj diff --git a/Platform/Apple/virtual/src/plasma/diskops.pla b/Platform/Apple/virtual/src/plasma/diskops.pla index 8a0eb510..f0c85e88 100644 --- a/Platform/Apple/virtual/src/plasma/diskops.pla +++ b/Platform/Apple/virtual/src/plasma/diskops.pla @@ -37,6 +37,7 @@ predef _newOrLoadGame(ask)#1 word[] funcTbl = @_saveGame, @_loadGame, @_newOrLoadGame byte[] game1_filename = "GAME.1.SAVE" +byte[] legendos_filename = "LEGENDOS.SYSTEM" /////////////////////////////////////////////////////////////////////////////////////////////////// // Definitions used by assembly code @@ -223,15 +224,23 @@ def _rwGame(cmd)#0 end /////////////////////////////////////////////////////////////////////////////////////////////////// -def _saveGame()#1 +def saveInternal()#0 + + // Never save corrupted heap + mmgr(CHECK_MEM, 0) // Perform garbage collection and record the size of the heap so we can restore it correctly global=>w_heapSize = mmgr(HEAP_COLLECT, 0) - HEAP_BOTTOM // Copy data to main memory, and write it out. - showMapName("Saving game...") memcpy(HEAP_BOTTOM, LOAD_SAVE_BUF, HEAP_SIZE) // LC to low mem _rwGame(RWTS_WRITE) +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def _saveGame()#1 + showMapName("Saving game...") + saveInternal() return 0 end @@ -246,12 +255,12 @@ def loadInternal()#1 p_loaded = LOAD_SAVE_BUF if p_loaded=>w_heapSize == 0 return FALSE // no game saved yet - fin - if p_loaded=>w_heapSize < 100 or p_loaded=>w_heapSize > HEAP_SIZE - fatal("Corrupt game file.") + elsif p_loaded=>w_heapSize < 100 or p_loaded=>w_heapSize > HEAP_SIZE or p_loaded=>w_typeHash <> typeHash + fatal("Incompatible game file.") fin memcpy(LOAD_SAVE_BUF, HEAP_BOTTOM, HEAP_SIZE) // low mem to LC initHeap(p_loaded=>w_heapSize) + mmgr(CHECK_MEM, 0) // make sure heap is valid return TRUE end @@ -270,21 +279,123 @@ def _loadGame()#1 return 0 end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def gameExists()#1 + word p_loaded + + // Load first part of save game into mem... 1 block should be plenty to verify it's real. + if callProRWTS(RWTS_READ | RWTS_OPENDIR, @game1_filename, LOAD_SAVE_BUF, 512) == 0 + // If heap size is reasonable and type hash matches, chances are high that it's a real save game. + p_loaded = LOAD_SAVE_BUF + if p_loaded=>w_heapSize >= 100 and p_loaded=>w_heapSize <= HEAP_SIZE and p_loaded=>w_typeHash == typeHash + return TRUE + fin + fin + return FALSE +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def getTextKey()#1 + byte key + ^$c053 + key = getUpperKey() + ^$c052 + textHome() + ^$25 = 20 + return key +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def pressAnyKey()#0 + puts("\n and press any key to continue.") + getTextKey() +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def reinsert()#0 + while TRUE + puts(" Re-insert disk 1") + pressAnyKey() + if callProRWTS(RWTS_READ | RWTS_OPENDIR, @legendos_filename, LOAD_SAVE_BUF, 512) == 0 + break + fin + puts("\n ") + beep() + loop +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def importGame()#1 + puts("\n Insert disk for import") + pressAnyKey() + if gameExists() + loadInternal() + puts("\n Game imported.") + reinsert() + saveInternal() + return TRUE + fin + puts("\n Not found.") + reinsert() + return FALSE +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def isNameChar(ch) + when ch + is '.' + is ',' + is '-' + is '\'' + is ' ' + return TRUE + wend + if ch >= 'A' and ch <= 'Z'; return TRUE; fin + if ch >= '0' and ch <= '9'; return TRUE; fin + return FALSE +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def isValidName(name) + byte len, c + len = ^name + if !len; return FALSE; fin + if ^(name+1) == ' '; return FALSE; fin // don't allow space at start + while len > 0 + len-- + name++ + c = charToUpper(^name) + if c == ' ' and len == 0; return FALSE; fin // don't allow space at end + if !isNameChar(c); return FALSE; fin + loop + return TRUE +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// def getCharacterName()#0 word cursX, cursY - displayStr("\nCharacter name?\n") - cursX, cursY = getCursor() - setWindow(cursY+24, cursY+24+18, cursX+154, cursX+154+62) - clearWindow() - global=>p_players=>s_name = getStringResponse() - setWindow2() - setCursor(cursX, cursY) + while TRUE + displayStr("Character name?\n") + cursX, cursY = getCursor() + setWindow(cursY+24, cursY+24+18, cursX+154, cursX+154+62) + clearWindow() + global=>p_players=>s_name = getStringResponse() + setWindow2() + setCursor(cursX, cursY) + if isValidName(global=>p_players=>s_name); break; fin + displayStr("\nInvalid name.\n\n") + beep() + loop end def getCharacterGender()#0 - displayStr("\n\nGender? (M/F/N/...) \n") - global=>p_players->c_gender = getUpperKey() - displayChar(global=>p_players->c_gender) + byte gender + repeat + displayStr("\n\nGender? (M/F/N/...) \n") + gender = getUpperKey() + displayChar(gender) + until gender >= 'A' and gender <= 'Z' + global=>p_players->c_gender = gender end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -303,6 +414,7 @@ def newGame()#0 showMapName("New game") setWindow2() newGameModule()() + clearWindow() getCharacterName() getCharacterGender() if global=>p_players->b_skillPoints @@ -310,52 +422,30 @@ def newGame()#0 fin end -/////////////////////////////////////////////////////////////////////////////////////////////////// -def gameExists()#1 - word p_loaded - - // Load first part of save game into mem... 1 block should be plenty to verify it's real. - if callProRWTS(RWTS_READ | RWTS_OPENDIR, @game1_filename, LOAD_SAVE_BUF, 512) == 0 - // If heap size is reasonable, assume it's a real save game. (Hash will be checked if the - // user chooses to actually load the game) - p_loaded = LOAD_SAVE_BUF - if p_loaded=>w_heapSize >= 100 and p_loaded=>w_heapSize <= HEAP_SIZE - return TRUE - fin - fin - return FALSE -end - /////////////////////////////////////////////////////////////////////////////////////////////////// def _newOrLoadGame(ask)#1 byte key if !gameExists() newGame(); return 1 + elsif !ask + loadInternal(); return 0 fin - if !ask - loadInternal() - return 0 - fin - - textHome() - ^$c053 - ^$25 = 20 - puts("\n N)ew game, or L)oad last game? ") - while TRUE - key = getUpperKey() - if key == 'N' - ^$c052 - newGame() - return 1 - elsif key == 'L' - ^$c052 - if loadInternal() - return 0 - fin - fin + textHome() + ^$25 = 20 + puts("\n Game: N)ew, L)oad, I)mport?") + key = getTextKey() + when key + is 'N' + newGame(); return 1 + is 'L' + if loadInternal(); return 0; fin + break + is 'I' + if importGame(); return 0; fin + wend beep() loop return 0 diff --git a/Platform/Apple/virtual/src/plasma/gamelib.plh b/Platform/Apple/virtual/src/plasma/gamelib.plh index 0fca045a..8a1b24ca 100644 --- a/Platform/Apple/virtual/src/plasma/gamelib.plh +++ b/Platform/Apple/virtual/src/plasma/gamelib.plh @@ -39,6 +39,7 @@ import gamelib predef countArray(arr)#1 predef countList(p)#1 predef countListFiltered(p, offset, filterFunc)#1 + predef crout()#0 predef displayChar(chr)#0 predef displayf1(fmt, arg1)#0 predef displayf2(fmt, arg1, arg2)#0 @@ -46,7 +47,6 @@ import gamelib predef displayStr(str)#0 predef encodeDice(nDice, dieSize, add)#1 predef fatal(msg)#1 - predef finalWin()#0 predef finishString(isPlural)#1 predef flipToPage1()#0 predef getCharResponse()#1 @@ -65,14 +65,15 @@ import gamelib predef initPlayerXP(player)#0 predef loadFrameImg(img)#0 predef loadMainFrameImg()#0 + predef lookupResourcePart(sectionNum, resourceNum)#1 predef makeModifier(name, value)#1 predef max(a, b)#1 predef memcpy(pSrc, pDst, len)#0 predef min(a, b)#1 predef mmgr(cmd, wordParam)#1 predef moveWayBackward()#1 + predef numToPlayer(num)#1 predef parseDec(str)#1 - predef parseDecWithDefault(str, default)#1 predef partyHasPlayer(playerName)#1 predef pause(count)#0 predef payGold(amount)#1 @@ -93,7 +94,6 @@ import gamelib predef rawDisplayf3(fmt, arg1, arg2, arg3)#0 predef rawDisplayStr(str)#0 predef rdkey()#1 - predef readStr()#1 predef removeFromList(pList, toRemove)#0 predef removePlayerFromParty(playerName)#0 predef rightJustifyNum(num, rightX)#0 @@ -116,7 +116,6 @@ import gamelib predef setMap(is3D, num, x, y, dir)#0 predef setMapWindow()#0 predef setBigWindow()#0 - predef setPlural(flg)#0 predef setPortrait(portraitNum)#0 predef setScriptInfo(mapName, trigTbl, wdt, hgt)#0 predef setSky(num)#0 @@ -147,6 +146,8 @@ import gamelib word groundNum byte portraitNum word pGodModule + word typeHash + byte isPlural /////////// Shared string constants ////////////// diff --git a/Platform/Apple/virtual/src/plasma/gameloop.pla b/Platform/Apple/virtual/src/plasma/gameloop.pla index 38bb8c19..3e6ee264 100644 --- a/Platform/Apple/virtual/src/plasma/gameloop.pla +++ b/Platform/Apple/virtual/src/plasma/gameloop.pla @@ -60,7 +60,6 @@ predef doRender()#0 predef playerDeath()#0 predef startGame(ask)#0 predef showAnimFrame()#0 -predef finalWin()#0 predef showParty()#0 /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -77,12 +76,12 @@ byte renderLoaded = FALSE byte texturesLoaded = FALSE byte textDrawn = FALSE byte textClearCountdown = 0 -byte isPlural = FALSE -byte skipEncounterCheck = FALSE +export byte isPlural = 0 // valid values: 0 or $40 +byte inScript = FALSE export word skyNum = 9 export word groundNum = 10 -export byte portraitNum = 1 +export byte portraitNum = 0 word triggerOriginX, triggerOriginY word triggerTbl @@ -97,6 +96,7 @@ word pResourceIndex = NULL word pGlobalTileset = NULL byte curMapPartition = 0 export word pGodModule = NULL +export word typeHash = 0 // Queue setMap / teleport / start_encounter, since otherwise script might be replaced while executing byte q_mapIs3D = 0 @@ -405,78 +405,73 @@ export asm finishString(isPlural)#1 sta cswl lda #$FD sta cswh - bit fixedRTS; V flag for prev-is-punctuation - ldy #1 ; dest offset in Y - ldx #1 ; source offset in X + clv ; V flag for prev-is-alpha + ldy #0 ; dest offset in Y (will be incremented before store) + ldx #0 ; source offset in X (will be incremented before load) cpx inbuf - beq + ; only process if string has at least 1 char - bcs .done -+ sty tmp+1 ; offset of last punctuation + beq .done ; failsafe: handle zero-length string .fetch - lda inbuf,x - cmp #"(" + inx + lda inbuf,x ; get next input char + iny + sta inbuf,y ; by default copy the char to output + cmp #"(" ; plural processing triggered by parentheses bne .notpar - bvs .notpar ; skip paren processing right punctuation - lda tmp ; check isPlural flag - bne .plurpr -- lda inbuf,x ; it's singular, so skip everything in parens - cmp #")" - beq .next + bvc .notpar ; but only parens directly after alpha char, e.g. preserving "Happy (and safe)." + + dey ; undo copy of the paren + stx tmp+1 ; save position in input + dex ; needed for failsafe operation + lda tmp ; set copy flag (V) initially + bne .findsl ; to same as isPlural flag + clv +.findsl ; see if there's a slash within the parens inx cpx inbuf - bne - - beq .done ; handle missing trailing paren -.plurpr - inx ; it's plural, so copy everything within the parens - lda inbuf,x ; copy characters - cpx inbuf ; handle missing trailing paren - beq + - bcs .store -+ cmp #")" ; go until we reach ending paren - beq .next - sta inbuf,y - iny - bne .plurpr ; always taken - -.notpar + lda inbuf,x cmp #"/" - bne .notsl - bvs .notsl ; skip slash processing right after punctuation - lda tmp ; check isPlural flag - bne .plursl -- inx ; loop that skips plural form - cpx inbuf + bne + + php + pla + eor #$40 ; flip V flag, meaning singular text is before slash, plural after. + pha + plp ++ cmp #")" ; scan until ending paren beq + - bcs .done ; handle end of string -+ lda inbuf,x - cmp #"A" ; eat letters (and stop when we hit punctuation) - bcs - - bcc .store ; copy the ending punctuation and continue normal processing -.plursl - ldy tmp+1 ; erase singular form by backing up to prev punc - iny ; plus 1 to retain prev punc - bne .next ; resume regular copying of the plural form - -.notsl - cmp #"A" ; if <= ASCII "A", consider it punctuation - bcc + - clv ; clear last-is-punc flag - bvc .store ; always taken -+ bit fixedRTS; set prev-is-punc flag - sty tmp+1 ; save dest offset of last punctuation - -.store - sta inbuf,y ; save to dest + cpx inbuf + bcc .findsl ; loop to scan next char + bcs .done ; failsafe: handle missing end-paren (always taken) ++ ldx tmp+1 ; get back to start of parens + ; copy mode flag is now in V: if slash present, single=copy, plural=nocopy + ; if no slash: single=nocopy, plural=copy +.plup + inx + lda inbuf,x + cmp #"/" + bne + + php + pla + eor #$40 ; flip from copying to not-copying, or vice-versa + pha + plp + bcs .plup ; always taken ++ cmp #")" + beq .notpar ; stop at closing paren + bvc .plup ; if not in copy mode, skip copy iny + sta inbuf,y ; else do copy + bne .plup ; always taken +.notpar + bit fixedRTS; set prev-is-alpha flag + cmp #"A" ; if >= ASCII "A", consider it alpha + bcs .next + clv ; clear prev-is-alpha flag .next - inx cpx inbuf ; compare src offset to length bcc .fetch ; loop while less than - beq .fetch ; or equal .done - dey sty inbuf ; save new length lda #inbuf @@ -729,7 +724,7 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// // Print a carriage return -asm crout()#0 +export asm crout()#0 +asmPlasmNoRet 0 lda #$8D jmp _safeCout @@ -743,28 +738,6 @@ export asm beep()#0 rts end -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Read a string from the keyboard, turn it into a PLASMA string and return a pointer to the string. -export asm readStr()#1 - +asmPlasmRet 0 - bit setROM - jsr ROM_getln1 - bit setLcRW+lcBank2 - txa - pha - beq + -- lda inbuf-1,x - and #$7F - sta inbuf,x - dex - bne - -+ pla - sta inbuf,x - lda #inbuf - rts -end - /////////////////////////////////////////////////////////////////////////////////////////////////// // Send a command to the memory manager export asm mmgr(cmd, wordParam)#1 @@ -998,7 +971,8 @@ export asm streqi(a, b)#1 bne -- ; abort on inequality dex bne - - ldy #0 ; okay, they're equal. Return non-zero (A is guaranteed to be a character already) + lda #1 + ldy #0 ; okay, they're equal. Return 1 (not just any char; so that PLASMA when...is can work) rts end @@ -1098,12 +1072,6 @@ export def getStringResponse()#1 return mmgr(HEAP_INTERN, $200) end -/////////////////////////////////////////////////////////////////////////////////////////////////// -// Setter functions for library use -export def setPlural(flg)#0 - isPlural = flg -end - /////////////////////////////////////////////////////////////////////////////////////////////////// // Convert signed decimal to string in decimalBuf (@decimalBuf returned) def convertDec(n)#1 @@ -1230,14 +1198,6 @@ export def parseDec(str)#1 return n end -/////////////////////////////////////////////////////////////////////////////////////////////////// -export def parseDecWithDefault(str, default)#1 - if ^str == 0 - return default - fin - return parseDec(str) -end - /////////////////////////////////////////////////////////////////////////////////////////////////// // Get a keystroke and convert it to upper case export def getUpperKey()#1 @@ -1316,7 +1276,7 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// export def rollDiceWithLuck(encoded, luck)#1 byte i, nDice, dieSize, add, droll, result - nDice = encoded >> 12 + nDice = (encoded >> 12) & $F // must mask off replicated hi-bits dieSize = (encoded >> 8) & $F add = encoded & $F result = add @@ -1331,8 +1291,7 @@ export def rollDiceWithLuck(encoded, luck)#1 droll = min(droll, (rand16() % dieSize) + 1) fin fin - add = (rand16() % dieSize) + 1 - result = result + add + result = result + droll next return result end @@ -1361,13 +1320,13 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// // Look up the partition for a resource. // sectioNum: 1=map2d, 2=map3d, 3=portrait -def lookupResourcePart(sectionNum, resourceNum)#1 +export def lookupResourcePart(sectionNum, resourceNum)#1 word ptr byte n - // Skip to the requested section + // Skip to the requested section (starting just after version num) ptr = pResourceIndex - while sectionNum > 1 + while sectionNum > 0 ptr = ptr + readAuxByte(ptr) + 1 sectionNum-- loop @@ -1381,17 +1340,13 @@ def lookupResourcePart(sectionNum, resourceNum)#1 if curMapPartition > 0; return curMapPartition; fin return 2 fin - if n < 1 or n > 20; fatal("lkupFail2"); fin + if n < 0 or n > 20; fatal("lkupFail2"); fin // allow zero (e.g. portrait not used so not packed) return n end /////////////////////////////////////////////////////////////////////////////////////////////////// // Set the sky color (relevant to 3D display only) export def setSky(num)#0 - // hack for end-game screen - if num == 99 - finalWin() - fin skyNum = num setColor(0, skyNum) needRender = TRUE @@ -1405,6 +1360,36 @@ export def setGround(num)#0 needRender = TRUE end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def printVersion()#0 + word p, len, cv, ch + if !pResourceIndex; return; fin + cv = ^$25 + ^$23 = 24 // full height window + ^$25 = 22 + crout() + ^$24 = 25 + puts("V ") + setWindow(183, 192, 161, 261) + clearWindow() + setWindow(183, 192, 168, 252) + rawDisplayStr("^YV ") + p = pResourceIndex + len = readAuxByte(p) + while len + p++ + ch = readAuxByte(p) + printChar(ch) + displayChar(ch) + len-- + loop + rawDisplayStr("^N") + ^$23 = 23 // shrink window to protect version num + ^$25 = cv-1 + crout() + setWindow2() +end + /////////////////////////////////////////////////////////////////////////////////////////////////// // Load the Frame Image, and lock it. export def loadFrameImg(img)#0 @@ -1435,6 +1420,9 @@ export def loadFrameImg(img)#0 // And show the first frame of the screen image showAnimFrame() + + // Brand the image with the version number + printVersion() else curFullscreenImg = NULL anyAnims = FALSE @@ -1591,6 +1579,9 @@ export def scriptEvent(event, param)#0 word script if !nMapScripts; return; fin + if inScript; return; fin // avoid doing scripted events inside other scripts + inScript = TRUE + setWindow2() textDrawn = FALSE @@ -1605,6 +1596,8 @@ export def scriptEvent(event, param)#0 script(event, param) fin next + + inScript = FALSE if textDrawn textClearCountdown = 3 @@ -1650,11 +1643,15 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// export def loadMainFrameImg()#0 - loadFrameImg(mapIs3D+2) if curFullscreenImg auxMmgr(FREE_MEMORY, curFullscreenImg) curFullscreenImg = NULL fin + loadFrameImg(mapIs3D+2) + if curFullscreenImg + auxMmgr(FREE_MEMORY, curFullscreenImg) // we don't allow animated main frames, so save memory + curFullscreenImg = NULL + fin end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1796,15 +1793,17 @@ def doRender()#0 end /////////////////////////////////////////////////////////////////////////////////////////////////// -// Advance one step forward (works for either 3D or 2D maps) -def moveForward()#1 +def moveInternal(facingDir, moveDir, beepOK)#1 byte val word x, y + + setDir(moveDir) val = advance() + setDir(facingDir) // If not blocked, render at the new position. if val == 0 - beep() + if beepOK and !inScript; beep(); fin // don't beep for scripted moves else if !mapIs3D doRender() @@ -1813,7 +1812,7 @@ def moveForward()#1 fin fin - // If we're on a new map tile, run leave handlers. + // If we're on a new map tile, run leave handlers from old tile. if val >= 2 scriptEvent(@S_LEAVE, NULL) nMapScripts = 0 @@ -1825,14 +1824,22 @@ def moveForward()#1 scanScripts(x, y) if nMapScripts scriptEvent(@S_ENTER, NULL) - elsif global=>p_encounterZones and !skipEncounterCheck + elsif global=>p_encounterZones checkEncounter(x, y, FALSE) fin - elsif val >= 2 and global=>p_encounterZones and !skipEncounterCheck + elsif val >= 2 and global=>p_encounterZones getPos(@x, @y) checkEncounter(x, y, FALSE) fin - return 0 + return val +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Advance one step forward (works for either 3D or 2D maps) +def moveForward()#1 + byte dir + dir = getDir() + return moveInternal(dir, dir, TRUE) end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1847,23 +1854,25 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// // Move backward one step (3D mode). Also actually works in 2D mode. def moveBackward()#1 - adjustDir(8) - moveForward() - return adjustDir(8) + byte facingDir, moveDir + facingDir = getDir() + moveDir = (facingDir + 8) & 15 + moveInternal(facingDir, moveDir, TRUE) + return 0 end /////////////////////////////////////////////////////////////////////////////////////////////////// // Move backward two steps (3D mode), or one step (2D mode). This is often used when exiting a // building or fleeing combat, so we don't want to generate any random encounters. export def moveWayBackward()#1 - adjustDir(8) - skipEncounterCheck = TRUE - moveForward() + byte facingDir, moveDir + facingDir = getDir() + moveDir = (facingDir + 8) & 15 + moveInternal(facingDir, moveDir, FALSE) if mapIs3D - moveForward() + moveInternal(facingDir, moveDir, FALSE) fin - skipEncounterCheck = FALSE - return adjustDir(8) + return 0 end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1885,17 +1894,19 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// // Sidestep to the right (3D mode) def strafeRight()#1 - adjustDir(4) - moveForward() - return adjustDir(-4) + byte facingDir, moveDir + facingDir = getDir() + moveDir = (facingDir + 4) & 15 + return moveInternal(facingDir, moveDir, FALSE) end /////////////////////////////////////////////////////////////////////////////////////////////////// // Sidestep to the left (3D mode) def strafeLeft()#1 - adjustDir(-4) - moveForward() - return adjustDir(4) + byte facingDir, moveDir + facingDir = getDir() + moveDir = (facingDir - 4) & 15 + return moveInternal(facingDir, moveDir, FALSE) end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2105,7 +2116,9 @@ export def getYN()#1 if key == 'Y' return 1 elsif key == 'N' - clearTextWindow() + if frameLoaded + clearTextWindow() + fin break fin beep() @@ -2185,7 +2198,8 @@ export def setPortrait(portraitNum)#0 // Load the portrait image and display it part = lookupResourcePart(3, portraitNum) - if part > 1; part = curMapPartition; fin // Look on disk 1 or current disk only + // Commented out below, because it prevents cycling thru all portraits (in god mode) + // NO: if part > 1; part = curMapPartition; fin // Look on disk 1 or current disk only mmgr(START_LOAD, part) curPortrait = auxMmgr(QUEUE_LOAD, portraitNum<<8 | RES_TYPE_PORTRAIT) curPortraitNum = portraitNum @@ -2303,12 +2317,6 @@ def loadEngine(moduleNum)#1 flipToPage1() mmgr(START_LOAD, 1) // code is in partition 1 curEngine = mmgr(QUEUE_LOAD, moduleNum<<8 | RES_TYPE_MODULE) - // For combat module, pre-load some global funcs - if moduleNum == MOD_COMBAT - mmgr(QUEUE_LOAD, GS_COMBAT_INTRO<<8 | RES_TYPE_MODULE) - mmgr(QUEUE_LOAD, GS_COMBAT_PROMPT<<8 | RES_TYPE_MODULE) - mmgr(QUEUE_LOAD, GS_ENEMY_INTRO<<8 | RES_TYPE_MODULE) - fin mmgr(FINISH_LOAD, 0) return curEngine() // return function table end @@ -2323,7 +2331,12 @@ def returnFromEngine(render)#0 if renderLoaded; texControl(1); texturesLoaded = TRUE; fin mapNameHash = 0; showMapName(global=>s_mapName) clearTextWindow() - if render; doRender(); fin + if render + doRender() + else + needRender = TRUE + fin + if mapIs3D; showCompassDir(getDir()); fin showParty() setWindow2() // in case we're mid-script fin @@ -2357,13 +2370,21 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// // Load the Party engine and show data for the given player def showPlayerSheet(num)#1 - word pItemToUse + word pItemToUse, oldFlg if num+1 > countList(global=>p_players); beep; return 0; fin pItemToUse = loadEngine(MOD_PARTY)=>party_showPlayerSheet(num) returnFromEngine(TRUE) // General 'use' handled here in case it triggers graphical effects if pItemToUse + oldFlg = textDrawn + textDrawn = FALSE scriptEvent(@S_USE, pItemToUse=>s_name) + if !textDrawn + displayStr("\nNothing happened.") + textDrawn = TRUE + else + textDrawn = oldFlg + fin fin return 0 end @@ -2391,6 +2412,7 @@ def levelUp()#1 return 0 fin player = player=>p_nextObj + n++ loop beep return 0 @@ -2433,13 +2455,14 @@ def doCombat(mapCode, backUpOnFlee)#1 // Handled in a separate module. Clear enemies out of the heap when finished. result = loadEngine(MOD_COMBAT)=>combat_zoneEncounter(mapCode) global=>p_enemyGroups = NULL + mmgr(CHECK_MEM, 0) mmgr(HEAP_COLLECT, 0) if (result == -99) playerDeath() return 0 fin - returnFromEngine(TRUE) + returnFromEngine(!inScript) // only re-render if outside script // If the party fled the combat instead of winning, back up to previous square. if !result and backUpOnFlee @@ -2455,6 +2478,9 @@ export def checkEncounter(x, y, force)#0 word p_bestZone, bestDist word d + // Don't check for encounter during scripted move + if inScript; return; fin + // Find the zone that's closest, but not too far. bestDist = INT_MAX p_bestZone = NULL @@ -2511,6 +2537,7 @@ def toggleGodMode()#1 if ^kbd == $84 // ctrl-D ^kbdStrobe global->b_godmode = !global->b_godmode + flipToPage1() clearTextWindow() displayf1("gm:%d\n", global->b_godmode & 1) beep; beep @@ -2521,15 +2548,6 @@ def toggleGodMode()#1 return 0 end -/////////////////////////////////////////////////////////////////////////////////////////////////// -export def finalWin()#0 - flipToPage1() - loadFrameImg(4) // total hack - while 1 // 1 infinite loop - getUpperKey() - loop -end - /////////////////////////////////////////////////////////////////////////////////////////////////// export def setCmd(key, func)#0 cmdTbl[key] = func @@ -2641,10 +2659,6 @@ def loadTitle()#0 auxMmgr(SET_MEM_TARGET, expandVec) auxMmgr(QUEUE_LOAD, CODE_EXPAND<<8 | RES_TYPE_CODE) - // Also grab the resource index (put it in aux) - pResourceIndex = auxMmgr(QUEUE_LOAD, CODE_RESOURCE_INDEX<<8 | RES_TYPE_CODE) - auxMmgr(LOCK_MEMORY, pResourceIndex) - mmgr(FINISH_LOAD, 0) // Tell the font engine where to find its font @@ -2663,13 +2677,20 @@ def loadTitle()#0 auxMmgr(SET_MEM_TARGET, expandVec) auxMmgr(REQUEST_MEMORY, expanderSize) auxMmgr(LOCK_MEMORY, expandVec) + + // To reduce fragmentation, load the resource index directly after the + // remaining part of the expander + pResourceIndex = expandVec + expanderSize + auxMmgr(SET_MEM_TARGET, pResourceIndex) + auxMmgr(QUEUE_LOAD, CODE_RESOURCE_INDEX<<8 | RES_TYPE_CODE) + auxMmgr(LOCK_MEMORY, pResourceIndex) + mmgr(FINISH_LOAD, 0) end /////////////////////////////////////////////////////////////////////////////////////////////////// // Set up the small-object heap. Set loadedSize to zero on initial, or non-zero for loaded game. export def initHeap(loadedSize)#0 byte i - word typeHash if !heapLocked mmgr(SET_MEM_TARGET, HEAP_BOTTOM) @@ -2687,12 +2708,8 @@ export def initHeap(loadedSize)#0 mmgr(HEAP_ADD_TYPE, typeTbls[i]) i = i+1 loop - typeHash = hashBuffer(@typeTbl_Global, @typeTbls - @typeTbl_Global) ^ HEAP_BOTTOM if loadedSize <> 0 global = HEAP_BOTTOM - if global=>w_typeHash <> typeHash - fatal("Incompatible saved game") - fin else global = mmgr(HEAP_ALLOC, TYPE_GLOBAL) global=>w_typeHash = typeHash @@ -2763,12 +2780,23 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// // Add to a list if named obj not already in it; returns TRUE if really added. +// Also handles incrementing stackable items. export def addUnique(pList, p_thing)#1 - if !scanForNamedObj(*pList, p_thing=>s_name) - addToList(pList, p_thing) - return TRUE + word p_existing + + // If it's stackable and player already has some, just increase the stack + p_existing = scanForNamedObj(*pList, p_thing=>s_name) + if p_existing + if p_existing->t_type == TYPE_FANCY_ITEM and p_existing=>w_count > 0 and !p_existing=>p_modifiers + p_existing=>w_count = p_existing=>w_count + p_thing=>w_count + return TRUE + fin + return FALSE // already have one, and it's not stackable fin - return FALSE + + // Otherwise, add a new item + addToList(pList, p_thing) + return TRUE end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2868,7 +2896,8 @@ end export def addPlayerToParty(playerFuncNum)#0 word p if countList(global=>p_players) == MAX_PARTY - displayStr("Party too large.") + rawDisplayStr("Party too large.") + beep return fin p = createAndAddUnique(MOD_GEN_PLAYERS, playerFuncNum, @global=>p_players) @@ -2882,7 +2911,12 @@ export def removeNamed(name, pList)#0 word p_thing p_thing = scanForNamedObj(*pList, name) if p_thing - removeFromList(pList, p_thing) + // If it's stackable and there's more than one, just reduce the count. Otherwise take it all. + if p_thing->t_type == TYPE_FANCY_ITEM and p_thing=>w_count > 1 + p_thing=>w_count-- + else + removeFromList(pList, p_thing) + fin else printf1("Warning: couldn't find '%s' to remove.\n", name) fin @@ -2912,29 +2946,29 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// export def getStat(player, statName)#1 word pSkill - when statName - is @S_INTELLIGENCE; return player->b_intelligence - is @S_STRENGTH; return player->b_strength - is @S_AGILITY; return player->b_agility - is @S_STAMINA; return player->b_stamina - is @S_CHARISMA; return player->b_charisma - is @S_SPIRIT; return player->b_spirit - is @S_LUCK; return player->b_luck - is @S_HEALTH; return player=>w_health - is @S_MAX_HEALTH; return player=>w_maxHealth - is @S_AIMING; return player->b_aiming - is @S_HAND_TO_HAND; return player->b_handToHand - is @S_DODGING; return player->b_dodging - is @S_GOLD; return global=>w_gold - is @S_XP; return player=>w_curXP - is @S_SP; return player->b_skillPoints + when 1 + is streqi(statName, @S_INTELLIGENCE); return player->b_intelligence + is streqi(statName, @S_STRENGTH); return player->b_strength + is streqi(statName, @S_AGILITY); return player->b_agility + is streqi(statName, @S_STAMINA); return player->b_stamina + is streqi(statName, @S_CHARISMA); return player->b_charisma + is streqi(statName, @S_SPIRIT); return player->b_spirit + is streqi(statName, @S_LUCK); return player->b_luck + is streqi(statName, @S_HEALTH); return player=>w_health + is streqi(statName, @S_MAX_HEALTH); return player=>w_maxHealth + is streqi(statName, @S_AIMING); return player->b_aiming + is streqi(statName, @S_HAND_TO_HAND); return player->b_handToHand + is streqi(statName, @S_DODGING); return player->b_dodging + is streqi(statName, @S_GOLD); return global=>w_gold + is streqi(statName, @S_XP); return player=>w_curXP + is streqi(statName, @S_SP); return player->b_skillPoints wend pSkill = player=>p_skills while pSkill if streqi(statName, pSkill=>s_name); return pSkill=>w_modValue; fin pSkill = pSkill=>p_nextObj loop - puts(statName); return fatal("Unknown stat") + puts(statName); return fatal("getStat") end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2953,22 +2987,22 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// export def setStat(player, statName, val)#0 word pSkill - when statName - is @S_INTELLIGENCE; player->b_intelligence = clampByte(val); break - is @S_STRENGTH; player->b_strength = clampByte(val); break - is @S_AGILITY; player->b_agility = clampByte(val); break - is @S_STAMINA; player->b_stamina = clampByte(val); break - is @S_CHARISMA; player->b_charisma = clampByte(val); break - is @S_SPIRIT; player->b_spirit = clampByte(val); break - is @S_LUCK; player->b_luck = clampByte(val); break - is @S_HEALTH; player=>w_health = max(0, min(player=>w_maxHealth, val)); needShowParty = TRUE; break - is @S_MAX_HEALTH; player=>w_maxHealth = max(0, val); break - is @S_AIMING; player->b_aiming = clampByte(val); break - is @S_HAND_TO_HAND; player->b_handToHand = clampByte(val); break - is @S_DODGING; player->b_dodging = clampByte(val); break - is @S_GOLD; global=>w_gold = max(0, val); needShowParty = TRUE; break - is @S_XP; player=>w_curXP = max(player=>w_curXP, val); needShowParty = TRUE; break - is @S_SP; player->b_skillPoints = max(0, val); needShowParty = TRUE; break + when 1 + is streqi(statName, @S_INTELLIGENCE); player->b_intelligence = clampByte(val); break + is streqi(statName, @S_STRENGTH); player->b_strength = clampByte(val); break + is streqi(statName, @S_AGILITY); player->b_agility = clampByte(val); break + is streqi(statName, @S_STAMINA); player->b_stamina = clampByte(val); break + is streqi(statName, @S_CHARISMA); player->b_charisma = clampByte(val); break + is streqi(statName, @S_SPIRIT); player->b_spirit = clampByte(val); break + is streqi(statName, @S_LUCK); player->b_luck = clampByte(val); break + is streqi(statName, @S_HEALTH); player=>w_health = max(0, min(player=>w_maxHealth, val)); needShowParty = TRUE; break + is streqi(statName, @S_MAX_HEALTH); player=>w_maxHealth = max(0, val); break + is streqi(statName, @S_AIMING); player->b_aiming = clampByte(val); break + is streqi(statName, @S_HAND_TO_HAND); player->b_handToHand = clampByte(val); break + is streqi(statName, @S_DODGING); player->b_dodging = clampByte(val); break + is streqi(statName, @S_GOLD); global=>w_gold = max(0, val); needShowParty = TRUE; break + is streqi(statName, @S_XP); player=>w_curXP = max(player=>w_curXP, val); needShowParty = TRUE; break + is streqi(statName, @S_SP); player->b_skillPoints = max(0, val); needShowParty = TRUE; break otherwise pSkill = player=>p_skills while pSkill @@ -2978,33 +3012,28 @@ export def setStat(player, statName, val)#0 fin pSkill = pSkill=>p_nextObj loop - puts(statName); fatal("Unknown stat") + puts(statName); fatal("setStat") wend end /////////////////////////////////////////////////////////////////////////////////////////////////// -export def setGameFlag(flagName, val)#0 - word p_flag - p_flag = scanForNamedObj(global=>p_gameFlags, flagName) - if p_flag - if val == 0 // setting flag to zero removes it - removeFromList(@global=>p_gameFlags, p_flag) - else - p_flag=>w_modValue = val - fin - elsif val <> 0 - addToList(@global=>p_gameFlags, makeModifier(flagName, val)) +export def setGameFlag(flagNum, val)#0 + byte byteNum, mask + byteNum = flagNum >> 3 + mask = 1<<(flagNum & 7) + if val + global->ba_gameFlags[byteNum] = global->ba_gameFlags[byteNum] | mask + else + global->ba_gameFlags[byteNum] = global->ba_gameFlags[byteNum] & ~mask fin end /////////////////////////////////////////////////////////////////////////////////////////////////// -export def getGameFlag(flagName)#1 - word p_flag - p_flag = scanForNamedObj(global=>p_gameFlags, flagName) - if p_flag - return p_flag=>w_modValue - fin - return 0 +export def getGameFlag(flagNum)#1 + byte byteNum, mask + byteNum = flagNum >> 3 + mask = 1<<(flagNum & 7) + return global->ba_gameFlags[byteNum] & mask end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3044,10 +3073,23 @@ export def buySell(storeCode, profitRatio)#0 if portrait; setPortrait(portrait); fin end +/////////////////////////////////////////////////////////////////////////////////////////////////// +export def numToPlayer(num)#1 + word player + player = global=>p_players + while player and num > 0 + player = player=>p_nextObj + num-- + loop + return player +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def startGame(ask)#0 word p_module + typeHash = hashBuffer(@typeTbl_Global, @typeTbls - @typeTbl_Global) ^ HEAP_BOTTOM + // Create a new game or load an existing one mmgr(START_LOAD, 1) // code is in partition 1 p_module = mmgr(QUEUE_LOAD, MOD_DISKOPS<<8 | RES_TYPE_MODULE) diff --git a/Platform/Apple/virtual/src/plasma/godmode.pla b/Platform/Apple/virtual/src/plasma/godmode.pla index 7ea9c0cb..9f321835 100644 --- a/Platform/Apple/virtual/src/plasma/godmode.pla +++ b/Platform/Apple/virtual/src/plasma/godmode.pla @@ -16,6 +16,7 @@ include "gen_images.plh" include "gen_modules.plh" include "gen_items.plh" include "gen_players.plh" +include "gen_flags.plh" // Exported functions go here. First a predef for each one, then a table with function pointers // in the same order as the constants are defined in the the header. @@ -24,6 +25,56 @@ predef _addItem(player)#1 predef _addPlayer()#1 word[] funcTbl = @_setCheatCmds, @_addItem, @_addPlayer +asm _defs + +; Use hi-bit ASCII for Apple II +!convtab "../../include/hiBitAscii.ct" + +; Headers +!source "../../include/global.i" +!source "../../include/plasma.i" +!source "../../include/mem.i" + +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Read a string from the keyboard, turn it into a PLASMA string and return a pointer to the string. +asm readStr()#1 + +asmPlasmRet 0 + bit setROM + jsr ROM_getln1 + bit setLcRW+lcBank2 + txa + pha + beq + +- lda inbuf-1,x + and #$7F + sta inbuf,x + dex + bne - ++ pla + sta inbuf,x + lda #inbuf + rts +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def parseDecWithDefault(str, default)#1 + if ^str == 0 + return default + fin + return parseDec(str) +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def splitScreenMode()#0 + flipToPage1() + textHome() + ^$c053 + ^$25 = 19 + crout() +end /////////////////////////////////////////////////////////////////////////////////////////////////// // Godmode cheats @@ -32,12 +83,10 @@ def kbdTeleport()#1 word x, y byte dir - flipToPage1() - ^$c053 - if ^$25 < 23; ^$25 = 23; fin + splitScreenMode() getPos(@x, @y) dir = getDir() - printf3("\nCurrent: X=%d Y=%d Facing=%d\n", x, y, dir) + printf3("Current: X=%d Y=%d Facing=%d\n", x, y, dir) printf1("3D [%d]: ", mapIs3D) d3 = parseDecWithDefault(readStr(), mapIs3D) @@ -62,11 +111,9 @@ def showPos()#1 word x, y byte dir - flipToPage1() - ^$c053 - if ^$25 < 23; ^$25 = 23; fin + splitScreenMode() getPos(@x, @y) - printf2("\nX=%d Y=%d ", x, y) + printf2("X=%d Y=%d ", x, y) if mapIs3D printf3("Facing=%d Sky=%d Ground=%d", getDir(), skyNum, groundNum) fin @@ -77,19 +124,28 @@ def showPos()#1 end /////////////////////////////////////////////////////////////////////////////////////////////////// -def nextPortrait()#1 - portraitNum = portraitNum + 1 - if portraitNum > PO_LAST; portraitNum = 1; fin +def advPortrait(dir)#1 + while TRUE + portraitNum = portraitNum + dir + if portraitNum > PO_LAST + portraitNum = 1 + elsif portraitNum < 1 + portraitNum = PO_LAST + fin + if lookupResourcePart(3, portraitNum); break; fin + loop setPortrait(portraitNum) return 0 end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def nextPortrait()#1 + return advPortrait(1) +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def prevPortrait()#1 - portraitNum = portraitNum - 1 - if portraitNum < 1; portraitNum = PO_LAST; fin - setPortrait(portraitNum) - return 0 + return advPortrait(-1) end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -126,6 +182,45 @@ def nextGround()#1 return 0 end +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Edit flags cheat +def editFlags()#1 + word pModule, funcTbl, flagName, flagNum + flipToPage1 + ^$c051 + textHome() + *$4000 = 0 // so renderer knows we messed up the page + puts("Game flags:") + mmgr(START_LOAD, 1) // code is in partition 1 + pModule = mmgr(QUEUE_LOAD, MOD_GEN_FLAGS<<8 | RES_TYPE_MODULE) + mmgr(FINISH_LOAD, 0) + funcTbl = pModule() + while TRUE + ^$25 = 0 + crout() + for flagNum = 0 to NUM_GAME_FLAGS-1 + if flagNum > 0; puts(", "); fin + flagName = funcTbl=>flags_nameForNumber(flagNum) + // make sure flag name will fit on line + if ^$24 + ^flagName > 37; crout(); fin + ^$32 = $FF // normal mode + if getGameFlag(flagNum); ^$32 = $3F; fin // inverse mode + puts(flagName) + ^$32 = $FF + next + puts("\nToggle: ") + ^$24 = 8 + flagName = readStr() + if !^flagName; break; fin + flagNum = funcTbl=>flags_numberForName(flagName) + if flagNum < 0; beep; continue; fin + setGameFlag(flagNum, !getGameFlag(flagNum)) + loop + ^$c050 + mmgr(FREE_MEMORY, pModule) + return 0 +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def _setCheatCmds()#1 // install cheat commands @@ -137,7 +232,7 @@ def _setCheatCmds()#1 setCmd('Y', @nextSky) setCmd('G', @nextGround) setCmd('&', @printMem) - setCmd('_', @finalWin) + setCmd('^', @editFlags) return 0 end @@ -176,10 +271,14 @@ def selectThing(moduleNum, nThings, nSkip, prompt)#1 nFunc = -1 textHome() fin + elsif moduleNum == MOD_GEN_PLAYERS + // Players are big, so need collect each time + mmgr(HEAP_COLLECT, 0) fin next mmgr(FREE_MEMORY, pModule) + ^$c050 return nFunc end @@ -188,11 +287,7 @@ end def _addItem(player)#1 word funcNum funcNum = selectThing(MOD_GEN_ITEMS, NUM_ITEMS, 2, "Add item #: ") - if funcNum >= 0 - giveItemToPlayer(player, funcNum) - rdkey() - fin - ^$c050 + if funcNum >= 0; giveItemToPlayer(player, funcNum); fin return 0 end @@ -201,11 +296,7 @@ end def _addPlayer()#1 word funcNum funcNum = selectThing(MOD_GEN_PLAYERS, NUM_PLAYERS, 1, "Add player #: ") - if funcNum >= 0 - addPlayerToParty(funcNum) - rdkey() - fin - ^$c050 + if funcNum >= 0; addPlayerToParty(funcNum); fin return 0 end diff --git a/Platform/Apple/virtual/src/plasma/party.pla b/Platform/Apple/virtual/src/plasma/party.pla index 027d03d8..5ce3ff4a 100644 --- a/Platform/Apple/virtual/src/plasma/party.pla +++ b/Platform/Apple/virtual/src/plasma/party.pla @@ -68,6 +68,14 @@ def travFind2(pObj, val, func) return NULL end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def isEquipped(pItem)#1 + if pItem->t_type == TYPE_ARMOR or pItem->t_type == TYPE_WEAPON + return pItem->b_flags & ITEM_FLAG_EQUIP + fin + return FALSE +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def itemByNum(player, num)#1 return travFind2(player=>p_items, num, &(n,p)(n-1, n==0)) @@ -81,7 +89,7 @@ def unequip(player, type, kind)#1 item = player=>p_items while item if item->t_type == type - if (streqi(item=>s_itemKind, kind) or type == TYPE_WEAPON) and item->b_flags & ITEM_FLAG_EQUIP + if (streqi(item=>s_itemKind, kind) or type == TYPE_WEAPON) and isEquipped(item) item->b_flags = item->b_flags & ~ITEM_FLAG_EQUIP break fin @@ -97,7 +105,7 @@ end def showColumnTitle(x, title, page, nPages)#0 rawDisplayf2("^V000\n^J^J^L^J^T%D%s", x, title) if nPages > 1 - rawDisplayf2(" - p.%d/%d", page, nPages) + rawDisplayf2(" p.%d/%d", page, nPages) fin rawDisplayStr("^N\n^J^J") end @@ -125,10 +133,8 @@ def showInventory(player, page, select)#1 if !first; displayChar('\n'); fin first = FALSE - if item->t_type == TYPE_WEAPON or item->t_type == TYPE_ARMOR - if item->b_flags & ITEM_FLAG_EQUIP - rawDisplayStr("*") - fin + if isEquipped(item) + rawDisplayStr("*") fin rawDisplayf2("^T%D%c.", INVLBL_X, 'A' + s_item) @@ -142,17 +148,6 @@ def showInventory(player, page, select)#1 return s_item end -def numToPlayer(num)#1 - word player - player = global=>p_players - while num > 0 - player = player=>p_nextObj - if !player; break; fin // Not that many players - num-- - loop - return player -end - def displayDice(dice)#0 byte n, d, p n = (dice >> 12) & $0F @@ -196,7 +191,7 @@ def showDerived(player)#0 // Get weapon weapon = player=>p_items while weapon - if weapon->t_type == TYPE_WEAPON and weapon->b_flags & ITEM_FLAG_EQUIP; break; fin + if weapon->t_type == TYPE_WEAPON and isEquipped(weapon); break; fin weapon = weapon=>p_nextObj loop @@ -339,12 +334,8 @@ def showInvMenu(player, totalItems, itemPage, itemsOnPage)#0 rawDisplayf1("Item [A-%c], ", itemsOnPage-1+'A') if totalItems > INV_ROWS rawDisplayStr("Pg [") - if totalItems > (itemPage + 1) * INV_ROWS - rawDisplayStr(">") - fin - if itemPage - rawDisplayStr("<") - fin + if totalItems > (itemPage + 1) * INV_ROWS; rawDisplayStr(">"); fin + if itemPage; rawDisplayStr("<"); fin rawDisplayStr("], ") fin fin @@ -372,18 +363,37 @@ def showSkillsMenu()#0 rawDisplayStr("X:Inv or [Esc]") end +def isSplittable(item)#1 + // Disallow splitting items with modifiers, because too edge-casey + return item->t_type == TYPE_FANCY_ITEM and item=>w_count > 1 and !item=>p_modifiers +end + +def isJoinable(item)#1 + // Disallow joining items with modifiers, because too edge-casey + return item->t_type == TYPE_FANCY_ITEM and item=>w_count > 0 and !item=>p_modifiers +end + // Display menu for selecting inventory items def showItemMenu(item)#0 byte type clearMenuRect() type = item->t_type if type == TYPE_ARMOR or type == TYPE_WEAPON - rawDisplayStr("E)quip/unequip, ") - fin - if type == TYPE_ITEM + if isEquipped(item) + rawDisplayStr("U)nequip, ") + else + rawDisplayStr("E)quip, ") + fin + elsif type == TYPE_PLAIN_ITEM or type == TYPE_FANCY_ITEM rawDisplayStr("U)se, ") + if isSplittable(item); rawDisplayStr("S)plit, "); fin + if isJoinable(item); rawDisplayStr("J)oin, "); fin fin - rawDisplayStr("D)estroy or [Esc]") + if !isEquipped(item) + if global=>p_players=>p_nextObj; rawDisplayStr("T)rade, "); fin + rawDisplayStr("D)rop ") + fin + rawDisplayStr("or [Esc]") end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -398,6 +408,7 @@ def clearMainRect()#0 setWindow(BIGWIN_TOP+9, BIGWIN_BOTTOM-10, BIGWIN_LEFT, BIGWIN_RIGHT) clearWindow() setBigWindow() + rawDisplayStr("^V000\n^J^J^J") end // Equip/unequip an item. @@ -408,26 +419,151 @@ def doEquip(player, item)#0 calcPlayerArmor(player) end +def choosePlayer(disp, chooseNum, avoid) + word player + byte num + player = global=>p_players + num = 0 + while player + if player <> avoid + if num; rawDisplayStr(", "); fin + if disp + rawDisplayf2("%c) %s", num+'A', player=>s_name) + elsif num == chooseNum + return player + fin + num++ + fin + player = player=>p_nextObj + loop + return num +end + +/////////////////////////////////////////////////////////////////////////////////////////////////// +def displayDone()#1 + clearMenuRect() + clearMainRect() + rawDisplayStr("Done.") + pause(800) + return NULL +end + +// Trade an item to another player/npc +def doTrade(player, item)#1 + word destPlayer + byte first, sel + + clearMenuRect() + rawDisplayStr("To: ") + destPlayer = global=>p_players + sel = 0 + first = TRUE + while destPlayer + if player <> destPlayer + if !first; rawDisplayStr(", "); fin + first = FALSE + rawDisplayf2("%d. %s", sel+1, destPlayer=>s_name) + fin + sel++ + destPlayer = destPlayer=>p_nextObj + loop + rawDisplayStr(" or [Esc]") + + while TRUE + sel = getUpperKey() + if sel == $1B; return 0; fin + destPlayer = numToPlayer(sel-'1') + if destPlayer and destPlayer <> player + removeFromList(@player=>p_items, item) + addUnique(@destPlayer=>p_items, item) + return displayDone + fin + beep + loop + return 0 +end + +// Split a stack of stackables +def doSplit(player, item)#1 + word nToSplit, newItem + if item=>w_count == 2 + nToSplit = 1 + else + clearMenuRect + rawDisplayf1("Split off 1-%d: ", item=>w_count - 1) + nToSplit = parseDec(getStringResponse()) + fin + if nToSplit + item=>w_count = item=>w_count - nToSplit + newItem = mmgr(HEAP_ALLOC, TYPE_FANCY_ITEM) + memcpy(item, newItem, FancyItem) + newItem=>w_count = nToSplit + newItem=>p_nextObj = item=>p_nextObj + item=>p_nextObj = newItem + return 1 + fin + beep + return 0 +end + +// Join a stack of stackables +def doJoin(player, item)#1 + word match, pPrev + byte anyJoined + pPrev = @player=>p_items + match = player=>p_items + anyJoined = FALSE + while match + if match <> item and match->t_type == TYPE_FANCY_ITEM + if streqi(match=>s_name, item=>s_name) + item=>w_count = item=>w_count + match=>w_count + *pPrev = match=>p_nextObj + match = match=>p_nextObj + anyJoined = TRUE + continue + fin + fin + pPrev = @match=>p_nextObj + match = *pPrev + loop + if anyJoined + displayDone() + return 1 + fin + clearMenuRect() + rawDisplayStr("No joinable stack found.") + beep + pause(800) + return 0 +end + // Select an item and use it. Returns item if it needs to be processed by outer loop, else NULL def doUse(player, item)#1 - if item=>p_modifiers - if streqi(item=>p_modifiers=>s_name, @S_HEALTH) - clearMenuRect() - clearMainRect() - rawDisplayStr("^V000\n^J^J^J") - if player=>w_health < player=>w_maxHealth - player=>w_health = min(player=>w_health + item=>p_modifiers=>w_modValue, player=>w_maxHealth) - item->b_curUses++ - if item->b_curUses >= item->b_maxUses // all used up - removeFromList(@player=>p_items, item) + word pMod, oldVal, newVal + if item->t_type == TYPE_FANCY_ITEM and item=>p_modifiers + clearMenuRect() + clearMainRect() + pMod = item=>p_modifiers + while pMod + oldVal = getStat(player, pMod=>s_name) + setStat(player, pMod=>s_name, oldVal + pMod=>w_modValue) + newVal = getStat(player, pMod=>s_name) + rawDisplayStr(pMod=>s_name) + if newVal <> oldVal + takeItemFromPlayer(player, item=>s_name) // also handles reducing count of stackables + if newVal > oldVal + rawDisplayStr(" increased") + else + rawDisplayStr(" decreased") fin - rawDisplayf2("Healed to %d/%d", player=>w_health, player=>w_maxHealth) + rawDisplayf2(" from %d to %d.", oldVal, newVal) else - rawDisplayStr("No healing needed.") + rawDisplayStr(" already at the limit.") fin pause(800) - return NULL - fin + pMod = pMod=>p_nextObj + loop + return NULL fin return item // general 'use' handled by outer engine, because it might involve graphics end @@ -435,7 +571,7 @@ end // Select an item and drop it. Returns TRUE if anything changed def doDestroy(player, item)#1 clearMenuRect() - rawDisplayStr("Destroy ") + rawDisplayStr("Drop ") _displayItemName(item) rawDisplayStr(" (Y/N)?") if getYN() @@ -451,11 +587,9 @@ def matchEquipped(player, match)#1 word item item = player=>p_items while item - if (item->t_type == match->t_type) and (item->t_type == TYPE_WEAPON or item->t_type == TYPE_ARMOR) - if item <> match and (item->b_flags & ITEM_FLAG_EQUIP) - if item->t_type <> TYPE_ARMOR or (item=>s_itemKind == match=>s_itemKind) - return item - fin + if item->t_type == match->t_type and item <> match and isEquipped(item) + if item->t_type <> TYPE_ARMOR or (item=>s_itemKind == match=>s_itemKind) + return item fin fin item = item=>p_nextObj @@ -466,7 +600,7 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def displayItems(pItem1, pItem2)#0 clearMainRect() - rawDisplayf1("^V000\n^J^J^L^J^T%DInventory", STATS_COL_1) + rawDisplayf1("^T%DInventory", STATS_COL_1) if pItem2 rawDisplayf1("^T%DEquipped", STATS_COL_2) fin @@ -477,37 +611,69 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def interactWithItem(player, item)#1 word comp, quantity - byte sel + byte sel, ok + ok = TRUE + displayItems(item, matchEquipped(player, item)) while TRUE - displayItems(item, matchEquipped(player, item)) - showItemMenu(item) + if ok + showItemMenu(item) + else + beep + fin + ok = FALSE sel = getUpperKey() - rawDisplayf1(" %c\n", sel) when sel - // Equip/unequip player with weapon/armor + // Equip player with weapon/armor is 'E' - if item->t_type == TYPE_ARMOR or item->t_type == TYPE_WEAPON + if (item->t_type == TYPE_ARMOR or item->t_type == TYPE_WEAPON) and !isEquipped(item) doEquip(player, item) - return NULL - else - beep + return displayDone() fin break - // Use an item + // Use or Unequip an item is 'U' - if item->t_type == TYPE_ITEM + if item->t_type == TYPE_PLAIN_ITEM or item->t_type == TYPE_FANCY_ITEM return doUse(player, item) // general 'use' handled by outer engine, because it might involve graphics - else - beep + elsif (item->t_type == TYPE_ARMOR or item->t_type == TYPE_WEAPON) and isEquipped(item) + doEquip(player, item) + displayItems(item, matchEquipped(player, item)) + ok = TRUE + fin + break + // Trade an item + is 'T' + if global=>p_players=>p_nextObj and !isEquipped(item) + return doTrade(player, item) + fin + break + // Split a stack + is 'S' + if isSplittable(item) + if doSplit(player, item) + return NULL + fin + fin + break + // Join stacks + is 'J' + if isJoinable(item) + if doJoin(player, item) + return NULL + fin fin break // Destroy an item is 'D' - if doDestroy(player, item); return NULL; fin + if !isEquipped(item) + if doDestroy(player, item) + displayDone() + return NULL + fin + ok = TRUE + fin break is $1B // Esc return NULL - otherwise beep wend loop return NULL @@ -538,18 +704,18 @@ def _showPlayerSheet(player_num)#1 // funcTbl functions always have to return a rawDisplayf1("^Y^I %s ^N\n", player=>s_name) redisplay = 1 totalItems = countList(player=>p_items) - elsif redisplay > 0 - clearInvRect() - fin - if redisplay > 0 if mode == 'I' - itemsOnPage = showInventory(player, i_page, 0) showDerived(player) else // 'S' showSkills(player) fin - redisplay = 0 + elsif redisplay > 0 + clearInvRect() fin + if redisplay > 0 and mode == 'I' + itemsOnPage = showInventory(player, i_page, 0) + fin + redisplay = 0 if !noRepeatMenu if mode == 'I' @@ -563,22 +729,24 @@ def _showPlayerSheet(player_num)#1 // funcTbl functions always have to return a // Get a key, do something sel = getUpperKey() when sel - is '1' - is '2' - is '3' + is '1'; is '2'; is '3' sel = sel - '1' - if player_num <> sel and countList(global=>p_players) > sel + if countList(global=>p_players) > sel player_num = sel i_page = 0 redisplay = 2 else beep fin + break // Next inventory page is '>' if mode=='I' and totalItems > (i_page + 1) * INV_ROWS i_page++ redisplay = 1 + else + beep + noRepeatMenu = TRUE fin break // Previous inventory page @@ -587,6 +755,9 @@ def _showPlayerSheet(player_num)#1 // funcTbl functions always have to return a if mode=='I' and i_page i_page-- redisplay = 1 + else + beep + noRepeatMenu = TRUE fin break // Other operations... @@ -604,13 +775,16 @@ def _showPlayerSheet(player_num)#1 // funcTbl functions always have to return a break is '%' // add item cheat if global->b_godmode + clearMainRect() pGodModule=>godmode_addItem(player) - redisplay = 1 + redisplay = 2 fin break is '9' // add player cheat if global->b_godmode + clearMainRect() pGodModule=>godmode_addPlayer() + redisplay = 2 fin break is '+' // level up cheat @@ -620,24 +794,30 @@ def _showPlayerSheet(player_num)#1 // funcTbl functions always have to return a fin break is $1B // Esc + mmgr(CHECK_MEM, 0) + mmgr(HEAP_COLLECT, 0) return NULL otherwise - if sel == 'X' and mode <> 'I' + if sel == 'X' and mode <> 'I' // switch from stats to inv mode = 'I' redisplay = 2 - elsif (sel == 'S' or sel == 'U') and mode <> 'S' + elsif (sel == 'S' or sel == 'U') and mode <> 'S' // switch from inv to stats mode = 'S' redisplay = 2 elsif mode == 'I' sel = sel - 'A' if sel >= 0 and sel < itemsOnPage - item = interactWithItem(player, itemByNum(player, sel)) + item = interactWithItem(player, itemByNum(player, i_page * INV_ROWS + sel)) if item; return item; fin // Use an item + if countList(player=>p_items) <= i_page * INV_ROWS // destroyed last item on pg 2 + i_page-- + fin redisplay = 2 else beep + noRepeatMenu = TRUE fin - elsif mode == 'S' + else // mode == 'S' noRepeatMenu = TRUE if sel >= 'A' and (sel-'A' < nSkills) adjustSkill(player, sel - 'A', 1) @@ -647,12 +827,10 @@ def _showPlayerSheet(player_num)#1 // funcTbl functions always have to return a beep noRepeatMenu = FALSE fin - else - beep fin wend until 0 - return NULL + return NULL // never reached end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -755,9 +933,9 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def formatEquipped(num)#1 if num == 1 - rawDisplayStr("Y") + rawDisplayStr("yes") else - rawDisplayStr("N") + rawDisplayStr("no") fin return 0 end @@ -818,7 +996,6 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def displayWeaponStats(pItem1, pItem2)#0 displayTwoCol("Equip'd", pItem1, pItem2, b_flags, @equippedField, @formatEquipped) - displayTwoCol("Uses", pItem1, pItem2, b_maxUses, @byteField, @formatNum) displayTwoCol("Ammo", pItem1, pItem2, s_ammoKind, @wordField, @formatStr) displayTwoCol("Clip", pItem1, pItem2, b_clipSize, @byteField, @formatNum) displayTwoCol("Melee", pItem1, pItem2, r_meleeDmg, @wordField, @formatDice) @@ -832,25 +1009,19 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def displayArmorStats(pItem1, pItem2)#0 displayTwoCol("Equip'd", pItem1, pItem2, b_flags, @equippedField, @formatEquipped) - displayTwoCol("Uses", pItem1, pItem2, b_maxUses, @byteField, @formatNum) displayTwoCol("Protec", pItem1, pItem2, b_armorValue, @byteField, @formatNum) end -/////////////////////////////////////////////////////////////////////////////////////////////////// -def displayStuffStats(pItem1, pItem2)#0 - // Nothing special -end - /////////////////////////////////////////////////////////////////////////////////////////////////// def _displayItemStats(pItem1, pItem2)#1 word pMod1, pMod2 // First, show the item type and name when pItem1->t_type - is TYPE_ITEM; rawDisplayStr("\nItem"); break - is TYPE_WEAPON; rawDisplayStr("\nWeapon"); break - is TYPE_ARMOR; rawDisplayStr("\nArmor"); break - is TYPE_STUFF; rawDisplayStr("\nSupply"); break + is TYPE_PLAIN_ITEM + is TYPE_FANCY_ITEM; rawDisplayStr("\nItem"); break + is TYPE_WEAPON; rawDisplayStr("\nWeapon"); break + is TYPE_ARMOR; rawDisplayStr("\nArmor"); break otherwise fatal("tItem") wend tabTo(STATS_COL_1); _displayItemName(pItem1) @@ -862,25 +1033,28 @@ def _displayItemStats(pItem1, pItem2)#1 when pItem1->t_type is TYPE_WEAPON; displayWeaponStats(pItem1, pItem2); break is TYPE_ARMOR; displayArmorStats(pItem1, pItem2); break - is TYPE_STUFF; displayStuffStats(pItem1, pItem2); break wend // If either item has modifiers, show them pMod1 = NULL - if pItem1->t_type <> TYPE_STUFF; pMod1 = pItem1=>p_modifiers; fin + if pItem1->t_type <> TYPE_PLAIN_ITEM; pMod1 = pItem1=>p_modifiers; fin pMod2 = NULL if pItem2 - if pItem2->t_type <> TYPE_STUFF; pMod2 = pItem2=>p_modifiers; fin + if pItem2->t_type <> TYPE_PLAIN_ITEM; pMod2 = pItem2=>p_modifiers; fin fin if pMod1 or pMod2 rawDisplayStr("\nSpecial") while pMod1 or pMod2 if pMod1 - rawDisplayf3("^T%D%d %s", STATS_COL_1, pMod1=>w_modValue, pMod1=>s_name) + rawDisplayf1("^T%D", STATS_COL_1) + if pMod1=>w_modValue >= 999; rawDisplayStr("Full "); else rawDisplayf1("%d ", pMod1=>w_modValue); fin + rawDisplayStr(pMod1=>s_name) pMod1 = pMod1=>p_nextObj fin if pMod2 - rawDisplayf3("^T%D%d %s", STATS_COL_2, pMod2=>w_modValue, pMod2=>s_name) + rawDisplayf1("^T%D", STATS_COL_2) + if pMod2=>w_modValue >= 999; rawDisplayStr("Full "); else rawDisplayf1("%d ", pMod2=>w_modValue); fin + rawDisplayStr(pMod2=>s_name) pMod2 = pMod2=>p_nextObj fin loop @@ -892,13 +1066,14 @@ end // For non-countable items, display singular name. // For countable "stuff" (e.g. ammo), display the count and appropriate singular or plural name. def _displayItemName(pItem)#1 - if pItem->t_type == TYPE_STUFF - setPlural(pItem=>w_count <> 1) + isPlural = FALSE + if pItem->t_type == TYPE_FANCY_ITEM and pItem=>w_count > 1 + isPlural = TRUE rawDisplayf1("%d ", pItem=>w_count) - else - setPlural(FALSE) fin - rawDisplayf1("%s", pItem=>s_name) // use displayf to get proper plural processing + buildString(@addToString) + printf1("%s", pItem=>s_name) // need proper plural processing + rawDisplayStr(finishString(isPlural)) return 0 end diff --git a/Platform/Apple/virtual/src/plasma/playtype.pla b/Platform/Apple/virtual/src/plasma/playtype.pla index 13b2324d..c3977d01 100644 --- a/Platform/Apple/virtual/src/plasma/playtype.pla +++ b/Platform/Apple/virtual/src/plasma/playtype.pla @@ -9,19 +9,17 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// // Garbage collection pointer offsets within each type -byte typeTbl_Global[] = Global, p_players, p_benched, p_enemyGroups, p_combatFirst, p_encounterZones, s_mapName, p_gameFlags, 0 -byte typeTbl_Player[] = Player, p_nextObj, s_name, p_combatNext, p_skills, p_items, p_effects, 0 -byte typeTbl_Modifier[] = Modifier, p_nextObj, s_name, 0 -byte typeTbl_Effect[] = Effect, p_nextObj, s_name, 0 -byte typeTbl_Item[] = Item, p_nextObj, s_name, s_itemKind, p_modifiers, 0 -byte typeTbl_Weapon[] = Weapon, p_nextObj, s_name, s_itemKind, p_modifiers, s_ammoKind, s_combatText, 0 -byte typeTbl_Armor[] = Armor, p_nextObj, s_name, s_itemKind, p_modifiers, 0 -byte typeTbl_Stuff[] = Stuff, p_nextObj, s_name, s_itemKind, 0 -byte typeTbl_Enemy[] = Enemy, p_nextObj, s_name, p_combatNext, s_attackText, 0 -byte typeTbl_EnemyGroup[] = EnemyGroup, p_nextObj, p_enemies, 0 +byte typeTbl_Global[] = Global, p_players, p_benched, p_enemyGroups, p_combatFirst, p_encounterZones, s_mapName, 0 +byte typeTbl_Player[] = Player, p_nextObj, s_name, p_combatNext, p_skills, p_items, 0 +byte typeTbl_Modifier[] = Modifier, p_nextObj, s_name, 0 +byte typeTbl_PlainItem[] = PlainItem, p_nextObj, s_name, 0 +byte typeTbl_FancyItem[] = FancyItem, p_nextObj, s_name, s_itemKind, p_modifiers, 0 +byte typeTbl_Weapon[] = Weapon, p_nextObj, s_name, s_itemKind, p_modifiers, s_ammoKind, s_combatText, 0 +byte typeTbl_Armor[] = Armor, p_nextObj, s_name, s_itemKind, p_modifiers, 0 +byte typeTbl_Enemy[] = Enemy, p_nextObj, s_name, p_combatNext, s_attackText, 0 +byte typeTbl_EnemyGroup[] = EnemyGroup, p_nextObj, p_enemies, 0 byte typeTbl_EncounterZone[] = EncounterZone, p_nextObj, s_name, 0 -word typeTbls = @typeTbl_Global, @typeTbl_Player, @typeTbl_Modifier, @typeTbl_Effect, @typeTbl_Item -word = @typeTbl_Weapon, @typeTbl_Armor, @typeTbl_Stuff, @typeTbl_Enemy, @typeTbl_EnemyGroup -word = @typeTbl_EncounterZone +word typeTbls = @typeTbl_Global, @typeTbl_Player, @typeTbl_Modifier, @typeTbl_PlainItem, @typeTbl_FancyItem +word = @typeTbl_Weapon, @typeTbl_Armor, @typeTbl_Enemy, @typeTbl_EnemyGroup, @typeTbl_EncounterZone word = 0 diff --git a/Platform/Apple/virtual/src/plasma/playtype.plh b/Platform/Apple/virtual/src/plasma/playtype.plh index c61257d7..c0048d00 100644 --- a/Platform/Apple/virtual/src/plasma/playtype.plh +++ b/Platform/Apple/virtual/src/plasma/playtype.plh @@ -36,8 +36,7 @@ struc Global word w_heapSize word w_typeHash - // General flags maintained by scripts. Linked list of Modifiers. - word p_gameFlags + byte[32] ba_gameFlags // General flags maintained by scripts (array of 256 bits = 32 bytes) byte b_curAvatar // god mode flag @@ -86,7 +85,6 @@ struc Player // Lists word p_skills // list:Modifier word p_items // list:Item - word p_effects // list:Effect end // Combat skills, weapon modifiers, etc. @@ -98,27 +96,29 @@ struc Modifier word w_modValue end -// Buffs and debuffs, that last until a specified time -const TYPE_EFFECT = $83 -struc Effect - byte t_type - word p_nextObj - byte s_name - word w_modValue - word w_endTurn -end - -const TYPE_ITEM = $84 -struc Item +// Plain items are for things that don't stack or have stat effects, +// but may have meaning to scenario logic (e.g. keys, tokens, etc.) +const TYPE_PLAIN_ITEM = $83 +struc PlainItem byte t_type word p_nextObj word s_name - word s_itemKind word w_price - word p_modifiers // list:modifier - // Usables properties - byte b_maxUses - byte b_curUses +end + +const TYPE_FANCY_ITEM = $84 +struc FancyItem + // Plain item properties + byte t_type + word p_nextObj + word s_name + word w_price + // Fancy properties + word s_itemKind // for ammo + word p_modifiers // list:modifier, e.g. boost health, etc. + word w_count // zero for singular items, 1+ for countables + word w_storeAmount + word r_lootAmount end const ITEM_FLAG_EQUIP = $80 // only one weapon/armor equipped (in use) at a time @@ -127,19 +127,16 @@ const WEAPON_FLAG_SINGLE_USE = $01 const TYPE_WEAPON = $85 struc Weapon - // Item properties + // Plain item properties byte t_type word p_nextObj word s_name - word s_itemKind word w_price - word p_modifiers // list:modifier - // Usables properties - byte b_maxUses - byte b_curUses // Weapon properties - byte b_flags // WEAPON_FLAG_* above - word s_ammoKind + word s_itemKind // for skill matching + word p_modifiers // list:modifier + byte b_flags // WEAPON_FLAG_* above + word s_ammoKind // for matching to Stackable ammo byte b_clipSize byte b_clipCurrent word r_meleeDmg // 3 hex digits: num dice, die size, add. E.g. $361 = 3d6+1 @@ -151,37 +148,19 @@ end const TYPE_ARMOR = $86 struc Armor - // General item properties + // Plain item properties byte t_type word p_nextObj word s_name - word s_itemKind word w_price - word p_modifiers // list:modifier - // Usables properties - byte b_maxUses - byte b_curUses // Armor properties + word s_itemKind + word p_modifiers // list:modifier byte b_flags // ARMOR_FLAG_* above byte b_armorValue end -// Countable things, e.g. ammo and pelts -const TYPE_STUFF = $87 -struc Stuff - // General item properties - byte t_type - word p_nextObj - word s_name - word s_itemKind - word w_price - // Stuff properties - word w_count - word w_storeAmount - word r_lootAmount -end - -const TYPE_ENEMY = $88 +const TYPE_ENEMY = $87 struc Enemy byte t_type word p_nextObj @@ -204,7 +183,7 @@ struc Enemy word r_goldLoot // monetary loot when killed, as 3 hex digits for dice end -const TYPE_ENEMY_GROUP = $89 +const TYPE_ENEMY_GROUP = $88 struc EnemyGroup byte t_type word p_nextObj @@ -212,7 +191,7 @@ struc EnemyGroup byte b_enemyGroupRange end -const TYPE_ENCOUNTER_ZONE = $8A +const TYPE_ENCOUNTER_ZONE = $89 struc EncounterZone byte t_type word p_nextObj diff --git a/Platform/Apple/virtual/src/plasma/store.pla b/Platform/Apple/virtual/src/plasma/store.pla index bb1178b1..14552bfd 100644 --- a/Platform/Apple/virtual/src/plasma/store.pla +++ b/Platform/Apple/virtual/src/plasma/store.pla @@ -31,8 +31,8 @@ word pItemsModule, pPartyModule const MAX_PAGE_ITEMS = 20 // should be plenty word pageItems[MAX_PAGE_ITEMS] word pagePrices[MAX_PAGE_ITEMS] -word pagePlayers[MAX_PAGE_ITEMS] -word pMatchPlayer +byte playerNum, playerCount +word pPlayer /////////////////////////////////////////////////////////////////////////////////////////////////// // Definitions used by assembly code @@ -61,14 +61,14 @@ def unloadExtraModules()#0 end /////////////////////////////////////////////////////////////////////////////////////////////////// -def displayBuyTitle(pageNum, nPages)#0 +def displayTitle(titleAction, columnAction, pageNum, nPages)#0 clearWindow() - // Can't use centering mode on oversize window - font engine can't handle width > 255 - rawDisplayStr("^Y^I Buying") + rawDisplayf2("^I %s %s ", pPlayer=>s_name, titleAction) if (nPages > 1) - rawDisplayf2(" - p. %d/%d", pageNum+1, nPages) + rawDisplayf2("p.%d/%d ", pageNum+1, nPages) fin - rawDisplayStr(" ^N\n^V014^LBrowse^T046Price^T085Item^L") + rightJustifyStr(sprintf1(" %d party gold ", global=>w_gold), BIGWIN_RIGHT - 15) + rawDisplayf1("^N\n^V014^L%s^T046Price^T085Item^L", columnAction) end /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -77,19 +77,45 @@ def displayItemLine(num)#0 pPartyModule()=>party_displayItemName(pageItems[num]) end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def displayPaging(action, nItems, pageNum, nPages)#0 + rawDisplayStr("\n^V164") + if nItems or playerCount > 1 + if nItems + rawDisplayf1("%s [A", action) + if nItems > 1; rawDisplayf1("-%c", nItems-1+'A'); fin + rawDisplayStr("], ") + fin + if nPages > 1 + rawDisplayStr("Pg [") + if pageNum+1 < nPages; rawDisplayStr(">"); fin + if pageNum; rawDisplayStr("<"); fin + rawDisplayStr("], ") + fin + if playerCount > 1 + rawDisplayf1("Plyr [1-%d], ", playerCount) + fin + rawDisplayStr("or [Esc].") + else + rawDisplayStr("Press [Esc].") + fin +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def displayBuyPage(pItemTbl, markupRatio, pageNum, nPages)#1 byte itemNum word pFunc, pItem - displayBuyTitle(pageNum, nPages) + // Clear stuff from previous page + mmgr(CHECK_MEM, 0) mmgr(HEAP_COLLECT, 0) + displayTitle("buying", "Browse", pageNum, nPages) pFunc = pItemTbl + ((pageNum*PAGE_SIZE) << 1) for itemNum = 0 to PAGE_SIZE-1 if !(*pFunc); break; fin pItem = (*pFunc)() - if pItem->t_type == TYPE_STUFF + if pItem->t_type == TYPE_FANCY_ITEM pItem=>w_count = pItem=>w_storeAmount fin pageItems[itemNum] = pItem @@ -97,14 +123,7 @@ def displayBuyPage(pItemTbl, markupRatio, pageNum, nPages)#1 displayItemLine(itemNum) pFunc = pFunc + 2 next - - rawDisplayf1("\n^V166Gold: %d. Browse [A", global=>w_gold) - if itemNum > 1; rawDisplayf1("-%c", itemNum-1+'A'); fin - rawDisplayStr("], ") - if nPages > 1 - rawDisplayf1("p. [1-%d], ", nPages) - fin - rawDisplayStr("or [Esc].") + displayPaging("Browse", itemNum, pageNum, nPages) return itemNum end @@ -113,7 +132,7 @@ def displayItemBrowse(pItem1, price, pItem2)#0 clearWindow() rawDisplayf1("^T108^I Browse ^N\n\n^T%D^LMerchandise^L", STATS_COL_1) if pItem2 - rawDisplayf2("^T%D^L%s^L", STATS_COL_2, pMatchPlayer=>s_name) + rawDisplayf2("^T%D^L%s^L", STATS_COL_2, pPlayer=>s_name) fin pPartyModule()=>party_displayItemStats(pItem1, pItem2) rawDisplayf2("\n\nPrice^T%D%d", STATS_COL_1, price) @@ -122,26 +141,20 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def matchEquipped(pMatch, nSkip)#1 - word pPlayer, pItem - pPlayer = global=>p_players - pMatchPlayer = pPlayer - while pPlayer - pItem = pPlayer=>p_items - while pItem - if (pItem->t_type == pMatch->t_type) and (pItem->t_type == TYPE_WEAPON or pItem->t_type == TYPE_ARMOR) - if (pItem->b_flags & ITEM_FLAG_EQUIP) - if pItem->t_type <> TYPE_ARMOR or (pItem=>s_itemKind == pMatch=>s_itemKind) - if nSkip == 0 - pMatchPlayer = pPlayer - return pItem - fin - nSkip = nSkip - 1 + word pItem + pItem = pPlayer=>p_items + while pItem + if (pItem->t_type == pMatch->t_type) and (pItem->t_type == TYPE_WEAPON or pItem->t_type == TYPE_ARMOR) + if (pItem->b_flags & ITEM_FLAG_EQUIP) + if pItem->t_type <> TYPE_ARMOR or (pItem=>s_itemKind == pMatch=>s_itemKind) + if nSkip == 0 + return pItem fin + nSkip = nSkip - 1 fin fin - pItem = pItem=>p_nextObj - loop - pPlayer = pPlayer=>p_nextObj + fin + pItem = pItem=>p_nextObj loop return NULL end @@ -169,6 +182,12 @@ def askQuantity(nMax)#1 return num end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def displayMsg(msg, beep)#0 + rawDisplayf1("^T000^C%s", msg) + pause(800) +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def browseItem(num)#0 word pItem, price, compSkip, pComp, quantity @@ -180,24 +199,22 @@ def browseItem(num)#0 displayItemBrowse(pItem, price, matchEquipped(pItem, compSkip)) displayItemMenu(price, compSkip or matchEquipped(pItem, 1+compSkip)) sel = getUpperKey() - rawDisplayf1(" %c\n", sel) if sel == 'B' and price <= global=>w_gold - matchEquipped(pItem, compSkip) // to set pMatchPlayer - pComp = scanForNamedObj(pMatchPlayer=>p_items, pItem=>s_name) + pComp = scanForNamedObj(pPlayer=>p_items, pItem=>s_name) if pComp - if pItem->t_type == TYPE_STUFF + if pItem->t_type == TYPE_FANCY_ITEM and pItem=>w_count > 0 pComp=>w_count = min(30000, pComp=>w_count + pItem=>w_count) else - rawDisplayStr("\nDuplicate item.") + rawDisplayStr("^T000^CDuplicate item.") beep() - pause(1000) + pause(400) continue fin else - addToList(@pMatchPlayer=>p_items, pItem) + addToList(@pPlayer=>p_items, pItem) fin global=>w_gold = global=>w_gold - price - rawDisplayStr("\nPurchase complete.") + rawDisplayStr("^T000^CDone.") pause(800) break elsif sel == 'N' and (compSkip or matchEquipped(pItem, 1+compSkip)) @@ -211,16 +228,24 @@ def browseItem(num)#0 loop end +/////////////////////////////////////////////////////////////////////////////////////////////////// +def storeSetup()#0 + loadExtraModules() + setBigWindow() + + playerCount = countList(global=>p_players) + playerNum = 0 + pPlayer = numToPlayer(playerNum) +end + /////////////////////////////////////////////////////////////////////////////////////////////////// def _buyFromStore(storeCode, profitPercent)#1 word pItemTbl, choice, ratio byte nItemsOnPage, pageNum, nPages, redisplay - loadExtraModules() + storeSetup() pItemTbl = pItemsModule()=>items_forStoreCode(storeCode) - setBigWindow() - nPages = (countArray(pItemTbl) + PAGE_SIZE - 1) / PAGE_SIZE pageNum = 0 @@ -233,8 +258,13 @@ def _buyFromStore(storeCode, profitPercent)#1 fin choice = getUpperKey() redisplay = TRUE - if choice >= '1' and (choice-'1') < nPages - pageNum = choice - '1' + if choice == '<' and pageNum + pageNum-- + elsif choice == '>' and pageNum+1 < nPages + pageNum++ + elsif choice >= '1' and (choice-'1') <= playerCount and (choice-'1') <> playerNum + playerNum = choice - '1' + pPlayer = numToPlayer(playerNum) elsif choice >= 'A' and (choice-'A' < nItemsOnPage) browseItem(choice-'A') elsif choice == $1B // Esc @@ -246,53 +276,38 @@ def _buyFromStore(storeCode, profitPercent)#1 loop unloadExtraModules() + mmgr(CHECK_MEM, 0) return mmgr(HEAP_COLLECT, 0) end -/////////////////////////////////////////////////////////////////////////////////////////////////// -def displaySellTitle(pageNum, nPages)#0 - clearWindow() - // Can't use centering mode on oversize window - font engine can't handle width > 255 - rawDisplayStr("^Y^I Selling") - if (nPages > 1) - rawDisplayf2(" - p. %d/%d", pageNum+1, nPages) - fin - rawDisplayStr(" ^N\n^V014^LSell^T046Amt.^T085Item^L") -end - /////////////////////////////////////////////////////////////////////////////////////////////////// def iterateSellables(skipItems, markdownRatio)#1 - word pPlayer, pItem, itemsOnPage, totalItems, price + word pItem, itemsOnPage, totalItems, price byte ok itemsOnPage = 0 totalItems = 0 - pPlayer = global=>p_players - while pPlayer - pItem = pPlayer=>p_items - while pItem - ok = pItem=>w_price > 0 - if pItem->t_type == TYPE_STUFF - ok = FALSE // too much trouble to figure out stuff prices - elsif pItem->t_type == TYPE_WEAPON or pItem->t_type == TYPE_ARMOR - if pItem->b_flags & ITEM_FLAG_EQUIP; ok = FALSE; fin + pItem = pPlayer=>p_items + while pItem + ok = pItem=>w_price > 0 + if pItem->t_type == TYPE_FANCY_ITEM + ok = pItem=>w_count > 0 // too much trouble to figure out prices of stackables + elsif pItem->t_type == TYPE_WEAPON or pItem->t_type == TYPE_ARMOR + if pItem->b_flags & ITEM_FLAG_EQUIP; ok = FALSE; fin // don't sell equipped things + fin + if ok + price = max(0, pItem=>w_price - addRatio(pItem=>w_price, markdownRatio)) + if !price; ok = FALSE; fin + fin + if ok + if totalItems >= skipItems and itemsOnPage < PAGE_SIZE + pageItems[itemsOnPage] = pItem + pagePrices[itemsOnPage] = price + displayItemLine(itemsOnPage) + itemsOnPage++ fin - if ok - price = max(0, pItem=>w_price - addRatio(pItem=>w_price, markdownRatio)) - if !price; ok = FALSE; fin - fin - if ok - if totalItems >= skipItems and itemsOnPage < PAGE_SIZE - pageItems[itemsOnPage] = pItem - pagePrices[itemsOnPage] = price - pagePlayers[itemsOnPage] = pPlayer - displayItemLine(itemsOnPage) - itemsOnPage++ - fin - totalItems++ - fin - pItem = pItem=>p_nextObj - loop - pPlayer = pPlayer=>p_nextObj + totalItems++ + fin + pItem = pItem=>p_nextObj loop if skipItems == 9999; return totalItems; fin return itemsOnPage @@ -301,18 +316,12 @@ end /////////////////////////////////////////////////////////////////////////////////////////////////// def displaySellPage(markdownRatio, pageNum, nPages)#1 word nItems - displaySellTitle(pageNum, nPages) + displayTitle("selling", "Sell", pageNum, nPages) nItems = iterateSellables(pageNum * PAGE_SIZE, markdownRatio) if !nItems rawDisplayStr("\n\nNothing to sell here.") fin - rawDisplayf1("\n^V166Gold: %d. Sell [A", global=>w_gold) - if nItems > 1; rawDisplayf1("-%c", nItems-1+'A'); fin - rawDisplayStr("], ") - if nPages > 1 - rawDisplayf1("p. [1-%d], ", nPages) - fin - rawDisplayStr("or [Esc].") + displayPaging("Sell", nItems, pageNum, nPages) return nItems end @@ -323,12 +332,10 @@ def sellItem(num)#0 pItem = pageItems[num] price = pagePrices[num] - clearWindow() - global=>w_gold = global=>w_gold + price - removeFromList(@pagePlayers=>p_items, pItem) + removeFromList(@pPlayer=>p_items, pItem) - rawDisplayStr("\nSale complete.") + rawDisplayStr("^T000^CDone.") pause(800) end @@ -337,27 +344,34 @@ def _sellToStore(profitPercent)#1 word pItemTbl, choice, ratio byte nItemsOnPage, pageNum, totalItems, nPages, redisplay - loadExtraModules() - setBigWindow() - pageNum = 0 + storeSetup() ratio = percentToRatio(profitPercent) / 2 // half profit on buying, half on selling + totalItems = iterateSellables(9999, 0) // initialize count for paging calcs redisplay = TRUE - while totalItems > 0 + while TRUE nPages = (totalItems + PAGE_SIZE - 1) / PAGE_SIZE // recalc each time since totalItems changes - pageNum = min(nPages-1, pageNum) if redisplay nItemsOnPage = displaySellPage(ratio, pageNum, nPages) fin choice = getUpperKey() redisplay = TRUE - if choice >= '1' and (choice-'1') < nPages - pageNum = choice - '1' + if choice == '<' and pageNum + pageNum-- + elsif choice == '>' and pageNum+1 < nPages + pageNum++ + elsif choice >= '1' and (choice-'1') <= playerCount + playerNum = choice - '1' + pPlayer = numToPlayer(playerNum) + totalItems = iterateSellables(9999, 0) elsif choice >= 'A' and (choice-'A' < nItemsOnPage) sellItem(choice-'A') totalItems = iterateSellables(9999, 0) + if totalItems <= pageNum * PAGE_SIZE // sold last item on pg 2 + pageNum-- + fin elsif choice == $1B // Esc break else @@ -367,6 +381,7 @@ def _sellToStore(profitPercent)#1 loop unloadExtraModules() + mmgr(CHECK_MEM, 0) return mmgr(HEAP_COLLECT, 0) end diff --git a/Platform/Apple/virtual/src/raycast/render.s b/Platform/Apple/virtual/src/raycast/render.s index 9ab0ebd3..a822dcee 100644 --- a/Platform/Apple/virtual/src/raycast/render.s +++ b/Platform/Apple/virtual/src/raycast/render.s @@ -1763,6 +1763,8 @@ pl_advance: !zone pha adc walkDirs+1,x sta playerX+1 + jsr .chk + sta .ora+1 lda playerY pha @@ -1773,27 +1775,15 @@ pl_advance: !zone pha adc walkDirs+3,x sta playerY+1 - - ; Check if the new position is blocked - jsr calcMapOrigin - ldy playerX+1 - lda (pMap),y - and #$1F - beq .ok ; empty tiles are never blocked - tax - jsr getTileFlags - sta tmp+1 - and #2 ; tile flag 2 is for obstructions + jsr .chk +.ora ora #11 ; self-modified above beq .ok ; Blocked! Restore old position. - pla - sta playerY+1 - pla - sta playerY - pla - sta playerX+1 - pla - sta playerX + ldx #3 +- pla + sta playerX,x + dex + bpl - ldy #0 beq .done .ok ; Not blocked. See if we're in a new map tile. @@ -1820,6 +1810,20 @@ pl_advance: !zone .done tya ; retrieve ret value ldy #0 ; hi byte of ret is always 0 rts ; all done + ; Check if the new position is blocked +.chk stx .rstx+1 + jsr calcMapOrigin + ldy playerX+1 + lda (pMap),y + and #$1F + beq .rstx ; empty tiles are never blocked + tax + jsr getTileFlags + sta tmp+1 + and #2 ; tile flag 2 is for obstructions +.rstx ldx #11 ; self-modified above + cmp #0 + rts ;------------------------------------------------------------------------------- ; Swap tiles at two positions.