lawless-legends/Platform/Apple/virtual/src/plasma/gameloop.pla

3744 lines
106 KiB
Plaintext

///////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015 The 8-Bit Bunch. Licensed under the Apache License, Version 1.1
// (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-1.1>.
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
// Fixed memory locations
const seed = $4E // Incremented continuously by keyboard read routine
const displayEngine = $6000 // main mem (raycaster and tile engine at same location)
///////////////////////////////////////////////////////////////////////////////////////////////////
// Other constants
const CHAR_WND_HEALTH_X = 112
const ANIM_PAUSE_MAX = 150
const CLOCK_ADV_2D_HOURS = 1
const CLOCK_ADV_2D_MINS = 0
const CLOCK_ADV_2D_SECS = 0
const CLOCK_ADV_3D_HOURS = 0
const CLOCK_ADV_3D_MINS = 0
const CLOCK_ADV_3D_SECS = 30
const CLOCK_ADV_COMBAT_HOURS = 0
const CLOCK_ADV_COMBAT_MINS = 10
const CLOCK_ADV_COMBAT_SECS = 0
// Raycaster tables - must match values in render.i
const RAY_TABLE_START = $A800
const RAY_TABLE_SIZE = $1781
// Max gold
const GOLD_MAX = 20000
include "globalDefs.plh"
include "playtype.plh"
include "gen_images.plh"
include "gen_modules.plh"
include "gen_enemies.plh"
include "gen_players.plh"
include "gen_items.plh"
include "combat.plh"
include "party.plh"
include "store.plh"
include "diskops.plh"
include "godmode.plh"
include "automap.plh"
include "questlog.plh"
include "story.plh"
include "util3d.plh"
///////////////////////////////////////////////////////////////////////////////////////////////////
// Data structures
include "playtype.pla"
export word global // the global heap object, from which all live objects must be reachable
///////////////////////////////////////////////////////////////////////////////////////////////////
// Predefined functions, for circular calls or out-of-order calls
predef setWindow2()#0
predef initCmds()#0
predef nextAnimFrame()#0
predef checkEncounter(x, y, force)#0
predef doCombat(mapCode, backUpOnFlee)#1
predef clearPortrait()#0
predef showMapName(mapName)#0
predef doRender()#0
predef pause(count)#1
predef printf1(str, arg1)#0
predef printf2(str, arg1, arg2)#0
predef playerDeath()#0
predef saveGame()#1
predef setStat(player, statName, val)#0
predef startGame(firstTime, ask)#0
predef showAnimFrame()#0
predef showParty()#0
predef textureControl(flg)#0
predef clearEncounterZones()#0
predef adjustPartyStat(statName, val)#0
predef printMem()#1
///////////////////////////////////////////////////////////////////////////////////////////////////
// Global variables
export byte mapNum = -1
export byte mapIs3D = 0
export word totalMapWidth
export word totalMapHeight
export word pCurMap
word mapNameHash = 0
export byte needRender = FALSE
byte needShowParty = FALSE
byte renderLoaded = FALSE
byte texturesLoaded = FALSE
byte textDrawn = FALSE
byte anyInteraction = FALSE
byte textClearCountdown = 0
byte forceRawScrDisp = FALSE
export byte isPlural = 0
byte inScript = FALSE
export byte isFloppyVer
export byte isJace
byte scriptModule = 0
byte prevScriptModule = 0
export word skyNum = 9
export word groundNum = 10
export byte portraitNum = 0
word triggerOriginX, triggerOriginY
word triggerTbl
byte cmdKey // last command key pressed
word cmdTbl[96] // ASCII $00..$5F
byte frameLoaded = 0
word curEngine = NULL
word pModUtil3d = NULL
word util3d = NULL
export word pResourceIndex = NULL
export word pGlobalTileset = NULL
export byte curMapPartition = 0
export word typeHash = 0
export byte curHeapPct = 0
byte lastMoveDir = $FF
// Queue setMap / teleport, since otherwise script might be replaced while executing
byte q_mapIs3D = 0
byte q_mapNum = 1
word q_x = 0
word q_y = 0
byte q_dir = 0
// Queue of player death
byte q_playerDeath = FALSE
// Script tracking
const MAX_MAP_SCRIPTS = 4
byte nMapScripts = 0
word mapScripts[MAX_MAP_SCRIPTS]
// For decimal conversion and display tabbing
byte decimalBuf[7]
byte fontPosBuf[4]
byte tabBuf[5]
// Animation and lamp tracking
word curPortrait = NULL
byte curPortraitNum = 0
byte preEnginePortraitNum = 0
word curFullscreenImg = NULL
byte animDirCt
byte anyAnims = TRUE
word animPauseCt
byte showingLamp = TRUE
export byte lampFrame = 0
export word lampDir = 1
byte storyMode = FALSE
// Time and clock
export byte prevClockColor, prevClockHour, prevClockMinute
export byte nextSignificantMinute
byte snoozeX0, snoozeX1, snoozeY
word timeEventFunc
byte mapEmuSound = 0
export byte[] S_GAME1_FILENAME = "GAME.1.SAVE"
// Context for lambda functions (in lieu of closures, for now at least)
export word ctx
// Shared string constants
export byte[] S_INTELLIGENCE = "Intelligence"
export byte[] S_STRENGTH = "Strength"
export byte[] S_AGILITY = "Agility"
export byte[] S_STAMINA = "Stamina"
export byte[] S_CHARISMA = "Charisma"
export byte[] S_SPIRIT = "Spirit"
export byte[] S_LUCK = "Luck"
export byte[] S_HEALTH = "Health"
export byte[] S_MAX_HEALTH = "Max health"
export byte[] S_AIMING = "Aiming"
export byte[] S_HAND_TO_HAND = "Hand-to-hand"
export byte[] S_DODGING = "Dodging"
export byte[] S_GOLD = "Gold"
export byte[] S_TIME = "Time"
export byte[] S_XP = "XP"
export byte[] S_SP = "SP"
export byte[] S_BANK_BAL = "Bank bal"
export byte[] S_PACK_SIZE = "Pack size"
export byte[] S_ENTER = "Enter"
export byte[] S_LEAVE = "Leave"
export byte[] S_USE = "Use"
export byte[] S_HIS = "his"
export byte[] S_HER = "her"
export byte[] S_THEIR = "their"
word lastTick = 0
export byte recordMode = 0
export word recordSeed
export byte diskLimit = 0
export word playerUsing = NULL
///////////////////////////////////////////////////////////////////////////////////////////////////
// Definitions used by assembly code
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"
!source "../../include/fontEngine.i"
!source "../../include/sound.i"
!source "../../include/prorwts.i"
; Optional debug printing support
DEBUG = 0
; General use
tmp = $2
pTmp = $4
ysav = $34
ysav1 = $35
; 16-bit random number seed - incremented by ROM kbd routine
seed = $4E
MAGIC = $7FED ; largest prime < $8000
; NOTE ABOUT ABSOLUTE CODE ADDRESSING (e.g. STA .var, JMP .label, etc.)
; We cannot use it: this code will be preceded by stubs for the PLASMA routines, hence
; absolute addressing must be done carefully, adding ABS_OFFSET below.
;
; So don't JMP to labels, declare any variables as !byte or !word here, etc. without
; accounting for that.
;
; The way to account for that is to use ABS_OFFSET, and only within this module. This
; method works because this module is the only one loaded at an absolute address.
;
; See examples below.
ABS_OFFSET = (_DEFCNT*5) - 13 ; 13 for plasma's module header (stripped by packer)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// After scriptDisplayStr is called, we clear the PLASMA string pool. That way, many long strings
// can be used in a single function.
export asm scriptDisplayStr(str)#0
lda .callScriptDisplay + ABS_OFFSET + 2 ; first time init?
beq +
.callScriptDisplay
jsr 0 ; self-modified below
lda framePtr
sta outerFramePtr
lda framePtr+1
sta outerFramePtr+1
rts
+ ; first-time init
lda evalStkL,x
sta .callScriptDisplay + ABS_OFFSET + 1
lda evalStkH,x
sta .callScriptDisplay + ABS_OFFSET + 2
inx
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// API to call rendering engine (same API for raycaster and tile engine)
asm initDisplay(mapPartition, mapNum, pMapData, x, y, dir)#0
+asmPlasmNoRet 6
jmp $6000
end
export asm flipToPage1()#0
+asmPlasmNoRet 0
jmp $6003
end
export asm getPos(px, py)#0
+asmPlasmNoRet 2
jmp $6006
end
asm setPos(x, y)#0
+asmPlasmNoRet 2
jmp $6009
end
export asm getDir()#1 // returns: dir (0-15)
+asmPlasmRet 0
jmp $600C
end
export asm setDir(dir)#0
+asmPlasmNoRet 1
jmp $600F
end
asm advance(nSteps)#1 // returns: 0 if same pos, 1 if new pos, 2 if new pos and scripted
+asmPlasmRet 1
jmp $6012
end
asm setColor(slot, color)#0 // params: slot (0=sky/1=ground), color (0-17)
+asmPlasmNoRet 2
jmp $6015
end
asm render(intrOnKbd)#0
+asmPlasmNoRet 1
jmp $6018
end
asm _texControl(doLoad)#0
+asmPlasmNoRet 1
jmp $601B
end
asm getMapScript()#1
+asmPlasmRet 0
jmp $601E
end
asm setAvatar(tileNum)#0 // tile number (in the global tileset)
+asmPlasmNoRet 1
jmp $6021
end
asm copyTile(fromX, fromY, toX, toY)#0
+asmPlasmNoRet 4
jmp $6024
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Memory copy - Non-overlapping regions only!
export asm memcpy(pSrc, pDst, len, auxWr)#0
+asmPlasmNoRet 4
lda evalStkL+3,x ; source ptr
sta tmp
lda evalStkH+3,x
sta tmp+1
lda evalStkL+2,x ; dest ptr
sta pTmp
lda evalStkH+2,x
sta pTmp+1
ldy evalStkL,x ; auxWr
sei ; prevent interrupts while possibly in aux
sta clrAuxWr,y
lda evalStkH+1,x ; len hi
pha
lda evalStkL+1,x ; len lo
tax
ldy #0
.pglup:
pla
sec
sbc #1
bcc .part
pha
- lda (tmp),y
sta (pTmp),y
iny
bne -
inc tmp+1
inc pTmp+1
bne .pglup ; always taken
.part:
txa
beq .done
- lda (tmp),y
sta (pTmp),y
iny
dex
bne -
.done
sta clrAuxWr
cli ; inerrupts ok now that we're back in main
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export asm memset(pDst, val, len)#0
+asmPlasmNoRet 3
ldy #0
lda evalStkL+2,x ; dest ptr
sta pTmp
lda evalStkH+2,x
sta pTmp+1
lda evalStkL+1,x ; value
sta tmp
lda evalStkL,x ; len lo
pha
lda evalStkH,x ; len hi
beq +
tax
lda tmp
- sta (pTmp),y
iny
bne -
inc pTmp+1
dex
bne -
+ pla
beq +
tax
lda tmp
- sta (pTmp),y
iny
dex
bne -
+ rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export asm readAuxByte(ptr)#1
+asmPlasmRet 1
sta pTmp
sty pTmp+1
ldy #12
- lda .rdauxb-1+ABS_OFFSET, y
sta $10-1,y
dey
bne -
sei ; prevent interrupts while in aux mem
sta setAuxRd
jmp $10
.rdauxb
lda (pTmp),y
sta clrAuxRd
cli
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// String building for display with the font engine. Includes plurality processing to handily
// handle things like "Dirt bag(s)" and "his/their"
export asm buildString()#0
+asmPlasmNoRet 0
lda cswl
sta prevCSWL+ABS_OFFSET
lda cswh
sta prevCSWL+1+ABS_OFFSET
lda #<addToString+ABS_OFFSET
sta cswl
lda #>addToString+ABS_OFFSET
sta cswh
lda #0
sta inbuf
rts
addToString:
sty ysav1
inc inbuf
ldy inbuf
and #$7F
sta inbuf,y
ldy ysav1
rts
prevCSWL !word 0
end
// Complete string building (including plural processing), and return pointer
// to the string (in the input buffer)
export asm finishString(isPlural)#1
!zone {
+asmPlasmRet 1
ldy prevCSWL+ABS_OFFSET ; put the cout vector back to default
sty cswl
ldy prevCSWL+1+ABS_OFFSET ; put the cout vector back to default
sty cswh
ldy #0 ; dest offset in Y (will be incremented before store)
cpy inbuf
beq .done1 ; failsafe: handle zero-length string
tax ; test for isPlural == 0
beq +
lda #$40 ; for setting V later
+ sta tmp ; save isPlural flag
clv ; V flag for prev-is-alpha
ldx #0 ; source offset in X (will be incremented before load)
.fetch
inx
lda inbuf,x ; get next input char
iny
and #$7F ; clear hi-bit for final string, in case it gets interned
sta inbuf,y ; by default copy the char to output
ora #$80
cmp #"(" ; plural processing triggered by parentheses
bne .notpar
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
bit tmp ; set copy flag (V) initially to same as isPlural flag
.findsl ; see if there's a slash within the parens
inx
cpx inbuf
bcs .done ; failsafe: handle missing end-paren
lda inbuf,x
ora #$80 ; normalize hi-bit for comparisons below
cmp #"/"
bne +
php
pla
eor #$40 ; flip V flag, meaning singular text is before slash, plural after.
pha
plp
+ cmp #")" ; scan until ending paren
bne .findsl ; loop to scan next char
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
ora #$80
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
and #$7F ; clear hi-bit for final string, in case it gets interned
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
cpx inbuf ; compare src offset to length
bcc .fetch ; loop while less than
.done
sty inbuf ; save new length
.done1
lda #<inbuf ; return pointer to string
ldy #>inbuf
rts
}
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export asm blit(isAux, srcData, dstScreenPtr, nLines, lineSize)#0
+asmPlasmNoRet 5
; Save line size
sta ysav
; Save nLines
lda evalStkL+1,x
pha
; Save the dest pointer
lda evalStkL+2,x
sta pTmp
lda evalStkH+2,x
sta pTmp+1
; Save the source pointer
lda evalStkL+3,x
sta tmp
lda evalStkH+3,x
sta tmp+1
; Save aux/main flag
lda evalStkL+4,x
lsr ; to carry bit
bcc +
ldy #15 ; put aux copy routine in zero page
- ldx .cpaux + ABS_OFFSET,y
stx $10,y
dey
bpl -
+ pla ; get line count
tax
--
ldy ysav ; get byte count
dey
bcc +
jsr $10 ; copy pixel bytes (aux version)
bcs ++
+
- lda (tmp),y
sta (pTmp),y
dey
bpl -
++
php
lda tmp ; advance to next row of data
clc
adc ysav
sta tmp
bcc +
inc tmp+1
+ jsr NextScreenLine ; and next screen line
plp
dex
bne -- ; Loop until we've done all rows.
rts
.cpaux
sei ; avoid interrupts while reading aux
sta setAuxRd
- lda (tmp),y
sta (pTmp),y
dey
bpl -
sta clrAuxRd
cli
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
asm vline(dstScreenPtr, val, nLines)#0
+asmPlasmNoRet 3
; Save number of lines
pha
; Save value
lda evalStkL+1,x
sta tmp
; Save the dest pointer
lda evalStkL+2,x
sta pTmp
lda evalStkH+2,x
sta pTmp+1
pla ; line count
tax
- ldy #0
lda tmp
sta (pTmp),y
jsr NextScreenLine
dex
bne -
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Simply retrieve the X register. Used to double-check that we're not leaking PLASMA eval
// stack entries.
asm getXReg()#1
+asmPlasmRet 0
txa
ldy #0
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate 16-bit hash of a buffer. Note: ignores anything beyond 256 bytes.
export asm hashBuffer(ptr, len)#1
+asmPlasmRet 2
lda evalStkL+1,x ; first arg is buffer pointer
sta pTmp
lda evalStkH+1,x
sta pTmp+1
lda evalStkL,x ; second arg is length
tax
ldy #0
sty tmp
tya
- clc
adc (pTmp),y
bcc +
inc tmp
+ lsr
ror tmp
bcc +
ora #$80
+ lsr
ror tmp
bcc +
ora #$80
+ iny
dex
bne -
ldy tmp
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a string to the current character output vector
export asm puts(str)#0
+asmPlasmNoRet 1
sta pTmp
lda #'!'
ldx #1
tya
beq + ; safety: print '!' instead of null string
sty pTmp+1
ldy #0
lda (pTmp),y
tax
beq ++ ; handle empty string
iny
- lda (pTmp),y
+ ora #$80
+safeCout
iny
dex
bne -
++ rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a character from the keyboard
export asm rdkey()#1
+asmPlasmRet 0
+safeRdkey
ldy #0
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Print part of a string, until we hit the end or a '%' code. Return how far we got, or -1 for end.
asm partialPrintf(str, pos)#1
!zone {
+asmPlasmRet 2
lda evalStkL+1,x ; get string pointer
sta pTmp
lda evalStkH+1,x
sta pTmp+1
ldy #0
lda (pTmp),y ; get length byte
sec
sbc evalStkL,x ; minus offset
sta tmp ; to count of characters left to print
bcc .eos ; avoid overrunning
beq .eos
lda evalStkL,x ; get desired offset into string
tay
iny ; increment past length byte
- lda (pTmp),y
ora #$80
cmp #'%' ; stop if we hit % code
beq +
+safeCout
iny
dec tmp ; otherwise go until end of string
bne -
.eos
ldy #$FF ; if we hit end of string, return -1
tya
rts
+ dey ; adjust back for length byte
tya ; that's the lo byte of return
ldy #0 ; hi byte of return is zero
rts
}
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a 16-bit hex value
export asm printHex(num)#0
+asmPlasmNoRet 1
pha
tya
+safePrbyte
pla
jmp _safePrbyte
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a single character
export asm printChar(ch)#0
+asmPlasmNoRet 1
ora #$80
jmp _safeCout
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a carriage return
export asm crout()#0
+asmPlasmNoRet 0
lda #$8D
jmp _safeCout
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Send a command to the memory manager
export asm mmgr(cmd, wordParam)#1
+asmPlasmRet 2
jsr .setmmgr+ABS_OFFSET
jsr mainLoader ; ret value in X=lo/Y=hi
txa ; to A=lo/Y=hi for asmPlasm
rts
.setmmgr
lda evalStkL+1,x ; command code
pha
ldy evalStkH,x ; address (or other param)... hi byte in Y
lda evalStkL,x
tax ; ...lo byte in X
pla
rts
end
// Aux version of memory manager command
export asm auxMmgr(cmd, wordParam)#1
+asmPlasmRet 2
jsr .setmmgr+ABS_OFFSET
jsr auxLoader ; ret value in X=lo/Y=hi
txa ; to A=lo/Y=hi for asmPlasm
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get mem mgr's flag telling if this is the floppy version or not
asm getFloppyFlg()#1
+asmPlasmRet 0
lda floppyFlg
ldy #0
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Generate a sound on the Apple II speaker
export asm genSound(dnnoise, dnvelo, dndelay, upnoise, upvelo, updelay, dur)#0
+asmPlasmNoRet 7
jmp genSound
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to clear the current text window.
// Parameters: top, bottom, left, right
export asm setWindow(top, bottom, left, right)#0
+asmPlasmNoRet 4
jmp SetWindow
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the font engine's current text window
// Returns: top, bottom, left, right
export asm getWindow()#4
bit setLcRW+lcBank2
jmp GetWindow
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the cursor position - returns X, Y
export asm getCursor()#2
bit setLcRW+lcBank2
jmp GetCursor
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a keyboard string up to 40 chars using the Font Engine. Returns a zero-terminated string
// in the input buffer at $200, with the length stored in $2FF.
asm rawGetStr()#1
+asmPlasmRet 0
jmp GetStr
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to clear the current text window
export asm clearWindow()#0
+asmPlasmNoRet 0
jmp ClearWindow
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to copy the current text window to hi-res page 2 (or vice-versa)
// If flip=0, copies page 1 to page 2
// If flip=$60, copies page 2 to page 1
export asm copyWindow(flip)#0
+asmPlasmNoRet 1
tay
jmp CopyWindow
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a character using the font engine.
export asm displayChar(ch)#0
+asmPlasmNoRet 1
jmp DisplayChar
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a string using the font engine.
asm internalDisplayStr(str)#0
+asmPlasmNoRet 1
jmp DisplayStr
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate string width using the font engine.
export asm calcWidth(pStr)#1
+asmPlasmRet 1
jmp CalcWidth
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the address of the given hi-res screen line
export asm getScreenLine(n)#1
+asmPlasmRet 1
jmp GetScreenLine
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a string using the font engine but not its parser. Also, interpret "^A" as ctrl chars.
export asm rawDisplayStr(pStr)#0
+asmPlasmNoRet 1
sta pTmp
sty pTmp+1
ldy #0
lda (pTmp),y
sta tmp
- cpy tmp
bcs ++
iny
lda (pTmp),y
ora #$80
cmp #"^"
bne +
iny
lda (pTmp),y
cmp #"^"
beq +
and #$1F
ora #$80
+ sty tmp+1
jsr DisplayChar
ldy tmp+1
bne - ; always taken
++rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Random number generator. A standard linear congruential prng featuring a full cycle (i.e.
// every value from 1..MAGIC-1 is part of the sequence).
asm internal_rand16()#1
!zone {
+asmPlasmRet 0
ldx #6 ; start check = 1, plus 5 loops to mul by 32 = 6 total
ldy seed
lda seed+1
and #$7F ; in case kbd routine has advanced past MAGIC
sta seed+1
bne .chk ; zero check - hi byte
tya
bne .chk ; zero check - lo byte
lda #1 ; force 0 seed to go to 1
bne .ret ; always taken
.shift
asl
tay
rol seed+1
.chk
cpy #<MAGIC
lda seed+1
sbc #>MAGIC
bcc .next
sta seed+1
tya
sbc #<MAGIC
!byte $C9 ; CMP imm - to skip next opcode
.next
tya
dex
bne .shift ; 32 / 25
.ret
sta seed
ldy seed+1
rts
}
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear the text-mode screen, and put the text cursor at the top of it.
export asm textHome()#0
+asmPlasmNoRet 0
+safeHome
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Compare two strings for equality, ignoring case.
export asm streqi(a, b)#1
+asmPlasmRet 2
sta tmp
sty tmp+1
tya
beq .null
lda evalStkL+1,x
sta pTmp
lda evalStkH+1,x
beq .null
sta pTmp+1
ldy #0
lda (tmp),y
cmp (pTmp),y
beq + ; if lengths not equal, strings can't be equal
.noteqi
lda #0
tay
rts
+ tax ; count up to (verified same) length of the strings
- iny
lda (tmp),y
eor (pTmp),y
beq + ; matched
cmp #$20 ; check for case bit
bne .noteqi ; abort on alpha inequality
ora (tmp),y ; convert to lower case
cmp #('z'&$7F)+1
bcs .noteqi ; abort on inequality
cmp #'a'&$7F
bcc .noteqi ; abort on inequality
+ dex
bne -
lda #1
ldy #0 ; okay, they're equal. Return 1 (not just any char; so that PLASMA when...is can work)
rts
.null
lda #$13 ; inverse 'S'
sta $7D0
brk
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Multiply 16 bit number by 8-bit ratio, and drop lower 8 bits of result. In effect this scales
// num by the approximate ratio 0=0% .. 128=50% .. 255=99%
asm mulRatio(num, ratio)#1
+asmPlasmRet 2
.ratio = evalStkL
.numL = evalStkL+1
.numH = evalStkH+1
.numHH = tmp
.accLL = tmp+1
.accL = pTmp
.accH = pTmp+1
lda .ratio,x ; save ratio
ldy #0
sty .numHH ; zero what will become upper 8 bits of num
sty .accLL ; clear accumulation area
sty .accL
sty .accH
ldy #8 ; loop over 8 bits of ratio
- lsr ; get next bit
bcc + ; skip add if clear
pha
clc
lda .numL,x ; 24-bit add
adc .accLL
sta .accLL
lda .numH,x
adc .accL
sta .accL
lda .numHH
adc .accH
sta .accH
pla
+ asl .numL,x ; shift number up
rol .numH,x
rol .numHH
dey ; and loop again
bne -
ldy .accH ; final result in Y/A
lda .accL
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Check for script(s) attached to the given location, and establish the array of map scripts.
// Does not call any of them -- that's the job of scriptEvent().
// Structure of trigger table:
// Y (1 byte)
// entry length (1 byte, includes Y and this length)
// X (1 byte)
// script ptr (2 bytes)
// X
// script ptr...
// Y (or $FF for end of table)
// ...
asm scanScripts(x, y, triggerTbl, mapScripts)#1
!zone {
.pMapScripts = tmp
.pTriggerTbl = pTmp
.outOffset = ysav
.limit = ysav1
.xCoord = evalStkL+3
.yCoord = evalStkL+2
+asmPlasmRet 4
sta .pMapScripts ; pointer to output mapScripts
sty .pMapScripts+1
lda evalStkL+1,x ; pointer to trigger tbl
sta .pTriggerTbl
lda evalStkH+1,x
sta .pTriggerTbl+1
lda #0 ; zero initial output offset
sta .outOffset
.ylup
ldy #0
lda (.pTriggerTbl),y
cmp #$FF ; Y=$FF marks end of trigger table
bne +
.done
lda .outOffset
lsr ; divide offset by 2 to get # of output scripts
ldy #0
rts ; all done
+ iny
cmp .yCoord,x ; check for Y coordinate we want
beq .goty
lda (.pTriggerTbl),y ; not found, add offset to find next Y section
bne +
brk
+ clc
adc .pTriggerTbl
sta .pTriggerTbl
bcc .ylup
inc .pTriggerTbl+1
bne .ylup ; always taken
.goty
lda (.pTriggerTbl),y ; get # of bytes in X table
sta .limit
iny
.xlup
lda (.pTriggerTbl),y
iny
cmp .xCoord,x
bne .nextx
tya
pha ; save trigger table pointer
iny
lda (.pTriggerTbl),y ; hi byte of script
pha
dey
lda (.pTriggerTbl),y ; lo byte of script
ldy .outOffset
sta (.pMapScripts),y
iny
pla
sta (.pMapScripts),y
iny
sty .outOffset
pla ; back to scanning trigger tbl
tay
.nextx
iny
iny
cpy .limit ; stop at end of X/ptr list
bcs .done
bcc .xlup ; always taken
}
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Params: cmd | open<<8, filename, addr, size. Returns: status (for open only)
export asm callProRWTS(cmdPlusOpenFlg, filename, addr, size)#1
+asmPlasmRet 4
sei ; inhibit interrupts during aux operations
; Params come to us from PLASMA in reverse order
sta setAuxZP
sta sizelo
sty sizehi
sta clrAuxZP
lda evalStkL+1,x ; addr
ldy evalStkH+1,x
sta setAuxZP
sta ldrlo
sty ldrhi
sta clrAuxZP
lda evalStkL+2,x ; filename
ldy evalStkH+2,x
sta setAuxZP
sta namlo
sty namhi
sta clrAuxZP
lda evalStkL+3,x ; openFlg
ldy evalStkH+3,x ; cmd
sta setAuxZP
sta reqcmd
lda #0
sta auxreq
bit setLcRW+lcBank1
bit setLcRW+lcBank1
lda #$4C ; set up JMP to the requested proRWTS vector
sta $44
sty $45
lda #>proRWTS
sta $46
jsr $44
lda status
ldy #0
bit setLcRW+lcBank2
bit setLcRW+lcBank2
sta clrAuxZP
cli ; interrupts ok again
rts
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// The following only used for speed testing
//asm readNoSlotClock(dstBuf)#0 // param: dstBuf (will receive 8 BCD bytes)
//!zone {
// +asmPlasmNoRet 1
// ; record dst ptr
// sta tmp
// sty tmp+1
// ; obtain a pointer to our little table of magic values
// lda #$60
// sta pTmp
//.base = *+2
// jsr pTmp
// tsx
// dex
// dex
// txs
// pla
// sta pTmp
// pla
// sta pTmp+1
// ldy #<(.tbl - .base)
// ; record state of slot ROM, then turn on C3 ROM
// sei
// lda $CFFF
// pha
// sta $C300
// lda $C304
// ldx #8
//.wr1:
// lda (pTmp),y
// sec
// ror
//.wr2:
// bcs +
// bit $C300
// bcc ++
//+ bit $C301
//++ lsr
// bne .wr2
// iny
// dex
// bne .wr1
// ldy #7
//.rd1:
// ldx #8
//.rd2:
// lda $C304
// lsr
// ror pTmp
// dex
// bne .rd2
// lda pTmp
// sta (tmp),y
// dey
// bpl .rd1
// ; restore slot ROM state
// pla
// bmi +
// sta $CFFF
//+ cli
// rts
//.tbl !byte $C5,$3A,$A3,$5C,$C5,$3A,$A3,$5C
//} ; end zone
//end
//
//def getTick()
// byte timeBuf[8]
// word tick
// readNoSlotClock(@timeBuf)
// tick = (timeBuf[7] & $F) + ((timeBuf[7] >> 4) * 10)
// tick = tick + ((timeBuf[6] & $F) * 100) + ((timeBuf[6] >> 4) * 1000)
// return tick + (((timeBuf[5] & $F) % 5) * 6000)
//end
//
//def tickDiff(tStart, tEnd)
// word diff
// diff = tEnd - tStart
// if diff >= 0; return diff; fin
// return diff + 30000
//end
//
//def prtick(str)#0
// if lastTick
// printf2("%s:%d ", str, tickDiff(lastTick, getTick()))
// fin
// lastTick = getTick()
//end
///////////////////////////////////////////////////////////////////////////////////////////////////
// General methods
///////////////////////////////////////////////////////////////////////////////////////////////////
// Generate a sound that indicates being blocked or failing
export def beep()#0
genSound(0, 600, 60200, 0, 0, 60200, 30)
end
export def rand16()#1
word result
if recordMode
*seed = recordSeed
//printf1("%x:", recordSeed)
result = internal_rand16()
recordSeed = *seed
//printf1("%x ", recordSeed)
return result
fin
return internal_rand16()
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Fatal error: print message and stop the system.
// Doesn't really return a value, but can be handy for chaining.
export def fatal(msg)#1
return mmgr(FATAL_ERROR, msg)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the max of two signed 16-bit numbers
export def max(a, b)#1
return a > b ?? a :: b
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the min of two signed 16-bit numbers
export def min(a, b)#1
return a < b ?? a :: b
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the absolute value of a number
export def abs(n)#1
return n < 0 ?? -n :: n
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert a lower-case character to upper-case (or return unchanged if it's not lower-case)
export def charToUpper(c)#1
return (c >= 'a' and c <= 'z') ?? (c - $20) :: c
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def recordChar(ch)#0
if ch < $20
printChar('^')
ch = ch + $40
fin
printChar(ch)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def recordDisplay(str)#0
word i
puts("DISP:")
for i = 1 to ^str
recordChar(^(str + i) & $7F)
next
crout
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def displayStr(str)#0
if recordMode; recordDisplay(str); fin
internalDisplayStr(str)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a string from the keyboard using the font manager, and intern it to the heap.
export def getStringResponse()#1
word p
rawGetStr()
rawDisplayStr("\n") // so Outlaw user doesn't have to remember to make a newline
if recordMode
puts("STRING:"); puts($200); crout
fin
return mmgr(HEAP_INTERN, $200)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert signed decimal to string in decimalBuf (@decimalBuf returned)
def convertDec(n)#1
word n0
word p
p = @decimalBuf + 1
if n < 0; ^p = '-'; p=p+1; n = -n; fin
n0 = n
if n0 > 9999; ^p = '0' + n/10000; p=p+1; n = n%10000; fin
if n0 > 999; ^p = '0' + n/1000; p=p+1; n = n%1000; fin
if n0 > 99; ^p = '0' + n/100; p=p+1; n = n%100; fin
if n0 > 9; ^p = '0' + n/10; p=p+1; n = n%10; fin
^p = '0' + n; p=p+1
decimalBuf[0] = p - @decimalBuf - 1 // record final length of string
return @decimalBuf
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert byte to 3-char string in decimalBuf, suitable for font engine, e.g. ^T065
// (@fontPosBuf returned so as to not disturb decimalBuf)
def convert3Dec(n)#1
fontPosBuf[0] = 3
fontPosBuf[1] = '0' + (n / 100); n = n%100
fontPosBuf[2] = '0' + (n / 10); n = n%10
fontPosBuf[3] = '0' + n
return @fontPosBuf
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a formatted string a'la C printf, with up to three parameters.
export def printf3(str, arg1, arg2, arg3)#0
word pos
word curArg
word p
if !str
printChar('!') // Safety valve for NULL string pointer
return
fin
pos = 0
curArg = @arg1
while TRUE
pos = partialPrintf(str, pos)
if pos < 0
break
fin
p = str + pos + 2
when ^p
is 'd' // %d = decimal
puts(convertDec(*curArg)); break
is 's' // %s = string
puts(*curArg); break
is 'D' // %D = 3-char decimal suitable for font engine ctrl codes
puts(convert3Dec(*curArg)); break
is 'c' // %c = character
printChar(*curArg); break
is 'x' // %x = hex with '$'
printHex(*curArg); break
is '%' // %% = perfect
printChar('%'); break
otherwise
printHex(^p); fatal("Unknown % code")
wend
curArg = curArg + 2
pos = pos + 2
loop
end
export def printf1(str, arg1)#0; printf3(str, arg1, 0, 0); end
export def printf2(str, arg1, arg2)#0; printf3(str, arg1, arg2, 0); end
// Like printf, but displays text using font engine
export def displayf3(str, arg1, arg2, arg3)#0
buildString()
printf3(str, arg1, arg2, arg3)
displayStr(finishString(isPlural))
end
export def displayf1(str, arg1)#0; displayf3(str, arg1, 0, 0); end
export def displayf2(str, arg1, arg2)#0; displayf3(str, arg1, arg2, 0); end
// Like printf, but buffers string in $200
export def sprintf3(str, arg1, arg2, arg3)#1
buildString()
printf3(str, arg1, arg2, arg3)
return finishString(isPlural)
end
export def sprintf1(str, arg1); return sprintf3(str, arg1, 0, 0); end
export def sprintf2(str, arg1, arg2); return sprintf3(str, arg1, arg2, 0); end
// Like printf, but displays text using font engine
export def rawDisplayf1(str, arg1)#0; rawDisplayStr(sprintf3(str, arg1, 0, 0)); end
export def rawDisplayf2(str, arg1, arg2)#0; rawDisplayStr(sprintf3(str, arg1, arg2, 0)); end
export def rawDisplayf3(str, arg1, arg2, arg3)#0; rawDisplayStr(sprintf3(str, arg1, arg2, arg3)); end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the cursor position in the font engine
export def setCursor(x, y)#0
rawDisplayf2("^V%D^T%D", y, x)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def parseDec(str)#1
word n
word pend
word p
byte neg
neg = FALSE
n = 0
p = str + 1
pend = p + ^str
while p < pend
if p == (str+1) and ^p == '-'
neg = TRUE
elsif ^p >= '0' and ^p <= '9'
n = (n*10) + (^p - '0')
else
break
fin
p = p+1
loop
if neg; return -n; fin
return n
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Used in record mode to print out a keystroke. Also handles caret -> ctrl translation during
// playback of recordings.
export def recordKey()#1
byte key
key = ^kbd & $7F
^kbdStrobe
if recordMode
if key == '^'
while ^kbd < 128; loop
key = (^kbd & $7F) - $40
^kbdStrobe
fin
puts("KEY:")
recordChar(key)
puts(" SEED:$")
printHex(recordSeed)
crout
fin
return key
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a keystroke and convert it to upper case
export def getUpperKey()#1
byte key
// Make sure same text displays on both hi-res while animating
if anyAnims and textDrawn and mapIs3D and texturesLoaded
copyWindow(0)
fin
// Now wait for a key, and animate while doing so.
while ^kbd < 128
if recordMode
pause(30000)
else
// pause() will terminate on keypress, returning the count it did
*seed = *seed + pause(30000)
fin
loop
key = recordKey
return charToUpper(key)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a single char from the keyboard, and intern it (as a string) to the heap.
export def getCharResponse()#1
if needShowParty; showParty(); fin
^$200 = 1
^$201 = getUpperKey()
return mmgr(HEAP_INTERN, $200)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Select the num'th entry in the list for which the selector returns TRUE. A NULL selector
// is considered to always be TRUE.
export def select(p, sel, num)#1
while p
if !sel
if !num; break; fin
num--
elsif sel(p)
if !num; break; fin
num--
fin
p = p=>p_nextObj
loop
return p
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def first(p, sel)#1
return select(p, sel, 0)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def index(p, num)#1
return select(p, NULL, num)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def sum(p, sel, func)#1
word sum
sum = 0
while p
if !sel
sum = sum + func(p)
elsif sel(p)
sum = sum + func(p)
fin
p = p=>p_nextObj
loop
return sum
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def forSome(p, sel, do)#0
while p
if !sel
do(p)#0
elsif sel(p)
do(p)#0
fin
p = p=>p_nextObj
loop
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def forEach(p, do)#0
forSome(p, NULL, do)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Pause for a specified count period, advancing the animation periodically. Terminates early
// if a key is pressed. In either case, returns the number of counts waited.
export def pause(count)#1
word i, prevSeed
if recordMode; prevSeed = recordSeed; fin
for i = 0 to count
if ^kbd >= 128; break; fin
animPauseCt--
if animPauseCt < 0
nextAnimFrame() // also resets animPauseCt
fin
next
if recordMode; recordSeed = prevSeed; fin
return i
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate markup on a price, where ratio is an 8.8 fixed-point number. Some approx ratios:
// $0000 = 0%
// $0026 = 15%
// $0080 = 50%
// $0100 = 100%
// $0180 = 150%
export def addRatio(start, ratio)#1
word markup
markup = 0
while ratio > 255
markup = markup + start
ratio = ratio - 256
loop
return markup + mulRatio(start, ratio)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def percentToRatio(pct)#1
return addRatio(pct, 656) // Scale 0..100 to 0..255
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def addPercent(start, pct)#1
return addRatio(start, percentToRatio(pct))
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def encodeDice(nDice, dieSize, add)#1 // ndice=0..15, dieSize=0..15, add=0..255
return (nDice << 12) | (dieSize << 8) | add
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def rollDice(encoded)#1
byte nDice, dieSize
word result
nDice = (encoded >> 12) & $F // must mask off replicated hi-bits
dieSize = (encoded >> 8) & $F
result = encoded & $FF // initial add
while nDice
result = result + (rand16() % dieSize) + 1
nDice--
loop
return result
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Look up the partition for a resource.
// sectioNum: 0=version num (do not use here), 1=map2d, 2=map3d, 3=portrait, 4=story
export def lookupResourcePart(sectionNum, resourceNum)#1
word ptr
byte n, i
// Special case: main frame img laods before resource index
if !pResourceIndex; return 1; fin
// Skip to the requested section (starting just after version num)
ptr = pResourceIndex
while sectionNum > 0
ptr = ptr + readAuxByte(ptr) + 1
sectionNum--
loop
// And grab the number from that section's table
n = readAuxByte(ptr)
if resourceNum > n; fatal("lkup1"); fin
n = readAuxByte(ptr + resourceNum)
// If resource is on the current map's disk, prefer that
if n & (1<<(curMapPartition-1))
return curMapPartition
fin
// Otherwise return the first disk it's on
for i = 0 to 7
if n & (1<<i); return i+1; fin
next
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the sky color (relevant to 3D display only)
export def setSky(num)#0
if num <> skyNum
skyNum = num
setColor(0, skyNum)
needRender = TRUE
showingLamp = mapIs3D and texturesLoaded and (skyNum == 0 or skyNum == 8)
if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin // to update image
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the ground color (relevant to 3D display only)
export def setGround(num)#0
if num <> groundNum
groundNum = num
setColor(1, groundNum)
needRender = TRUE
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def heapCollect()#0
word pct
mmgr(CHECK_MEM, 0)
global=>w_heapSize = mmgr(HEAP_COLLECT, 0) - HEAP_BOTTOM
curHeapPct = min(99, max(0, ((global=>w_heapSize / 10) * 100) / (HEAP_SIZE / 10)))
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def resetAnimPause()#0
anyAnims = TRUE // for now; might get cleared if we discover otherwise on advance
animDirCt = 1
animPauseCt = ANIM_PAUSE_MAX
showingLamp = mapIs3D and texturesLoaded and (skyNum == 0 or skyNum == 8)
lampFrame = 0
lampDir = 1
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the Frame Image, and lock it.
export def loadFrameImg(img)#0
// Skip redundant reload
if frameLoaded == img; return; fin
// Free prev img and/or portrait (if any)
if curFullscreenImg
// must clear img directly, to avoid loading main frame img only to replace it
auxMmgr(FREE_MEMORY, curFullscreenImg)
curFullscreenImg = NULL
fin
clearPortrait()
// Make room in aux mem by throwing out textures
textureControl(FALSE)
// Load the image data into aux mem
if img
auxMmgr(START_LOAD, lookupResourcePart(4, img))
if img == 1
auxMmgr(SET_MEM_TARGET, $4000) // well above where expander loads at startup
fin
curFullscreenImg = auxMmgr(QUEUE_LOAD, img<<8 | RES_TYPE_SCREEN)
auxMmgr(FINISH_LOAD, 0)
resetAnimPause()
// And show the first frame of the screen image
^EMUSIG_FULL_COLOR
showAnimFrame()
else
curFullscreenImg = NULL
anyAnims = FALSE
fin
frameLoaded = img
// Do not render over the image
needRender = FALSE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the map name bar
export def setWindow1()#0
setWindow(8, 17, 35, 119) // Top, Bottom, Left, Right
mapNameHash = 0 // on the assumption that it's being set because somebody's going to print there
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the large upper right bar
export def setWindow2()#0
setWindow(24, 132, 154, 267) // Top, Bottom, Left, Right
displayChar('N'-$40) // Set normal mode - clear all special modes (like underline, etc.)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the mid-size lower right bar
export def setWindow3()#0
setWindow(144, 181, 154, 267) // Top, Bottom, Left, Right
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear map window, then set a text area within it.
export def useMapWindow()#0
byte bottom1, bottom2
if frameLoaded == 3 // don't check mapIs3D, since we might be in an engine
bottom1 = 154; bottom2 = 150
else
bottom1 = 169; bottom2 = 168
fin
setWindow(24, bottom1, 14, 140) // Top, Bottom, Left, Right
clearWindow()
setWindow(24, bottom2, 14, 140) // Top, Bottom, Left, Right
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def clearTextWindow()#0
if textDrawn or textClearCountdown
setWindow2(); clearWindow()
if mapIs3D and texturesLoaded; copyWindow(0); fin
textDrawn = FALSE
textClearCountdown = 0
snoozeX1 = -1
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def soundPlayEmu(numAndFlgs)#0
if mapEmuSound == $FF // if in initMap...
mapEmuSound = numAndFlgs // ... then record the music for resume after combat
fin
^EMUSOUND_PLAY = numAndFlgs
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def hline(addr, startByte, midByte, midSize, endByte)#0
^addr = startByte
memset(addr+1, midByte, midSize)
^(addr+midSize+1) = endByte
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Window that covers the entire inner area (destroys frame image, so be sure to loadMainFrame
// afterwards)
export def setBigWindow()#0
// Draw border (if not already drawn)
if frameLoaded
hline(getScreenLine(BIGWIN_TOP-4)+1, 0, 0, 36, 0)
hline(getScreenLine(BIGWIN_TOP-3)+1, $F8, $FF, 36, $8F)
hline(getScreenLine(BIGWIN_TOP-2)+1, $FC, $FF, 36, $9F)
hline(getScreenLine(BIGWIN_TOP-1)+1, $8C, 0, 36, $98)
vline(getScreenLine(BIGWIN_TOP)+1, $8C, 174)
vline(getScreenLine(BIGWIN_TOP)+38, $98, 174)
hline(getScreenLine(BIGWIN_BOTTOM-1)+1, $8C, 0, 36, $98)
hline(getScreenLine(BIGWIN_BOTTOM)+1, $FC, $FF, 36, $9F)
hline(getScreenLine(BIGWIN_BOTTOM+1)+1, $F8, $FF, 36, $8F)
hline(getScreenLine(BIGWIN_BOTTOM+2)+1, 0, 0, 36, 0)
frameLoaded = 0 // since we just destroyed it
textDrawn = FALSE
textClearCountdown = 0
^EMUSIG_FULL_TEXT
fin
setWindow(BIGWIN_TOP, BIGWIN_BOTTOM, BIGWIN_LEFT, BIGWIN_RIGHT)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def rightJustifyStr(str, rightX)#0
word space
space = rightX - calcWidth(str)
if (space > 0)
rawDisplayStr("^T") // do not use printf variants, since it might overwrite str
rawDisplayStr(convert3Dec(space))
fin
rawDisplayStr(str)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def rightJustifyNum(num, rightX)#0
rightJustifyStr(convertDec(num), rightX)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def centerStr(str, windowWidth)#0
word x
x = (windowWidth - calcWidth(str)) >> 1
if (x >= 0)
rawDisplayStr("^T") // do not use printf variants, since it might overwrite str
rawDisplayStr(convert3Dec(x))
fin
rawDisplayStr(str)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Display the party data on the screen
def showPartyLine(p)#0
if p <> global=>p_players; displayChar('\n'); fin
if p->b_skillPoints; rawDisplayStr("^I"); fin // inverse for chars needing lvl-up
rawDisplayf1("%s^N", p=>s_name)
rightJustifyStr(sprintf2("%d/%d", p=>w_health, p=>w_maxHealth), CHAR_WND_HEALTH_X)
end
export def showParty()#0
word p, cursX, cursY
cursX, cursY = getCursor()
setWindow3()
clearWindow()
// Display header (or LEVEL UP message if any player has un-applied skill pts)
if first(global=>p_players, &(p) p->b_skillPoints)
rawDisplayStr("^Y^I LEVEL U)P ^N\n")
else
rawDisplayStr("^LName")
rightJustifyStr(@S_HEALTH, CHAR_WND_HEALTH_X) // begin underline mode
rawDisplayStr("^N\n")
fin
// Display each character's name and health
forEach(global=>p_players, @showPartyLine)
// Finish up
if mapIs3D and texturesLoaded; copyWindow(0); fin
setWindow2()
setCursor(cursX, cursY)
needShowParty = FALSE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def getArgCount(pFunc)#1
word pBytecode
// skip over JMP to plasma interp, get addr in aux mem
pBytecode = pFunc=>3
// Check if the function starts with ENTER op
if readAuxByte(pBytecode) == $58
return readAuxByte(pBytecode+2)
fin
// Zero-arg functions sometimes omit ENTER altogether.
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def setTextCountdown()#0
textClearCountdown = 3
if mapIs3D and texturesLoaded
copyWindow(0)
fin
textDrawn = FALSE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Send an event to the scripts on the current map square
export def scriptEvent(event, param)#0
byte i, argCount
word script
if !nMapScripts or q_playerDeath; return; fin
if inScript; return; fin // avoid doing scripted events inside other scripts
inScript = TRUE
setWindow2()
textDrawn = FALSE
for i = 0 to nMapScripts-1
script = mapScripts[i]
argCount = getArgCount(script)
if argCount == 0 and event == @S_ENTER // zero-param scripts are assumed to be strictly 'enter' handlers
script()
elsif argCount == 1
script(event)
elsif argCount == 2
script(event, param)
fin
next
inScript = FALSE
if textDrawn; setTextCountdown; fin
clearPortrait()
if needShowParty; showParty(); fin
if global=>p_players=>w_health == 0; q_playerDeath = TRUE; fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def loadMainFrameImg()#0
if curFullscreenImg
auxMmgr(FREE_MEMORY, curFullscreenImg)
curFullscreenImg = NULL
fin
loadFrameImg(mapIs3D+2)
if mapIs3D
^EMUSIG_3D_MAP
else
^EMUSIG_2D_MAP
fin
if curFullscreenImg
auxMmgr(FREE_MEMORY, curFullscreenImg) // we don't allow animated main frames, so save memory
curFullscreenImg = NULL
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def loadUtil3d()#0
if !pModUtil3d and mapIs3D
mmgr(START_LOAD, 1) // code is in partition 1
pModUtil3d = mmgr(QUEUE_LOAD, MOD_UTIL3D<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
util3d = pModUtil3d()
initCmds() // rebuild the command table with new function ptrs
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Load code and data, set up everything to display a 2D or 3D map
def initMap(x, y, dir)#0
//AUTOMAP_CHECK// word pDiskOps
// If we have a renderer loaded, let it know to flush automap marks
textureControl(FALSE)
// Reset memory (our module will stay since memory manager locked it upon load)
mmgr(RESET_MEMORY, 0)
// Reset all pointers to non-locked memory, since the stuff might go away
pModUtil3d = NULL
util3d = NULL
pCurMap = NULL
curEngine = NULL
pGlobalTileset = NULL
curPortrait = NULL
curPortraitNum = 0
curFullscreenImg = NULL
renderLoaded = FALSE // leave it this way until all scripts done, else scriptDisplayStr renders
// Load the frame image, then raycaster or tile engine
loadMainFrameImg()
mmgr(START_LOAD, 1)
mmgr(SET_MEM_TARGET, displayEngine)
if mapIs3D
mmgr(QUEUE_LOAD, CODE_RENDER<<8 | RES_TYPE_CODE)
// Reserve memory for the raycaster's tables
mmgr(SET_MEM_TARGET, RAY_TABLE_START)
mmgr(REQUEST_MEMORY, RAY_TABLE_SIZE)
else
mmgr(QUEUE_LOAD, CODE_TILE_ENGINE<<8 | RES_TYPE_CODE)
fin
pGlobalTileset = mmgr(QUEUE_LOAD, 1<<8 | RES_TYPE_TILESET) // even in 3d, need tiles for lamp/etc.
//AUTOMAP_CHECK// pDiskOps = mmgr(QUEUE_LOAD, MOD_DISKOPS<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
if mapIs3D; loadUtil3d(); fin
//AUTOMAP_CHECK// pDiskOps()=>diskops_checkAutomap()
//AUTOMAP_CHECK// mmgr(FREE_MEMORY, pDiskOps)
// Set up the command table
initCmds() // must be after loading util3d
// Load the map
curMapPartition = lookupResourcePart(mapIs3D+1, mapNum)
if !curMapPartition; fatal("lkup2"); fin
mmgr(START_LOAD, curMapPartition)
pCurMap = mmgr(QUEUE_LOAD, mapNum<<8 | (RES_TYPE_2D_MAP+mapIs3D))
mmgr(FINISH_LOAD, 0)
// Clear all the windows to the background color (hi-bit set)
// except window3 because showParty does it for us
setWindow1(); clearWindow()
setWindow2(); clearWindow()
// Clear the list of encounter zones from any previous maps
clearEncounterZones
// Start up the display engine with map data and starting position. This will also load and
// init the script module, if any, which will end up calling us back at the setScriptInfo
triggerTbl = NULL
setWindow2()
skyNum = 9 // default
groundNum = 10 // default
timeEventFunc = NULL
mapEmuSound = $FF // special value to mark init
initDisplay(curMapPartition, mapNum, pCurMap, x, y, dir)
if mapEmuSound == $FF // if no music assigned...
mapEmuSound = 0 // ...then record that fact.
fin
texturesLoaded = TRUE
needRender = FALSE
textDrawn = FALSE
textClearCountdown = 0
curEngine = NULL
curPortrait = NULL
curPortraitNum = 0
curFullscreenImg = NULL
prevClockColor = 99
snoozeX1 = -1
if timeEventFunc; timeEventFunc(global->b_hour); fin
if mapIs3D
if util3d
util3d=>util3d_showCompassDir(dir)
util3d=>util3d_showClock()
fin
elsif global->b_curAvatar <> 0
setAvatar(global->b_curAvatar)
doRender()
fin
// Init the floppy flag after the first full load, which will have certainly
// hit some disk besides 1 if there is such a disk.
isFloppyVer = getFloppyFlg
// Assume there might be animations until we learn otherwise
resetAnimPause
// Populate script handlers for the current square, so that leave handlers will trigger right.
nMapScripts = scanScripts(x-triggerOriginX, y-triggerOriginY, triggerTbl, @mapScripts)
// Display the party characters
showParty()
// All done.
renderLoaded = TRUE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def scriptSetAvatar(avatarTileNum)#0
global->b_curAvatar = avatarTileNum
if renderLoaded; setAvatar(avatarTileNum); fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def textureControl(flg)#0
if renderLoaded
flipToPage1
if flg <> texturesLoaded
if flg
_texControl(1)
if mapIs3D; loadUtil3d; fin
showingLamp = mapIs3D and (skyNum == 0 or skyNum == 8)
else
_texControl(0)
if !mapIs3D
pCurMap = NULL // tile engine frees all maps, even current one
fin
if pModUtil3d and mapIs3D and !inScript // e.g. strafe -> script -> combat: need strafe to stay in mem!
mmgr(FREE_MEMORY, pModUtil3d)
pModUtil3d = NULL
util3d = NULL
fin
showingLamp = FALSE
needRender = FALSE
fin
fin
texturesLoaded = flg
else
texturesLoaded = FALSE
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a portrait drawing (typically called from scripts)
export def clearPortrait()#0
if curPortrait
auxMmgr(FREE_MEMORY, curPortrait)
curPortrait = NULL
curPortraitNum = 0
needRender = TRUE
fin
if curFullscreenImg
auxMmgr(FREE_MEMORY, curFullscreenImg)
curFullscreenImg = NULL
loadMainFrameImg()
needRender = TRUE
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Perform rendering, copy if necessary, clear appropriate flags
def doRender()#0
if curPortrait; clearPortrait(); fin
textureControl(TRUE)
if showingLamp; util3d=>util3d_nextLampFrame(); fin
render(0) // Don't interrupt on kbd - must finish the render
needRender = FALSE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def advTime(hours, mins, secs)#0
byte redrawClock, runScript, prevHour, add
word player
if secs
global->b_second = global->b_second + secs
while global->b_second >= 60
global->b_second = global->b_second - 60
mins++
loop
fin
if mins or hours
redrawClock = FALSE
runScript = FALSE
global->b_minute = global->b_minute + mins
if global->b_minute >= nextSignificantMinute; redrawClock = TRUE; fin
while global->b_minute >= 60
global->b_minute = global->b_minute - 60
hours++
loop
if hours
redrawClock = TRUE
prevHour = global->b_hour
global->b_hour = global->b_hour + hours
while global->b_hour >= 24
global->b_hour = global->b_hour - 24
loop
runScript = TRUE
// Heal over time every 12 hours
if (prevHour / 12) <> (global->b_hour / 12)
player = global=>p_players
while player
add = (rand16 % (player->b_stamina+1))/3
setStat(player, @S_HEALTH, player=>w_health + add)
player = player=>p_nextObj
loop
if needShowParty; showParty(); fin
fin
fin
if mapIs3D and redrawClock and renderLoaded and texturesLoaded and util3d
util3d=>util3d_showClock()
fin
if runScript and timeEventFunc
timeEventFunc(global->b_hour)
fin
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by scripts to display a string. We set the flag noting that something has been
// displayed, then use an assembly routine to do the work.
def _scriptDisplayStr(str)#0
if needRender and renderLoaded and texturesLoaded and !curPortrait and !curFullscreenImg
doRender()
flipToPage1()
needRender = FALSE
fin
if textClearCountdown; clearTextWindow(); fin
if forceRawScrDisp
rawDisplayStr(str)
else
displayStr(str)
fin
textDrawn = TRUE
anyInteraction = TRUE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def promptAnyKey(clearAfter)#0
scriptDisplayStr("\n(press any key)")
getUpperKey
if clearAfter; clearWindow; fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def snooze()#1
word cursX, cursY
cursX, cursY = getCursor()
if cursX == snoozeX1 and cursY == snoozeY
if mapIs3D
advTime(0, 14 - (global->b_minute % 15), 60 - global->b_second) // next 15 min mark
else
advTime(0, 59 - global->b_minute, 60 - global->b_second) // start of next hour
fin
fin
nextAnimFrame()
cursX, cursY = getCursor()
if cursX == snoozeX1 and cursY == snoozeY
rawDisplayf1("^T%D", snoozeX0)
else
if cursY; rawDisplayStr("\n"); fin
if cursX; rawDisplayStr("\n"); fin
rawDisplayStr("The time: ")
snoozeX0, snoozeY = getCursor()
fin
rawDisplayf1("^C%d:", global->b_hour == 0 ?? 12 :: global->b_hour == 12 ?? 12 :: global->b_hour % 12)
rawDisplayf2("%s%d ", global->b_minute < 10 ?? "0" :: "", global->b_minute)
rawDisplayf1("%s.", global->b_hour < 12 ?? "am" :: "pm")
snoozeX1, snoozeY = getCursor()
setTextCountdown
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def moveInternal(facingDir, moveDir, beepOK, shouldAdvTime)#1
byte val, i
word x, y
setDir(moveDir)
val = advance(global->b_moveMode)
lastMoveDir = moveDir
setDir(facingDir)
// If not blocked, render at the new position.
if val == 0
if beepOK and !inScript // don't beep for scripted moves
clearTextWindow
rawDisplayStr("Blocked!\n")
beep
setTextCountdown
fin
else
if !mapIs3D
doRender()
else
needRender = TRUE
fin
fin
// Advance time if requested
if shouldAdvTime
if mapIs3D
for i = 1 to global->b_moveMode
advTime(CLOCK_ADV_3D_HOURS, CLOCK_ADV_3D_MINS, CLOCK_ADV_3D_SECS)
next
else
advTime(CLOCK_ADV_2D_HOURS, CLOCK_ADV_2D_MINS, CLOCK_ADV_2D_SECS)
fin
fin
// If we're on a new map tile, run leave handlers from old tile.
if val >= 2
scriptEvent(@S_LEAVE, NULL)
nMapScripts = 0
// If there are script(s) on the new tile, run them.
getPos(@x, @y)
nMapScripts = scanScripts(x-triggerOriginX, y-triggerOriginY, triggerTbl, @mapScripts)
if nMapScripts
scriptEvent(@S_ENTER, NULL)
elsif global=>p_encounterZones
checkEncounter(x, y, FALSE)
fin
fin
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, TRUE) // beep ok, adv time
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Move backward two quarter-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
byte facingDir, moveDir
facingDir = getDir()
if lastMoveDir <> $FF
moveDir = (lastMoveDir + 8) & 15 // reverse of last move
moveInternal(facingDir, moveDir, FALSE, TRUE) // no beep, but do adv time
if global->b_moveMode == 1
moveInternal(facingDir, moveDir, FALSE, TRUE) // no beep, but do adv time
fin
fin
lastMoveDir = $FF // avoid problems if moveWayBackward called repeatedly
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Move to the north/east/south/west (2D mode)
def moveNorth()#1
word x, y
getPos(@x, @y)
if y > 4
setDir(0)
moveForward()
else
beep()
fin
return 0
end
def moveEast()#1
word x, y
getPos(@x, @y)
if x < totalMapWidth-5
setDir(4)
moveForward()
else
beep()
fin
return 0
end
def moveSouth()#1
word x, y
getPos(@x, @y)
if y < totalMapHeight-5
setDir(8)
moveForward()
else
beep()
fin
return 0
end
def moveWest()#1
word x, y
getPos(@x, @y)
if x > 4
setDir(12)
moveForward()
else
beep()
fin
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Switch to a new map (2D or 3D) and establish position on it
export def setMap(is3D, num, x, y, dir)#0
if is3D == mapIs3D and num == mapNum
setPos(x, y)
setDir(dir)
needRender = TRUE
else
flipToPage1()
showMapName("Traveling...")
useMapWindow()
mapIs3D = is3D
mapNum = num
initMap(x, y, dir)
saveGame
fin
// Don't send enter event, because we often land on an "Exit to wilderness?" script
//NO:scriptEvent(S_ENTER, NULL)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def callGlobalFunc(moduleNum, arg1, arg2, arg3)#1
word pModule, pFunc, ret
// First load the module
if renderLoaded; flipToPage1(); fin
mmgr(START_LOAD, moduleNum <= LAST_REQ_GS_MOD ?? 1 :: curMapPartition)
pModule = mmgr(QUEUE_LOAD, moduleNum<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
// Call the function, passing it the number of args it expects
pFunc = pModule()
when getArgCount(pFunc)
is 0; ret = pFunc(); break
is 1; ret = pFunc(arg1); break
is 2; ret = pFunc(arg1, arg2); break
is 3; ret = pFunc(arg1, arg2, arg3); break
otherwise fatal("maxGlobParams")
wend
// Unload the module and we're done.
mmgr(FREE_MEMORY, pModule)
return ret
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def queue_setMap(is3D, num, x, y, dir)#0
byte part
part = lookupResourcePart(is3d+1, num)
if diskLimit and part > diskLimit
clearWindow
callGlobalFunc(GS_DISK_LIMIT, 1, 0, 0)
return
fin
q_mapIs3D = is3D
q_mapNum = num
q_x = x
q_y = y
q_dir = dir
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def queue_teleport(x, y, dir)#0
queue_setMap(mapIs3D, mapNum, x, y, dir)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a key and dispatch it to a command. Then do it again, forever.
def kbdLoop()#0
word func, tmp
byte xreg
xreg = getXReg()
while TRUE
// If the asm routines all work correctly, by the time we get to the top of this loop
// the X register should always have the same value.
tmp = getXReg()
if tmp <> xreg; printHex(xreg<<8 | tmp); fatal("xRegChg"); fin
cmdKey = getUpperKey()
if cmdKey >= 0 and cmdKey < $60
func = cmdTbl[cmdKey]
if func
if textClearCountdown
if textClearCountdown == 1; clearTextWindow(); fin
textClearCountdown--
fin
func()
fin
fin
if q_playerDeath; playerDeath(); continue; fin
if q_mapNum; setMap(q_mapIs3D, q_mapNum, q_x, q_y, q_dir); q_mapNum = 0; fin
if needRender; doRender(); fin
if prevScriptModule
if prevScriptModule <> scriptModule
tmp = mmgr(FIND_IN_MEM, prevScriptModule<<8 | RES_TYPE_MODULE)
if tmp; mmgr(FREE_MEMORY, tmp); fin
fin
prevScriptModule = 0
fin
loop
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def hashString(str)#1
return hashBuffer(str+1, ^str)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def showMapName(mapName)#0
word newNameHash
newNameHash = hashString(mapName)
if newNameHash <> mapNameHash
setWindow1()
clearWindow()
centerStr(mapName, 84)
if mapIs3D and texturesLoaded; copyWindow(0); fin
mapNameHash = newNameHash
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Set initial info for the scripts on this map: the name of the map, its trigger table, and the
// maximum extent (width, height). This is called by the init function for the scripts.
export def setScriptInfo(mapName, moduleNum, timeFn, trigTbl, wdt, hgt)#0
word tmp
// Record the time event function, if any
timeEventFunc = timeFn
// Record new script, and prepare to free old one. While theoretically there could be a
// circumstance where the old prev is still un-freed, it's rare enough that it'd be hard to
// test freeing it here correctly. So we leave freeing to the main keyboard loop.
if scriptModule <> moduleNum
prevScriptModule = scriptModule
scriptModule = moduleNum
fin
// Grab the trigger table origins (used so the table can be more compact)
triggerOriginX = trigTbl=>0
triggerOriginY = trigTbl=>2
// Record the trigger table pointer
triggerTbl = trigTbl + 4
// Record the maximum width and height
totalMapWidth = wdt
totalMapHeight = hgt
// Display map name
global=>s_mapName = mmgr(HEAP_INTERN, mapName)
showMapName(mapName)
// Get ready for new encounter zones
clearEncounterZones
// Back to the main text window.
setWindow2()
// If there's a time script, run it so it can set sky color, etc.
if timeFn; timeFn(global->b_hour); fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by scripts to swap a map tile. We set a flag noting we need to re-render, then use an
// assembly routine to do the work.
export def scriptCopyTile(fromX, fromY, toX, toY)#0
needRender = TRUE
copyTile(fromX, fromY, toX, toY)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a key, and don't return until it's Y or N (or lower-case of those). Returns 1 for Y.
export def getYN()#1
byte key
while TRUE
key = getUpperKey()
if key == 'Y'
return 1
elsif key == 'N'
if frameLoaded
clearTextWindow()
fin
break
fin
beep()
loop
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Show the current animation frame
def showAnimFrame()#0
byte top
if curPortrait
if storyMode
blit(1, curPortrait + 2, getScreenLine(0)+20, 128, 18)
else
// Blit portrait to the appropriate area on the screen
top = 32 // start at 4th text line in 2D
if frameLoaded == 3 // 3D-mode frame? Note: don't check mapIs3D, because we might be in an engine
top = 24 // start at 4th text line in 3D
fin
blit(1, curPortrait + 2, getScreenLine(top)+2, 128, 18)
fin
needRender = FALSE // suppress display of map for this frame
if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin
elsif curFullscreenImg
blit(1, curFullscreenImg + 2, getScreenLine(0), 192, 40) // the +2 is to skip anim hdr offset
needRender = FALSE // suppress display of map for this frame
elsif texturesLoaded
if mapIs3D
if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin
render($FF) // it's only animation, so do interrupt if a key is pressed
if showingLamp and util3d; util3d=>util3d_nextLampFrame(); fin
else
render(0) // 2d map view
fin
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Advance to next frame of current animation, if any
def nextAnimFrame()#0
word flags, param
byte dir, randNum
animPauseCt = ANIM_PAUSE_MAX
if !anyAnims; return; fin
// Choose a new direction based on the flags. Do this the first time, and once every 3-7 frames.
animDirCt = animDirCt - 1
if animDirCt <= 0
animDirCt = (rand16() % 5) + 3
fin
// Advance animations.
// First part is whether to switch directions on fwd/back anims.
// Second part is how many frames to advance random anims.
param = ((animDirCt==1) & 1) | (((rand16() % 10)+1)<<8)
flags = auxMmgr(ADVANCE_ANIMS, param) | mmgr(ADVANCE_ANIMS, param)
if (flags >> 8)
// An animation was changed -- display it
showAnimFrame()
elsif (flags & $FF) == 0
// No animations in memory; turn off future checking
anyAnims = FALSE
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a portrait drawing (typically called from scripts)
export def setPortrait(portraitNum)#0
word srcData
byte part, cx, cy, cursX, cursY
if portraitNum == curPortraitNum; return; fin
clearPortrait()
// We're going to switch windows. Save the cursor pos in the text window.
cursX, cursY = getCursor()
// Make room by unloading the textures (only if renderer is loaded)
textureControl(FALSE)
// Now clear out the map area
useMapWindow()
// Restore the cursor position
setWindow2()
setCursor(cursX, cursY)
// Load the portrait image and display it
part = lookupResourcePart(3, portraitNum)
if !part; fatal("lkup3"); fin
// 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
mmgr(FINISH_LOAD, 0)
resetAnimPause
// And show the first frame
showAnimFrame()
// Do not render over the portrait
needRender = FALSE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def countList(p)#1
return sum(p, NULL, &(p) 1)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Call like this: addToList(@player=>p_items, itemToAdd)
export def addToList(addTo, p)#0
// Get to the end of the list
while *addTo
addTo = (*addTo) + p_nextObj
loop
p=>p_nextObj = *addTo
*addTo = p
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Call like this: removeFromList(@player=>items, itemToRemove)
export def removeFromList(pList, toRemove)#0
word p
p = *pList
while p and p <> toRemove
pList = p + p_nextObj
p = *pList
loop
if p
*pList = p=>p_nextObj
p=>p_nextObj = NULL
else
fatal("unlink")
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def saveMapPos()#0
global->b_mapIs3D = mapIs3D
global->b_mapNum = mapNum
getPos(@global=>w_mapX, @global=>w_mapY)
global->b_mapDir = getDir()
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def showMoveMode()#0
rawDisplayStr("Move mode:\n")
if global->b_moveMode == 1; rawDisplayStr("^I"); fin
rawDisplayStr("Default^N ")
if global->b_moveMode == 2; rawDisplayStr("^I"); fin
rawDisplayStr("Fast^N ")
if global->b_moveMode == 4; rawDisplayStr("^I"); fin
rawDisplayStr("Classic^N\n")
setTextCountdown
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def restoreMapPos()#0
mapIs3D = global->b_mapIs3D
mapNum = global->b_mapNum
initMap(global=>w_mapX, global=>w_mapY, global->b_mapDir)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def loadEngine(moduleNum)#1
if curEngine; fatal("dblEng"); fin
preEnginePortraitNum = curPortraitNum
clearPortrait()
textureControl(FALSE) // also flips to page 1
mmgr(START_LOAD, 1) // code is in partition 1
curEngine = mmgr(QUEUE_LOAD, moduleNum<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
return curEngine() // return function table
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def returnFromEngine(render)#0
if curEngine
mmgr(FREE_MEMORY, curEngine)
curEngine = NULL
loadMainFrameImg()
if preEnginePortraitNum
setPortrait(preEnginePortraitNum)
else
clearPortrait()
fin
textureControl(TRUE)
mapNameHash = 0; showMapName(global=>s_mapName)
clearTextWindow()
if render
doRender()
else
needRender = TRUE
fin
if mapIs3D
prevClockColor = 99
if util3d
util3d=>util3d_showCompassDir(getDir())
util3d=>util3d_showClock()
fin
fin
showParty()
setWindow2() // in case we're mid-script
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the automap engine and display the map
def showAutomap()#1
loadEngine(MOD_AUTOMAP)=>automap_show(9999, 9999) // 9999 = no quest target
returnFromEngine(TRUE)
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the Party engine and show data for the given player
def showPlayerSheet(num)#1
word sNameToUse, oldFlg, pItemutilModule
if num+1 > countList(global=>p_players); beep; return 0; fin
sNameToUse = loadEngine(MOD_PARTY)=>party_showPlayerSheet(num)
returnFromEngine(TRUE)
// General 'use' handled here in case it triggers graphical effects
if sNameToUse
anyInteraction = FALSE
scriptEvent(@S_USE, sNameToUse)
if !anyInteraction
scriptDisplayStr("Nothing happens.")
setTextCountdown
fin
fin
playerUsing = NULL
return 0
end
def showPlayer1()#1
return showPlayerSheet(0)
end
def showPlayer2()#1
return showPlayerSheet(1)
end
def showPlayer3()#1
return showPlayerSheet(2)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Level up the first character that's applicable
def levelUp()#1
word player
byte n
player = global=>p_players
n = 0
while player
if player->b_skillPoints
showPlayerSheet(n)
return 0
fin
player = player=>p_nextObj
n++
loop
beep
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def addEncounterZone(code, x, y, dist, chance)#0
word p
p = mmgr(HEAP_ALLOC, TYPE_ENCOUNTER_ZONE)
p=>s_name = mmgr(HEAP_INTERN, code)
p=>w_encX = x
p=>w_encY = y
p=>w_encMaxDist = dist
p=>w_encChance = chance
addToList(@global=>p_encounterZones, p)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def clearEncounterZones()#0
global=>p_encounterZones = NULL
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by user-defined map scripts to initiate a combat encounter.
export def scriptCombat(mapCode)#1
return doCombat(mapCode, TRUE)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def playerDeath()#0
setWindow2; clearWindow
callGlobalFunc(GS_DEATH, 0, 0, 0)
startGame(FALSE, FALSE) // don't ask, just load
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def resumeMapMusic()#0
if mapEmuSound > 0 and mapEmuSound <> $FF
soundPlayEmu(mapEmuSound)
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def doCombat(mapCode, backUpOnFlee)#1
word result
// Give a clue as to what's happening
if needRender; doRender; fin
flipToPage1
setWindow2()
clearWindow()
callGlobalFunc(GS_COMBAT_INTRO, 0, 0, 0)
// Remainder handled in a separate module. Clear enemies out of the heap when finished.
result = loadEngine(MOD_COMBAT)=>combat_zoneEncounter(mapCode)
global=>p_enemyGroups = NULL
heapCollect()
if (result == -99)
q_playerDeath = TRUE
return 0
fin
returnFromEngine(!inScript) // only re-render if outside script
resumeMapMusic
// Advance time a little. It's tricky advancing during combat; this is an ok substitute.
advTime(CLOCK_ADV_COMBAT_HOURS, CLOCK_ADV_COMBAT_MINS, CLOCK_ADV_COMBAT_SECS)
// If the party fled the combat instead of winning, back up to previous square.
if !result and backUpOnFlee
moveWayBackward()
fin
return result
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Check for a random encounter at this position
export def checkEncounter(x, y, force)#0
word p
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
p = global=>p_encounterZones
while p
d = min(abs(x - p=>w_encX), abs(y - p=>w_encY))
// Using '<=' below so that later-added zones added by a scripted event take precedence
if d <= bestDist and (p=>w_encMaxDist == 0 or d <= p=>w_encMaxDist)
p_bestZone = p
bestDist = d
fin
p = p=>p_nextObj
loop
// Roll for an encounter in the zone.
d = rand16() % 1000
if p_bestZone
if (d < p_bestZone=>w_encChance or force)
// Encounter!
doCombat(p_bestZone=>s_name, !force)
fin
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def rwGame(cmd)#0
while TRUE
if callProRWTS(cmd | RWTS_OPENDIR, @S_GAME1_FILENAME, LOAD_SAVE_BUF, HEAP_SIZE) == 0
break
fin
textHome()
^$c051
puts("Insert disk 1")
rdkey()
^$c050
loop
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def saveGame()#1
word cursX, cursY
cursX, cursY = getCursor()
saveMapPos()
showMapName("Saving game...")
textureControl(FALSE) // also flips to page 1
// Perform garbage collection and record the size of the heap so we can restore it correctly
// (also does a CHECK_MEM to be sure we never save corrupted heap)
heapCollect()
//AUTOMAP_CHECK// readDiskMarks; checkMarks
// Copy data to main memory, and write it out.
memcpy(HEAP_BOTTOM, LOAD_SAVE_BUF, HEAP_SIZE, 0) // LC to low mem
rwGame(RWTS_WRITE)
mapNameHash = 0; showMapName(global=>s_mapName)
doRender
setWindow2()
setCursor(cursX, cursY)
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def loadGame()#1
// Since old map may have been modified by scripts, force it to really be gone.
if pCurMap
mmgr(FREE_MEMORY, pCurMap)
mmgr(SET_MEM_TARGET, pCurMap)
mmgr(REQUEST_MEMORY, 5)
mmgr(FREE_MEMORY, pCurMap)
pCurMap = NULL
fin
loadEngine(MOD_DISKOPS)=>diskops_loadGame()
restoreMapPos()
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def help()#1
flipToPage1
setBigWindow; clearWindow
forceRawScrDisp = TRUE
loadEngine(GS_HELP)()
forceRawScrDisp = FALSE
returnFromEngine(TRUE)
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def toggleGodMode()#1
byte key
key = getUpperKey
if key == 15 // ctrl-O
key = getUpperKey
if key == 4 // ctrl-D
flipToPage1()
clearTextWindow()
if key == 4 // ctrl-D
global->b_godmode = !global->b_godmode
displayf1("gm:%d\n", global->b_godmode & 1)
initCmds() // rebuild the command table with new commands
fin
textDrawn = TRUE
beep; beep
clearTextWindow()
fin
fin
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def cheatCmd()#1
byte key
word pModule
// We don't use laodEngine, since godmode may use it too, and we'd get double modules
textureControl(FALSE) // seems to be necessary for teleport-to-3d to work right
flipToPage1
mmgr(START_LOAD, 1) // code is in partition 1
pModule = mmgr(QUEUE_LOAD, MOD_GODMODE<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
pModule()=>godmode_cheatCmd(cmdKey)
mmgr(FREE_MEMORY, pModule)
textureControl(TRUE)
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// This one godmode command needs to not load a module -- print memory without disturbing it
export def printMem()#1
flipToPage1
^$c051
mmgr(DEBUG_MEM, 0)
rdkey
auxMmgr(DEBUG_MEM, 0)
rdkey
^$c050
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def showQuests()#1
loadEngine(MOD_QUESTLOG)=>questlog_showQuests()
returnFromEngine(TRUE) // do render
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def switchMoveMode()#1
byte dir
word x, y
dir = getDir()
if global->b_moveMode == 1
global->b_moveMode = 2
dir = dir & ~1
elsif global->b_moveMode == 2
global->b_moveMode = 4
dir = dir & ~3
else
global->b_moveMode = 1
fin
setDir(dir)
getPos(@x, @y)
setPos(x, y) // to round it
needRender = TRUE
flipToPage1()
clearTextWindow()
showMoveMode
return 0
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the command table for 3D mode
def initCmds()#0
// Clear the command table
byte i
for i = 0 to 95
cmdTbl[i] = 0
next
// Commands common to both 2D and 3D
cmdTbl['1'] = @showPlayer1
cmdTbl['2'] = @showPlayer2
cmdTbl['3'] = @showPlayer3
cmdTbl['U'] = @levelUp
cmdTbl[$13] = @saveGame // ctrl-S
cmdTbl[$0c] = @loadGame // ctrl-L
cmdTbl['?'] = @help
cmdTbl['/'] = @help // in case they forget to hit shift
cmdTbl[$07] = @toggleGodMode // ctrl-G
cmdTbl[' '] = @snooze // "space out" (snooze)
cmdTbl['M'] = @showAutomap
cmdTbl['Q'] = @showQuests
if global->b_godmode
// install cheat commands
cmdTbl['T'] = @cheatCmd // teleport
cmdTbl[$10] = @cheatCmd // ctrl-P: show pos
cmdTbl['>'] = @cheatCmd // next portrait
cmdTbl['<'] = @cheatCmd // prev portrait
cmdTbl['!'] = @cheatCmd // test combat
cmdTbl['Y'] = @cheatCmd // next sky
cmdTbl['G'] = @cheatCmd // next ground
cmdTbl['&'] = @printMem // print mem
cmdTbl['^'] = @cheatCmd // edit flags
cmdTbl['*'] = @cheatCmd // soundGen test
fin
// Commands handled differently in 3D vs 2D
if mapIs3D
cmdTbl[3] = @switchMoveMode // ctrl-C
cmdTbl['W'] = @moveForward
cmdTbl['A'] = util3d=>util3d_rotateLeft
cmdTbl['D'] = util3d=>util3d_rotateRight
cmdTbl['S'] = util3d=>util3d_moveBackward
cmdTbl['Z'] = util3d=>util3d_strafeLeft
cmdTbl['C'] = util3d=>util3d_strafeRight
cmdTbl['I'] = @moveForward
cmdTbl['J'] = util3d=>util3d_rotateLeft
cmdTbl['L'] = util3d=>util3d_rotateRight
cmdTbl['K'] = util3d=>util3d_moveBackward
cmdTbl['H'] = util3d=>util3d_strafeLeft
cmdTbl[';'] = util3d=>util3d_strafeRight
cmdTbl[11] = @moveForward // up-arrow
cmdTbl[8] = util3d=>util3d_rotateLeft // left-arrow
cmdTbl[21] = util3d=>util3d_rotateRight // right-arrow
cmdTbl[10] = util3d=>util3d_moveBackward // down-arrow
else
cmdTbl['W'] = @moveNorth
cmdTbl['D'] = @moveEast
cmdTbl['S'] = @moveSouth
cmdTbl['A'] = @moveWest
cmdTbl['I'] = @moveNorth
cmdTbl['J'] = @moveWest
cmdTbl['L'] = @moveEast
cmdTbl['K'] = @moveSouth
cmdTbl[11] = @moveNorth // up-arrow
cmdTbl[8] = @moveWest // left-arrow
cmdTbl[21] = @moveEast // right-arrow
cmdTbl[10] = @moveSouth // down-arrow
fin
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
if loadedSize
mmgr(SET_MEM_TARGET, HEAP_BOTTOM + loadedSize)
fin
mmgr(HEAP_SET, HEAP_BOTTOM)
i = 0
while typeTbls[i]
mmgr(HEAP_ADD_TYPE, typeTbls[i])
i = i+1
loop
if loadedSize
global = HEAP_BOTTOM
else
global = mmgr(HEAP_ALLOC, TYPE_GLOBAL)
global=>w_typeHash = typeHash
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Constructor: create a modifier given its name and value
export def makeModifier(name, value)#1
word p; p = mmgr(HEAP_ALLOC, TYPE_MODIFIER)
p=>s_name = mmgr(HEAP_INTERN, name)
p=>w_modValue = value
return p
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Recalculate player's armor score based on their currently equipped armor
export def calcPlayerArmor(pl)#0
pl->b_armor = sum(pl=>p_items, &(p) p->t_type == TYPE_ARMOR and p->b_flags & ITEM_FLAG_EQUIP, &(p) p->b_armorValue)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Add gold, return the amount
export def addGold(amount)#1
if amount <= 0; return 0; fin
amount = min(amount, GOLD_MAX - global=>w_gold)
global=>w_gold = global=>w_gold + amount
return amount
end
// Pay out gold, return the amount
export def payGold(amount)#1
if amount > global=>w_gold
return 0
fin
global=>w_gold = global=>w_gold - amount
return amount
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def scanForNamedObj(list, name)#1
// We don't use lambda here, to preserve context of outer lambdas
while list
if streqi(list=>s_name, name); break; fin
list = list=>p_nextObj
loop
return list
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def createStruct(moduleID, creationFuncNum)#1
word p_module, funcTbl, func, p_thing
// Unload textures to make room for the module (also flips to page 1 if needed)
textureControl(FALSE)
// Load the module that is capable of creating the thing
mmgr(START_LOAD, 1) // code is in partition 1
p_module = mmgr(QUEUE_LOAD, moduleID<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
// Figure out which creation function to call there, and create the thing
funcTbl = p_module()
func = *(funcTbl + creationFuncNum)
p_thing = func() // full
// Finished with the module now.
mmgr(FREE_MEMORY, p_module)
return p_thing
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def roomInPack(p_player)#1
// Note: initial pack size is established by gen_players.pla, by code generated in PackPartitions.
return countList(p_player=>p_items) < p_player->b_packSize
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def partyHasItem(itemName)#1
ctx = itemName; return first(global=>p_players, &(p) scanForNamedObj(p=>p_items, ctx))
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def createItem(itemNum)#1
word pItem
if itemNum <= NUM_WEAPONS
pItem = createStruct(MOD_GEN_WEAPONS, (itemNum-1)<<1)
elsif itemNum <= NUM_WEAPONS+NUM_ARMORS
pItem = createStruct(MOD_GEN_ARMORS, (itemNum-NUM_WEAPONS-1)<<1)
else
pItem = createStruct(MOD_GEN_MISC_ITEMS, (itemNum-NUM_WEAPONS-NUM_ARMORS-1)<<1)
if pItem->t_type == TYPE_FANCY_ITEM
pItem=>w_count = pItem=>w_storeAmount
fin
fin
return pItem
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Returns: 0 = dupe
// -1 = no room
// 1 = added normally
// 2 = override-added quest item
// 3 = combined with existing stackable
export def fullAddItem(pItem, doit)#1
word pPlayer, pComp, addToPlayer
pPlayer = global=>p_players
addToPlayer = NULL
while pPlayer
pComp = scanForNamedObj(pPlayer=>p_items, pItem=>s_name)
if pComp
if pComp->t_type == TYPE_FANCY_ITEM and pComp=>w_count > 0
pComp=>w_count = min(30000, pComp=>w_count + pItem=>w_count)
return 3 // combined
fin
if !pComp=>w_price
return 0 // dupe quest item
fin
fin
if !addToPlayer and roomInPack(pPlayer); addToPlayer = pPlayer; fin
pPlayer = pPlayer=>p_nextObj
loop
if addToPlayer
if doit; addToList(@addToPlayer=>p_items, pItem); fin
return 1
fin
if pItem=>w_price
return -1 // no room, and item has a price so it's not a quest item
fin
// Override: quest items allowed to exceed pack limitations
if doit; addToList(@global=>p_players=>p_items, pItem); fin
return 2
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def giveItemToParty(p_item, displayFunc)#0
when fullAddItem(p_item, TRUE)
is 0
displayFunc("Duplicate item.\n")#0
beep
break
is -1
displayFunc(sprintf1("\nYou have no room; %s dropped.\n", p_item=>s_name))#0
beep
break
otherwise
heapCollect
wend
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Add XP to one player, leveling them up as appropriate
def addXP(player, val)#0
word n
if val < 0; return; fin
// Add the value plus an intelligence-based bonus
player=>w_curXP = player=>w_curXP + val + addPercent(val, 10 * player->b_intelligence)
// Enforce cap on number of points to stay within limit of 15 bits
if player=>w_curXP < 0 or player=>w_curXP >= 32760 // goes neg if wrapped around
player=>w_curXP = 32760
fin
while player=>w_curXP >= player=>w_nextXP and player=>w_curXP < 32761
// Level up!
player->b_level++
player->b_skillPoints = player->b_skillPoints + callGlobalFunc(GS_LEVEL_S_P, player->b_level, 0, 0)
player=>w_maxHealth = player=>w_maxHealth + ((player->b_stamina+1)>>1) + rollDice($2600) // roundup(stam/2) + 2d6
player=>w_health = player=>w_maxHealth // let's make leveling up an extra nice thing
needShowParty = TRUE
// Check XP for next level, and enforce level cap if any
n = callGlobalFunc(GS_LEVEL_X_P, player->b_level + 1, 0, 0)
if n > player=>w_nextXP
player=>w_nextXP = n
else
player=>w_nextXP = 32767 // XP cap reached
break
fin
loop
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Initialize XP (and skill pts) for newly created character (called by packer code)
export def initPlayerXP(player)#0
player->b_skillPoints = callGlobalFunc(GS_LEVEL_S_P, player->b_level, 0, 0)
player=>w_curXP = callGlobalFunc(GS_LEVEL_X_P, player->b_level, 0, 0)
player=>w_nextXP = callGlobalFunc(GS_LEVEL_X_P, player->b_level + 1, 0, 0)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def equipItem(item)#0
if item
item->b_flags = item->b_flags | ITEM_FLAG_EQUIP
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Arm newly created player (or NPC) with first weapon and all armor.
export def girdPlayer(player)#0
equipItem(first(player=>p_items, &(p) p->t_type == TYPE_WEAPON))
forSome(player=>p_items, &(p) p->t_type == TYPE_ARMOR, @equipItem)
calcPlayerArmor(player)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def addPlayerToParty(playerFuncNum, displayFunc)#0
word p_player
if countList(global=>p_players) == MAX_PARTY
displayFunc("Party too large.\n")#0
beep
return
fin
// Create the player (NPC flag will be established inside the creation func)
p_player = createStruct(MOD_GEN_PLAYERS, playerFuncNum)
// Add if not dupe
if !scanForNamedObj(global=>p_players, p_player=>s_name)
addToList(@global=>p_players, p_player)
adjustPartyStat(@S_XP, 0) // to set initial skill pts
heapCollect
needShowParty = TRUE
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def removeNamed(name, pList)#1
word p_thing
p_thing = scanForNamedObj(*pList, name)
if 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
return TRUE
fin
return FALSE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def takeItemFromParty(itemName)#0
ctx = itemName; first(global=>p_players, &(p) removeNamed(ctx, @p=>p_items))
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def removePlayerFromParty(playerName)#0
removeNamed(playerName, @global=>p_players)
needShowParty = TRUE
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def partyHasPlayer(playerName)#1
return scanForNamedObj(global=>p_players, playerName)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def getStat(player, statName)#1
word pSkill
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_TIME); return global->b_hour
is streqi(statName, @S_XP); return player=>w_curXP
is streqi(statName, @S_SP); return player->b_skillPoints
is streqi(statName, @S_PACK_SIZE); return player->b_packSize
is streqi(statName, @S_BANK_BAL); return global=>w_bankBal
wend
pSkill = scanForNamedObj(player=>p_skills, statName)
if pSkill; return pSkill=>w_modValue; fin
puts(statName); return fatal("getStat")
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Return an appropriate pronoun for the given gender (M/F/N...)
export def hisHerTheir(gender)#1
if gender == 'M'; return @S_HIS; fin
if gender == 'F'; return @S_HER; fin
return @S_THEIR
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def clampByte(val)#1
return max(0, min(255, val))
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def setHour(val) #0
val = val % 24 // clamp to range 0..23
if val <> global->b_hour or global->b_minute
if global->b_hour >= val; val = val + 24; fin // to ensure positive difference
advTime(val - global->b_hour - 1, 59 - global->b_minute, 60 - global->b_second)
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Adjust the given stat for every (living) player in the party
export def adjustPartyStat(statName, val)#0
word p_player
when 1
is streqi(statName, @S_GOLD); global=>w_gold = max(0, global=>w_gold + val); needShowParty = TRUE; break
is streqi(statName, @S_TIME); setHour(global->b_hour + val); break;
is streqi(statName, @S_BANK_BAL); global=>w_bankBal = global=>w_bankBal + val; break
otherwise
p_player = global=>p_players
while p_player
if p_player=>w_health > 0
setStat(p_player, statName, getStat(p_player, statName) + val)
fin
p_player = p_player=>p_nextObj
loop
wend
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Get the max value of the stat for any (living) character in the party.
// Except if a particular player is using a skill or item in which case it's their stat only.
export def getStatInContext(statName)#1
word p_player, val
val = 0
when 1
is streqi(statName, @S_GOLD); return global=>w_gold
is streqi(statName, @S_TIME); return global->b_hour
is streqi(statName, @S_BANK_BAL); return global=>w_bankBal
is playerUsing <> NULL; return getStat(playerUsing, statName)
otherwise
p_player = global=>p_players
while p_player
val = max(val, getStat(p_player, statName))
p_player = p_player=>p_nextObj
loop
wend
return val
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def setStat(player, statName, val)#0
word pSkill
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_TIME); setHour(val); break;
is streqi(statName, @S_XP); addXP(player, val - player=>w_curXP); needShowParty = TRUE; break
is streqi(statName, @S_SP); player->b_skillPoints = clampByte(max(0, val)); needShowParty = TRUE; break
is streqi(statName, @S_PACK_SIZE); player->b_packSize = clampByte(max(player->b_packSize, val)); break
is streqi(statName, @S_BANK_BAL); global=>w_bankBal = val; break
otherwise
pSkill = scanForNamedObj(player=>p_skills, statName)
if pSkill
pSkill=>w_modValue = max(0, val)
else
puts(statName); fatal("setStat")
fin
wend
end
///////////////////////////////////////////////////////////////////////////////////////////////////
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(flagNum)#1
byte byteNum, mask
byteNum = flagNum >> 3
mask = 1<<(flagNum & 7)
return global->ba_gameFlags[byteNum] & mask
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def setStoryMode(enable)#0
// Story mode only exists on 800K or hard drive builds, and not the disk-limited ones.
if !isFloppyVer and !diskLimit
storyMode = enable
if enable
loadEngine(MOD_STORY)
curEngine()=>story_mode(TRUE, preEnginePortraitNum)
needRender = FALSE
frameLoaded = 0 // since we just destroyed it
textDrawn = FALSE
textClearCountdown = 0
else
curEngine()=>story_mode(FALSE, 0)
preEnginePortraitNum = 0 // so we don't restore it
returnFromEngine(TRUE)
fin
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def displayStory(storyNum)#0
curEngine()=>story_display(storyNum)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def benchPlayer()#0
loadEngine(MOD_PARTY)=>party_benchPlayer()
returnFromEngine(TRUE)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def unbenchPlayer()#0
loadEngine(MOD_PARTY)=>party_unbenchPlayer()
returnFromEngine(TRUE)
end
///////////////////////////////////////////////////////////////////////////////////////////////////
export def buySell(storeCode, profitRatio)#0
if storeCode
loadEngine(MOD_STORE)=>store_buyFromStore(storeCode, profitRatio)
else
loadEngine(MOD_STORE)=>store_sellToStore(profitRatio)
fin
returnFromEngine(FALSE) // no render, we're mid-script
end
///////////////////////////////////////////////////////////////////////////////////////////////////
def startGame(firstTime, ask)#0
word p_module
typeHash = hashBuffer(@typeTbl_TGlobal, @typeTbls - @typeTbl_TGlobal) ^ HEAP_BOTTOM
// Create a new game or load an existing one
mmgr(START_LOAD, 1) // code is in partition 1
if firstTime
// Temporarily reserve a big chunk of aux mem to keep diskops bytecode out
auxMmgr(SET_MEM_TARGET, $800) // well above where resource index loads at startup
auxMmgr(REQUEST_MEMORY, $7F00) // plenty of room for resource index + temp expander
// Reserve space for the small-object heap
mmgr(SET_MEM_TARGET, HEAP_BOTTOM)
mmgr(REQUEST_MEMORY, HEAP_SIZE)
mmgr(LOCK_MEMORY, HEAP_BOTTOM)
fin
p_module = mmgr(QUEUE_LOAD, MOD_DISKOPS<<8 | RES_TYPE_MODULE)
mmgr(FINISH_LOAD, 0)
q_playerDeath = FALSE
if firstTime; p_module()=>diskops_startup(); fin
if p_module()=>diskops_newOrLoadGame(ask)
mapIs3D = q_mapIs3D
mapNum = q_mapNum
q_mapNum = 0
initMap(q_x, q_y, q_dir)
saveGame()
setTextCountdown
else
q_mapNum = 0
restoreMapPos()
fin
end
///////////////////////////////////////////////////////////////////////////////////////////////////
// Main code.
//
scriptDisplayStr(@_scriptDisplayStr) // 1-time init
startGame(TRUE, TRUE) // first time init; ask whether new or load
kbdLoop()
done