diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..15918b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*.{py,json,hy,yaml}] +charset = utf-8 +indent_style = spaces +insert_final_newline = true +indent_size = 4 + diff --git a/.kateconfig b/.kateconfig new file mode 100644 index 0000000..d116d72 --- /dev/null +++ b/.kateconfig @@ -0,0 +1,3 @@ + + +kate: indent-pasted-text false; indent-width 4; space-indent true; diff --git a/asciifarm/client/commandhandler.py b/asciifarm/client/commandhandler.py index ca72b68..f2e94d1 100644 --- a/asciifarm/client/commandhandler.py +++ b/asciifarm/client/commandhandler.py @@ -95,40 +95,40 @@ class CommandHandler: self.client.inputHandler.startTyping(startText) def selectWidget(self, value, relative=False, modular=False): - self.client.display.getWidget("switch").select(value, relative, modular) + self.client.display.selectMenu(value, relative, modular) def selectItem(self, value, relative=False, modular=False): - self.client.display.getWidget("switch").getSelectedItem().getImpl().select(value, relative, modular) + self.client.display.selectItem(None, value, relative, modular) - def actWithSelected(self, action, widget): - self.input([action, self.client.display.getWidget(widget).getSelected()]) + def actWithSelected(self, action, menu): + self.input([action, self.client.display.getSelectedItem(menu).getSelected()]) def useSelected(self): - widget = self.client.display.getWidget("switch").getSelectedItem() - selected = widget.getImpl().getSelected() - if widget.name in ("inventory", "equipment"): + menu = self.client.display.getSelectedMenu() + selected = self.client.display.getSelectedItem(menu) + if menu in ("inventory", "equipment"): action = "use" - elif widget.name == "ground": + elif menu == "ground": action = "interact", else: return self.input([action, selected]) def unUseSelected(self): - widget = self.client.display.getWidget("switch").getSelectedItem() - selected = widget.getImpl().getSelected() - if widget.name == "inventory": + menu = self.client.display.getSelectedMenu() + selected = self.client.display.getSelectedItem(menu) + if menu == "inventory": action = "drop" - elif widget.name == "equipment": + elif menu == "equipment": action = "unequip" else: return self.input([action, selected]) def takeSelected(self): - widget = self.client.display.getWidget("switch").getSelectedItem() - selected = widget.getImpl().getSelected() - if widget.name == "ground": + menu = self.client.display.getSelectedMenu() + selected = self.client.display.getSelectedItem(menu) + if menu == "ground": action = "take" else: return diff --git a/asciifarm/client/display.py b/asciifarm/client/display.py new file mode 100644 index 0000000..f138382 --- /dev/null +++ b/asciifarm/client/display.py @@ -0,0 +1,174 @@ + + + +import os +from ratuil.layout import Layout +from ratuil.bufferedscreen import BufferedScreen as Screen +#from ratuil.screen import Screen +from ratuil.textstyle import TextStyle +from asciifarm.common.utils import get +from .listselector import ListSelector + + +SIDEWIDTH = 20 + +ALPHABET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + +class Display: + + def __init__(self, charMap): + + self.characters = {} + + def parseSprite(sprite): + if isinstance(sprite, str): + return (sprite, None, None) + char = get(sprite, 0, " ") + fg = get(sprite, 1) + bg = get(sprite, 2) + return (char, fg, bg) + for name, sprite in charMap["mapping"].items(): + vals = parseSprite(sprite) + if vals: + self.characters[name] = vals + + for name, colours in charMap.get("writable", {}).items(): + fg = get(colours, 0) + bg = get(colours, 1) + for i in range(min(len(ALPHABET), len(charMap.get("alphabet", [])))): + self.characters[name + '-' + ALPHABET[i]] = (charMap["alphabet"][i], fg, bg) + + self.defaultChar = parseSprite(charMap.get("default", "?")) + + self.messageColours = charMap.get("msgcolours", {}) + + fname = os.path.join(os.path.dirname(__file__), "layout.xml") + self.layout = Layout.from_xml_file(fname) + self.layout.get("field").set_char_size(charMap.get("charwidth", 1)) + + self.screen = Screen() + self.screen.clear() + + self.layout.set_target(self.screen) + self.layout.update() + + + + # temporary, until these have a better place + self.inventory = ListSelector(self.getWidget("inventory")) + self.inventory._debug_name = "inventory" + self.equipment = ListSelector(self.getWidget("equipment")) + self.equipment._debug_name = "equipment" + self.ground = ListSelector(self.getWidget("ground")) + self.ground._debug_name = "ground" + self.switch = ListSelector(self.getWidget("switchtitles")) + self.switch._debug_name = "switch" + + self.switch.setItems(["inventory", "equipment", "ground"]) + self.menus = { + "inventory": self.inventory, + "equipment": self.equipment, + "ground": self.ground + } + + self.layout.get("switch").select(0) + + + def getWidget(self, name): + return self.layout.get(name) + + def resizeField(self, size): + self.getWidget("field").set_size(*size) + + def drawFieldCells(self, cells): + field = self.getWidget("field") + for cell in cells: + (x, y), spriteNames = cell + if not len(spriteNames): + char, fg, bg = self.getChar(' ') + else: + char, fg, bg = self.getChar(spriteNames[0]) + for spriteName in spriteNames[1:]: + if bg is not None: + break + _char, _fg, bg = self.getChar(spriteName) + field.change_cell(x, y, char, TextStyle(fg, bg)) + + + def setFieldCenter(self, pos): + self.getWidget("field").set_center(*pos) + + def setHealth(self, health, maxHealth): + if health is None: + health = 0 + if maxHealth is None: + maxHealth = 0 + self.getWidget("health").set_total(maxHealth) + self.getWidget("health").set_filled(health) + + + def showInfo(self, infostring): + self.getWidget("info").set_text(infostring) + + def selectMenu(self, *args, **kwargs): + self.switch.select(*args, **kwargs) + self.layout.get("switch").select(self.getSelectedMenu()) + + def getSelectedMenu(self): + return self.switch.getSelectedItem() + + def getSelectedItem(self, menu=None): + return self._getMenu(menu).getSelected() + + def selectItem(self, menu=None, *args, **kwargs): + self._getMenu(menu).select(*args, **kwargs) + + def _getMenu(self, name=None): + if name is None: + name = self.getSelectedMenu() + name = name.casefold() + return self.menus[name] + + def setInventory(self, items): + self.inventory.setItems(items) + + + def setEquipment(self, slots): + self.equipment.setItems( + sorted([ + slot + ": " + (item if item else "") + for slot, item in slots.items() + ]) + ) + + def setGround(self, items): + self.ground.setItems(items) + + + def addMessage(self, message, msgtype=None): + if msgtype is not None: + style = TextStyle(*self.messageColours.get(msgtype, (7,0))) + else: + style = None + self.getWidget("msg").add_message(message, style) + + def log(self, message): + self.addMessage(str(message)) + + def scrollBack(self, amount, relative=True): + self.getWidget("msg").scroll(amount, relative) + + def setInputString(self, string, cursor): + self.getWidget("textinput").set_text(string, cursor) + + def update(self): + self.layout.update() + self.screen.update() + + def getChar(self, sprite): + """This returns the character belonging to some spritename. This does not read a character""" + return self.characters.get(sprite, self.defaultChar) + + def update_size(self): + self.screen.reset() + diff --git a/asciifarm/client/display/__init__.py b/asciifarm/client/display/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/asciifarm/client/display/colours.py b/asciifarm/client/display/colours.py deleted file mode 100644 index 1403300..0000000 --- a/asciifarm/client/display/colours.py +++ /dev/null @@ -1,32 +0,0 @@ - -import curses - -class Colours: - - def __init__(self): - - self.colours = min(curses.COLORS, 16) - self.pairs = self.colours*self.colours - - curses.use_default_colors() - for i in range(0, self.pairs): - curses.init_pair(i, i%self.colours, i//self.colours) - - def get(self, fg=0, bg=0): - if self.colours == 16: - return curses.color_pair(fg + bg*self.colours) - elif self.colours == 8: - dfg = fg % 8 - dbg = bg % 8 - if bg == 8: - dbg = 7 - if fg == 8: - dfg = 7 - colour = curses.color_pair(dfg + dbg*self.colours) - if fg >= 8 and bg < 8: - colour |= curses.A_BOLD - elif fg < 8 and bg >= 8: - colour |= curses.A_DIM - return colour - else: - return curses.color_pair(0) diff --git a/asciifarm/client/display/display.py b/asciifarm/client/display/display.py deleted file mode 100644 index edc3f95..0000000 --- a/asciifarm/client/display/display.py +++ /dev/null @@ -1,154 +0,0 @@ - -import curses - -from .field import Field -from .info import Info -from .health import Health -from .inventory import Inventory -from .screen import Screen -from .colours import Colours -from .messages import Messages -from .switcher import Switcher -from .textinput import TextInput -from .widget import Widget - -from asciifarm.common.utils import get - - -SIDEWIDTH = 20 - -ALPHABET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" - -class Display: - - def __init__(self, stdscr, charMap, colours=False): - - if colours and curses.has_colors and curses.COLORS > 1: - self.colours = Colours() - else: - self.colours = None - self.characters = {} - - def parseSprite(sprite): - if isinstance(sprite, str): - return (sprite, None, None) - char = get(sprite, 0, " ") - fg = get(sprite, 1) - bg = get(sprite, 2) - return (char, fg, bg) - for name, sprite in charMap["mapping"].items(): - vals = parseSprite(sprite) - if vals: - self.characters[name] = vals - - for name, colours in charMap.get("writable", {}).items(): - fg = get(colours, 0) - bg = get(colours, 1) - for i in range(min(len(ALPHABET), len(charMap.get("alphabet", [])))): - self.characters[name + '-' + ALPHABET[i]] = (charMap["alphabet"][i], fg, bg) - - self.defaultChar = parseSprite(charMap.get("default", "?")) - screen = Screen(self, stdscr, self.colours) - self.screen = screen - - self.widgets = {} - - self.addWidget(Field((1, 1), charMap.get("charwidth", 1), self.colours), "field") - self.addWidget(Info(), "info") - self.addWidget(Health( - charMap.get("healthfull", ("@",7, 2)), - charMap.get("healthempty", ("-",7, 1)) - ), - "health") - self.addWidget(Inventory("Inventory"), "inventory") - self.addWidget(Inventory("Ground"), "ground") - self.addWidget(Inventory("Equipment"), "equipment") - - - switcher = Switcher([self.widgets["ground"], self.widgets["inventory"], self.widgets["equipment"]], 1) - self.addWidget(switcher, "switch") - self.addWidget(Messages(charMap.get("msgcolours", {})), "msg") - self.addWidget(TextInput(), "textinput") - - self.forced = False - - def addWidget(self, w, name, winname=None): - if not winname: - winname = name - widget = Widget(w, name) - self.widgets[name] = widget - widget.setWin(winname, self.screen) - - def getWidget(self, name): - if name in self.widgets: - return self.widgets[name].getImpl() - else: - return None - - def resizeField(self, size): - self.getWidget("field").resize(*size) - self.forced = True - - def drawFieldCells(self, cells): - field = self.getWidget("field") - for cell in cells: - (x, y), spriteNames = cell - sprites = [self.getChar(spriteName) for spriteName in spriteNames] - if not len(sprites): - sprites = [self.getChar(" ")] - field.changeCell(x, y, sprites) - - - def setFieldCenter(self, pos): - self.getWidget("field").setCenter(pos) - - def setHealth(self, health, maxHealth): - self.getWidget("health").setHealth(health, maxHealth) - - - def showInfo(self, infostring): - self.getWidget("info").showString(infostring) - - - def setInventory(self, items): - self.getWidget("inventory").setInventory(items) - - - def setEquipment(self, slots): - self.getWidget("equipment").setInventory( - sorted([ - slot + ": " + (item if item else "") - for slot, item in slots.items() - ]) - ) - - def setGround(self, items): - self.getWidget("ground").setInventory(items) - - - def addMessage(self, message, type): - self.getWidget("msg").addMessage(message, type) - - def scrollBack(self, amount, relative=True): - self.getWidget("msg").scroll(amount, relative) - - def getChar(self, sprite): - """This returns the character belonging to some spritename. This does not read a character""" - return self.characters.get(sprite, self.defaultChar) - - def setInputString(self, string, cursor): - self.getWidget("textinput").setText(string, cursor) - - def update(self): - changed = False - for widget in self.widgets.values(): - if self.forced or widget.isChanged(): - widget.update() - changed = True - if changed: - self.screen.update() - self.forced = False - - def forceUpdate(self): - self.forced = True - diff --git a/asciifarm/client/display/field.py b/asciifarm/client/display/field.py deleted file mode 100644 index 64c4bfc..0000000 --- a/asciifarm/client/display/field.py +++ /dev/null @@ -1,70 +0,0 @@ - -import curses -from .widimp import WidImp - -class Field(WidImp): - - - def __init__(self, size=(1,1), charSize=1, colours=False): - self.pad = curses.newpad(size[1]+1, (size[0]+1)*charSize) - self.size = size - self.charSize = charSize - self.center = (0, 0) - self.colours = colours - self.changed = False - self.redraw = False - - def resize(self, width, height): - self.size = (width, height) - self.pad.resize(height+1, width*self.charSize) - self.redraw = True - self.change() - - def changeCell(self, x, y, sprites): - """ sprites must always have at least one element """ - char, colour, bgcolour = sprites[0] - if bgcolour is None: - for (ch, co, bg) in sprites: - if bg is not None: - bgcolour = bg - break - else: - bgcolour = 0 - if colour is not None and self.colours: - self.pad.addstr(y, x*self.charSize, " "*self.charSize, self.colours.get(7, 0)) - self.pad.addstr(y, x*self.charSize, char, self.colours.get(colour, bgcolour)) - else: - self.pad.addstr(y, x*self.charSize, char) - self.change() - - def setCenter(self, pos): - self.center = pos - self.change() - - def getWidth(self): - return self.size[0]*self.charSize - - def getHeight(self): - return self.size[1] - - def _roundWidth(self, x): - return x // self.charSize * self.charSize - - def update(self, win): - if self.redraw: - win.erase() - win.noutrefresh() - self.redraw = False - width, height = win.getSize() - x, y = win.getPos() - xmax = x + width - ymax = y + height - self.pad.noutrefresh( - max(0, min(self.getHeight()-height, self.center[1] - int(height/2))), - max(0, min( - self._roundWidth(self.getWidth()-width), - self._roundWidth(self.center[0]*self.charSize - int(width/2)))), - y, - x + max(0, (width - self.getWidth()) // 2), - ymax, - xmax) diff --git a/asciifarm/client/display/health.py b/asciifarm/client/display/health.py deleted file mode 100644 index d42edbf..0000000 --- a/asciifarm/client/display/health.py +++ /dev/null @@ -1,27 +0,0 @@ - - -from .widimp import WidImp - -class Health(WidImp): - - def __init__(self, char=None, emptyChar=None): - self.char = char or ('@',7,0) - self.emptyChar = emptyChar or ('-',7,0) - self.changed = False - self.health = 0 - self.maxHealth = 0 - - def setHealth(self, health, maxHealth): - self.health = health or 0 - self.maxHealth = maxHealth or 0 - self.change() - - def update(self, win): - width, height = win.getSize() - width -= 1 - barEnd = round(self.health/self.maxHealth * width) if self.maxHealth > 0 else 0 - win.erase() - win.addLine((0,0),"Health: {}/{}".format(self.health, self.maxHealth)[:width]) - win.addLine((0, 1), self.char[0]*barEnd, self.char[1:]) - win.addLine((barEnd, 1), self.emptyChar[0]*(width-barEnd), self.emptyChar[1:]) - win.noutrefresh() diff --git a/asciifarm/client/display/info.py b/asciifarm/client/display/info.py deleted file mode 100644 index 599627c..0000000 --- a/asciifarm/client/display/info.py +++ /dev/null @@ -1,24 +0,0 @@ - -from .widimp import WidImp - -class Info(WidImp): - - def __init__(self): - self.changed = False - self.lines = [] - self.lastString = None - - def showString(self, string): - if string == self.lastString: - return - self.lines = string.split('\n') - self.change() - self.lastString = string - - def update(self, win): - width, height = win.getSize() - lines = [line[:width-1] for line in self.lines][:height] - win.erase() - for i, line in enumerate(lines): - win.addLine((0, i), line) - win.noutrefresh() diff --git a/asciifarm/client/display/inventory.py b/asciifarm/client/display/inventory.py deleted file mode 100644 index 48bf41e..0000000 --- a/asciifarm/client/display/inventory.py +++ /dev/null @@ -1,73 +0,0 @@ - -from asciifarm.common import utils - -from .widimp import WidImp - -class Inventory(WidImp): - - def __init__(self, title, titlebar="{}:", selectorChar="*"): - self.title = title - self.titlebar = titlebar - self.selectorChar = selectorChar - self.items = [] - self.selector = 0 - - def getSelected(self): - return self.selector - - def select(self, value, relative=False, modular=False): - invLen = len(self.items) - if relative: - value += self.selector - if modular and invLen: - value %= invLen - if value < 0: - value = 0 - if value >= invLen: - value = invLen-1 - if value in range(invLen): - self.doSelect(value) - - def doSelect(self, value): - self.selector = value - self.change() - - def setInventory(self, items): - self.items = items - self.selector = utils.clamp(self.selector, 0, len(items)-1) - self.change() - - def getItem(self, num): - return self.items[num] - - def getSelectedItem(self): - return self.getItem(self.getSelected()) - - def setTitle(self, title): - self.title = title - - def getNumItems(self): - return len(self.items) - - def itemName(self, item): - return item - - def update(self, win): - - width, height = win.getSize() - height -= 1 - selected = self.selector - start = min(selected - height//2, len(self.items)-height) - start = max(start, 0) - end = start + height - win.erase() - win.addLine((0,0), (self.titlebar.format(self.title))[:width]) - for i, item in enumerate(self.items[start:end]): - if i + start == selected: - win.addLine((0, i+1), self.selectorChar) - win.addLine((1, i+1), self.itemName(item)) - if end < len(self.items): - win.addLine((width-1, height), "+") - if start > 0: - win.addLine((width-1, 1), "-") - win.noutrefresh() diff --git a/asciifarm/client/display/messages.py b/asciifarm/client/display/messages.py deleted file mode 100644 index d551cc2..0000000 --- a/asciifarm/client/display/messages.py +++ /dev/null @@ -1,56 +0,0 @@ - -import textwrap - -from .widimp import WidImp - -class Messages(WidImp): - - def __init__(self, colours): - self.changed = False - self.messages = [] - self.scrolledBack = 0 - self.colours = colours - - def addMessage(self, message, type=None): - self.messages.append([message, type]) - if self.scrolledBack: - self.scrolledBack += 1 - self.change() - - def scroll(self, amount, relative=True): - if relative: - self.scrolledBack += amount - else: - self.scrolledBack = amount - self.scrolledBack = max(self.scrolledBack, 0) - self.change() - - def update(self, win): - width, height = win.getSize() - if height < 1: - return - lines = [] - messages = self.messages - for message, type in messages: - colour = self.colours.get(type, (7,0)) - for line in textwrap.wrap(message, width): - lines.append((line, colour)) - self.scrolledBack = max(min(self.scrolledBack, len(lines)-height), 0) - moreDown = False - if self.scrolledBack > 0: - lines = lines[:-self.scrolledBack] - moreDown = True - moreUp = False - if len(lines) > height: - moreUp = True - lines = lines[len(lines)-height:] - elif len(lines) < height: - lines = (height-len(lines)) * [("",)] + lines - win.erase() - for i, line in enumerate(lines): - win.addLine((0,i), *line) - if moreUp: - win.addLine((width-1, 0), '-') - if moreDown: - win.addLine((width-1, height-1), '+') - win.noutrefresh() diff --git a/asciifarm/client/display/screen.py b/asciifarm/client/display/screen.py deleted file mode 100644 index 666d3bc..0000000 --- a/asciifarm/client/display/screen.py +++ /dev/null @@ -1,95 +0,0 @@ - -import curses -from asciifarm.common.utils import clamp -from .window import Window - -import signal - -class Screen: - - def __init__(self, display, stdscr, colours): - self.display = display - try: - curses.curs_set(0) - self.cursorSet = False - except curses.error: - # Not all terminals support this functionality. - # When the error is ignored the screen will look a little uglier, - # A cursor will move around, but that's not terrible - # So in order to keep the game as accesible as possible to everyone, it should be safe to ignore the error. - self.cursorSet = True - # It is probably possible to make sure the cursor is only in a corner of the screen - # but I can't figure out how. - # it seems to ignore all my move commands unless I press a key - # I give up - self.stdscr = stdscr - self.colours = colours - self.setWins() - signal.signal(signal.SIGWINCH, self.updateSize) - - def _limitHeight(self, h, y): - return min(h + y, self.height) - y - - def setWins(self): - height, width = self.height, self.width = self.stdscr.getmaxyx() - - sideW = 20 - sideX = width-sideW - - msgH = clamp(height // 5, 3, 5) - msgY = height - msgH-1 - inputH = 1 - inputY = msgY + msgH - - healthY = 0 - healthH = self._limitHeight(2, healthY) - indexY = healthY + healthH - indexH = self._limitHeight(4, indexY) - listY = indexY + indexH + 1 - listH = self._limitHeight(12, listY) - infoY = listY + listH - infoH = self._limitHeight(20, infoY) - - lists = self.makeWin(sideX, listY, sideW, listH) - - self.windows = { - "field": self.makeWin(0, 0, sideX - 1, msgY), - "msg": self.makeWin(0, msgY, sideX - 1, msgH), - "textinput": self.makeWin(0, inputY, sideX - 1, inputH), - - "health": self.makeWin(sideX, healthY, sideW, healthH), - "switch": self.makeWin(sideX, indexY, sideW, indexH), - "ground": lists, - "inventory": lists, - "equipment": lists, - "info": self.makeWin(sideX, infoY, sideW, infoH) - } - - - def makeWin(self, x, y, width, height): - if width < 1 or height < 1: - win = None - else: - win = curses.newwin(height, width, y, x) - return Window(win, self.colours) - - def getWin(self, name): - return self.windows.get(name, None) - - def updateSize(self, *args): - curses.endwin() - curses.initscr() - self.setWins() - self.stdscr.clear() - self.display.forceUpdate() - - def update(self): - curses.doupdate() - - def getWidth(self): - return self.width - - def getHeight(self): - return self.height - - diff --git a/asciifarm/client/display/switcher.py b/asciifarm/client/display/switcher.py deleted file mode 100644 index 035aea3..0000000 --- a/asciifarm/client/display/switcher.py +++ /dev/null @@ -1,29 +0,0 @@ - - -from .inventory import Inventory - -class Switcher(Inventory): - """An area that can contain multiple widgets but only shows one at a time. - There is a function to switch between the displayed widgets. - """ - - def __init__(self, widgets, initial=0): - Inventory.__init__(self, "", "", "=") - self.setInventory(widgets) - - for wid in widgets: - wid.hidden = True - - self.select(initial) - - def doSelect(self, value): - self.getSelectedItem().hidden = True - self.selector = value - self.change() - newWid = self.getSelectedItem() - newWid.hidden = False - newWid.change() - - def itemName(self, item): - return item.getImpl().title - diff --git a/asciifarm/client/display/textinput.py b/asciifarm/client/display/textinput.py deleted file mode 100644 index 092386c..0000000 --- a/asciifarm/client/display/textinput.py +++ /dev/null @@ -1,22 +0,0 @@ - -import curses -from .widimp import WidImp - -class TextInput(WidImp): - - def __init__(self): - self.text = "" - self.cursor = -1 - - def setText(self, text, cursor): - self.text = text - self.cursor = cursor - self.change() - - def update(self, win): - width, height = win.getSize() - win.erase() - win.addLine((0, 0), self.text[:width]) - if self.cursor >= 0 and self.cursor <= len(self.text): - win.setAttr((min(self.cursor, width-1), 0), curses.A_REVERSE) - win.noutrefresh() diff --git a/asciifarm/client/display/widget.py b/asciifarm/client/display/widget.py deleted file mode 100644 index 31bb093..0000000 --- a/asciifarm/client/display/widget.py +++ /dev/null @@ -1,35 +0,0 @@ - -class Widget: - - - def __init__(self, impl, name=None): - self.impl = impl - - self.win = None - self.screen = None - self.changed = False - self.hidden = False - self.name = name - self.impl.setWidget(self) - - def setWin(self, win, screen): - self.win = win - self.screen = screen - - def getWin(self): - return self.win and self.screen and self.screen.getWin(self.win) - - def getImpl(self): - return self.impl - - def change(self): - self.changed = True - - def isChanged(self): - return self.changed - - def update(self): - if not self.getWin() or self.hidden: - return - self.impl.update(self.getWin()) - self.changed = False diff --git a/asciifarm/client/display/widimp.py b/asciifarm/client/display/widimp.py deleted file mode 100644 index 145aa6b..0000000 --- a/asciifarm/client/display/widimp.py +++ /dev/null @@ -1,18 +0,0 @@ - - -class WidImp: - - """widget implementation""" - - _widget = None - - def setWidget(self, widget): - self._widget = widget - self.change() - - def change(self): - if self._widget is not None: - self._widget.change() - - def update(self, win): - pass diff --git a/asciifarm/client/display/window.py b/asciifarm/client/display/window.py deleted file mode 100644 index d5c3945..0000000 --- a/asciifarm/client/display/window.py +++ /dev/null @@ -1,74 +0,0 @@ - -import curses - -class Window: - """ Small wrapper around curses windows """ - - def __init__(self, win, colours=None): - - self.win = win - self.colours = colours - - def getSize(self): - if not self.win: - return (0, 0) - height, width = self.win.getmaxyx() - return (width, height) - - def getPos(self): - if not self.win: - return (0, 0) - y, x = self.win.getparyx() - return (x, y) - - def addLine(self, pos, string, colour=(0,0)): - """Draw a string that does not contain newlines or characters with larger width - - long lines are cropped to fit in the window""" - - if not self.win: - return - x, y = pos - width, height = self.getSize() - string = string[:width-x] - drawLast = None - if self.colours: - self._addstr(y, x, string, self.colours.get(*colour)) - else: - self._addstr(y, x, string) - - - def _addstr(self, y, x, string, *args): - if not self.win: - return - width, height = self.getSize() - if y == height-1 and x+len(string) == width: - if len(string) > 1: - self.win.addstr(y, x, string[:-1], *args) - try: - self.win.addstr(height-1, width-1, string[-1], *args) - except curses.error: - # ncurses has a weird problem: - # it always raises an error when drawing to the last character in the window - # it draws first and then raises the error - # therefore to draw in the last place of the window the last character needs to be ingored - # other solutions might be possible, but are more hacky - pass - else: - self.win.addstr(y, x, string, *args) - - def erase(self): - if self.win: - self.win.erase() - - def noutrefresh(self): - if self.win: - self.win.noutrefresh() - - def setAttr(self, pos, attr, num=1): - if self.win: - x, y = pos - self.win.chgat(y, x, num, attr) - - - diff --git a/asciifarm/client/gameclient.py b/asciifarm/client/gameclient.py index 2bd5420..d9d0c89 100644 --- a/asciifarm/client/gameclient.py +++ b/asciifarm/client/gameclient.py @@ -1,4 +1,4 @@ -#! /usr/bin/python3 + import os import sys @@ -10,13 +10,14 @@ import argparse import string from queue import Queue +import ratuil.inputs from .inputhandler import InputHandler class Client: - def __init__(self, stdscr, display, name, connection, keybindings, logFile=None): - self.stdscr = stdscr + def __init__(self, display, name, connection, keybindings, logFile=None): + self.display = display self.name = name self.keepalive = True @@ -54,7 +55,7 @@ class Client: def getInput(self): while True: - key = self.stdscr.getch() + key = ratuil.inputs.get_key() self.queue.put(("input", key)) def close(self, msg=None): @@ -138,12 +139,19 @@ class Client: if action[0] == "message": self.update(action[1]) elif action[0] == "input": + if action[1] == "^C": + raise KeyboardInterrupt self.inputHandler.onInput(action[1]) elif action[0] == "error": raise action[1] + elif action[0] == "sigwinch": + self.display.update_size() else: raise Exception("invalid action in queue") + def onSigwinch(self, signum, frame): + self.queue.put(("sigwinch", (signum, frame))) + diff --git a/asciifarm/client/inputhandler.py b/asciifarm/client/inputhandler.py index 2d038d2..b2e4ac9 100644 --- a/asciifarm/client/inputhandler.py +++ b/asciifarm/client/inputhandler.py @@ -1,9 +1,9 @@ -import curses -import curses.ascii +import string from .commandhandler import CommandHandler, InvalidCommandException -from .keynames import nameFromKey + +import ratuil.inputs as inp class InputHandler: @@ -20,7 +20,7 @@ class InputHandler: def onInput(self, key): if not self.typing: - keyName = nameFromKey(key) + keyName = key if keyName in self.keybindings: self.commandHandler.execute(self.keybindings[keyName]) else: @@ -58,36 +58,36 @@ class InputHandler: self.client.display.setInputString(self.string, self.cursor if self.typing else -1) def addKey(self, key): - if curses.ascii.isprint(key): - self.string = self.string[:self.cursor] + chr(key) + self.string[self.cursor:] + if key in string.printable: + self.string = self.string[:self.cursor] + key + self.string[self.cursor:] self.cursor += 1 - elif key == curses.KEY_BACKSPACE or key == curses.ascii.BS or key == curses.ascii.DEL: + elif key == inp.BACKSPACE: self.string = self.string[:self.cursor-1] + self.string[self.cursor:] self.cursor = max(self.cursor - 1, 0) - elif key == curses.KEY_RIGHT: + elif key == inp.RIGHT: self.cursor = min(self.cursor + 1, len(self.string)) - elif key == curses.KEY_LEFT: + elif key == inp.LEFT: self.cursor = max(self.cursor - 1, 0) - elif key == curses.KEY_DC: + elif key == inp.DELETE: self.string = self.string[:self.cursor] + self.string[self.cursor+1:] - elif key == curses.KEY_HOME: + elif key == inp.HOME: self.cursor = 0 - elif key == curses.KEY_END: + elif key == inp.END: self.cursor = len(self.string) - elif key == curses.ascii.ESC or key == curses.KEY_DL: + elif key == inp.ESCAPE: # throw away entered string and go back to game self.typing = False self.string = "" self.cursor = 0 - elif key == curses.ascii.LF or key == curses.ascii.CR: + elif key == inp.ENTER: # process entered string and reset it message = self.string self.string = "" self.cursor = 0 self.typing = False self.processString(message) - elif key == curses.ascii.TAB: + elif key == "^I": # tab # return to game but keep entered string self.typing = False diff --git a/asciifarm/client/keynames.py b/asciifarm/client/keynames.py deleted file mode 100644 index bbb8009..0000000 --- a/asciifarm/client/keynames.py +++ /dev/null @@ -1,15 +0,0 @@ - -import curses - -prenamed = { - 10: "NEWLINE" -} - -def nameFromKey(key): - if key in prenamed: - return prenamed[key] - try: - keyname = curses.keyname(key) - except ValueError: - return None - return str(keyname, "utf-8") diff --git a/asciifarm/client/layout.xml b/asciifarm/client/layout.xml new file mode 100644 index 0000000..78337dc --- /dev/null +++ b/asciifarm/client/layout.xml @@ -0,0 +1,40 @@ + + + + + + + + Inventory: + + milk + eggs + bread + + + + Equipment: + + cotton underwear + cotton shirt + jeans + friendship bracelet + + + + Ground: + + concrete + + + + + + + hello + + Welcome to asciifarm + + + + diff --git a/asciifarm/client/listselector.py b/asciifarm/client/listselector.py new file mode 100644 index 0000000..b88b967 --- /dev/null +++ b/asciifarm/client/listselector.py @@ -0,0 +1,49 @@ + +from asciifarm.common import utils + + +class ListSelector: + + def __init__(self, widget): + self.widget = widget + self.items = [] + self.selector = 0 + + def getSelected(self): + return self.selector + + def select(self, value, relative=False, modular=False): + invLen = len(self.items) + if relative: + value += self.selector + if modular and invLen: + value %= invLen + if value < 0: + value = 0 + if value >= invLen: + value = invLen-1 + if value in range(invLen): + self.doSelect(value) + + def doSelect(self, value): + self.selector = value + self.widget.select(value) + + def setItems(self, items): + self.items = items + self.selector = utils.clamp(self.selector, 0, len(items)-1) + self.widget.set_items([self.itemName(item) for item in self.items]) + self.widget.select(self.selector) + + def getItem(self, num): + return self.items[num] + + def getSelectedItem(self): + return self.getItem(self.getSelected()) + + def getNumItems(self): + return len(self.items) + + def itemName(self, item): + return item + diff --git a/asciifarm/client/loaders.py b/asciifarm/client/loaders.py index 488ccf9..efdd1c0 100644 --- a/asciifarm/client/loaders.py +++ b/asciifarm/client/loaders.py @@ -61,8 +61,6 @@ def loadCharmap(name): writable = {} default = None charwidth = 1 - healthfull = None - healthempty = None alphabet = "" msgcolours = {} @@ -71,8 +69,6 @@ def loadCharmap(name): writable.update(template.get("writable", {})) default = template.get("default", default) charwidth = template.get("charwidth", charwidth) - healthfull = template.get("healthfull", healthfull) - healthempty = template.get("healthempty", healthempty) alphabet = template.get("alphabet", alphabet) msgcolours.update(template.get("msgcolours", {})) return { @@ -80,8 +76,6 @@ def loadCharmap(name): "writable": writable, "default": default, "charwidth": charwidth, - "healthfull": healthfull, - "healthempty": healthempty, "alphabet": alphabet, "msgcolours": msgcolours } diff --git a/asciifarm/client/main.py b/asciifarm/client/main.py index c00b592..d720477 100644 --- a/asciifarm/client/main.py +++ b/asciifarm/client/main.py @@ -1,13 +1,18 @@ #! /usr/bin/python3 -import curses + import json -import os -import getpass + import sys +import termios +import tty +import signal +#import os + from .connection import Connection from .gameclient import Client -from .display.display import Display +from .display import Display from .parseargs import parse_args +from ratuil.screen import Screen def main(argv=None): @@ -24,33 +29,19 @@ def main(argv=None): error = None closeMessage = None - os.environ.setdefault("ESCDELAY", "25") + #os.environ.setdefault("ESCDELAY", "25") + + fd = sys.stdin.fileno() + oldterm = termios.tcgetattr(fd) try: - # Initialize curses - stdscr = curses.initscr() + + tty.setraw(sys.stdin) + Screen.default.hide_cursor() - # Turn off echoing of keys, and enter cbreak mode, - # where no buffering is performed on keyboard input - curses.noecho() - curses.cbreak() - - # In keypad mode, escape sequences for special keys - # (like the cursor keys) will be interpreted and - # a special value like curses.KEY_LEFT will be returned - stdscr.keypad(1) - - # Start color, too. Harmless if the terminal doesn't have - # color; user can test with has_color() later on. The try/catch - # works around a minor bit of over-conscientiousness in the curses - # module -- the error return from C start_color() is ignorable. - try: - curses.start_color() - except: - pass - - display = Display(stdscr, characters, colours) - client = Client(stdscr, display, name, connection, keybindings, logfile) + display = Display(characters) + client = Client(display, name, connection, keybindings, logfile) + signal.signal(signal.SIGWINCH, client.onSigwinch) try: client.start() except KeyboardInterrupt: @@ -61,12 +52,9 @@ def main(argv=None): error = e closeMessage = client.closeMessage finally: - # Set everything back to normal - if 'stdscr' in locals(): - stdscr.keypad(0) - curses.echo() - curses.nocbreak() - curses.endwin() + ## Set everything back to normal + termios.tcsetattr(fd, termios.TCSADRAIN, oldterm) + Screen.default.finalize() if error is not None: diff --git a/asciifarm/keybindings/default.json b/asciifarm/keybindings/default.json index a816527..f8a5b25 100644 --- a/asciifarm/keybindings/default.json +++ b/asciifarm/keybindings/default.json @@ -4,10 +4,10 @@ "s": ["move", "south"], "d": ["move", "east"], "a": ["move", "west"], -"KEY_UP": ["move", "north"], -"KEY_DOWN": ["move", "south"], -"KEY_RIGHT": ["move", "east"], -"KEY_LEFT": ["move", "west"], +"up": ["move", "north"], +"down": ["move", "south"], +"right": ["move", "east"], +"left": ["move", "west"], "k": ["move", "north"], "j": ["move", "south"], "l": ["move", "east"], @@ -33,9 +33,9 @@ "D": ["input", ["attack", "east"]], "A": ["input", ["attack", "west"]], "t": ["runinput"], -"NEWLINE": ["runinput"], -"KEY_PPAGE": ["scrollchat", 1], -"KEY_NPAGE": ["scrollchat", -1], +"enter": ["runinput"], +"pageup": ["scrollchat", 1], +"pagedown": ["scrollchat", -1], "/": ["runinput", "/"] }, "help": "Controls:\n wasd or arrows:\n Move around\n e: Grab\n q: Drop/unequip\n selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n Q: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client"