diff --git a/assets/kenney_TD_MR_IP.png b/assets/kenney_TD_MR_IP.png index 600c8d9..5e1efc1 100644 Binary files a/assets/kenney_TD_MR_IP.png and b/assets/kenney_TD_MR_IP.png differ diff --git a/assets/sfx/splat1.ogg b/assets/sfx/splat1.ogg new file mode 100644 index 0000000..0d09909 Binary files /dev/null and b/assets/sfx/splat1.ogg differ diff --git a/assets/sfx/splat2.ogg b/assets/sfx/splat2.ogg new file mode 100644 index 0000000..5301e86 Binary files /dev/null and b/assets/sfx/splat2.ogg differ diff --git a/assets/sfx/splat3.ogg b/assets/sfx/splat3.ogg new file mode 100644 index 0000000..ed5dade Binary files /dev/null and b/assets/sfx/splat3.ogg differ diff --git a/assets/sfx/splat4.ogg b/assets/sfx/splat4.ogg new file mode 100644 index 0000000..d7e7b1b Binary files /dev/null and b/assets/sfx/splat4.ogg differ diff --git a/assets/sfx/splat5.ogg b/assets/sfx/splat5.ogg new file mode 100644 index 0000000..a77f465 Binary files /dev/null and b/assets/sfx/splat5.ogg differ diff --git a/assets/sfx/splat6.ogg b/assets/sfx/splat6.ogg new file mode 100644 index 0000000..f2d2c1f Binary files /dev/null and b/assets/sfx/splat6.ogg differ diff --git a/assets/sfx/splat7.ogg b/assets/sfx/splat7.ogg new file mode 100644 index 0000000..ea51d22 Binary files /dev/null and b/assets/sfx/splat7.ogg differ diff --git a/assets/sfx/splat8.ogg b/assets/sfx/splat8.ogg new file mode 100644 index 0000000..c23fdd8 Binary files /dev/null and b/assets/sfx/splat8.ogg differ diff --git a/assets/sfx/splat9.ogg b/assets/sfx/splat9.ogg new file mode 100644 index 0000000..a0e2d95 Binary files /dev/null and b/assets/sfx/splat9.ogg differ diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index eb4bfad..83a9dcb 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -28,7 +28,6 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) PyObject* PyTexture::pyObject() { - std::cout << "Find type" << std::endl; auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"); PyObject* obj = PyTexture::pynew(type, Py_None, Py_None); diff --git a/src/UICaption.cpp b/src/UICaption.cpp index d960c25..539ec38 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -249,7 +249,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) self->data->text.setPosition(pos_result->data); // check types for font, fill_color, outline_color - std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; + //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; if (font != NULL && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){ PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance"); return -1; diff --git a/src/scripts/cos_entities.py b/src/scripts/cos_entities.py index ed916f7..0643c00 100644 --- a/src/scripts/cos_entities.py +++ b/src/scripts/cos_entities.py @@ -1,4 +1,5 @@ import mcrfpy +import random #t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) #def iterable_entities(grid): @@ -37,7 +38,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu self._entity.sprite_number = value def __repr__(self): - return f"" + return f"<{self.__class__.__name__} ({self.draw_pos})>" def die(self): # ugly workaround! grid.entities isn't really iterable (segfaults) @@ -69,7 +70,9 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu for e in self.game.entities: if e is self: continue if e.draw_pos == (tx, ty): e.ev_enter(self) - + + def act(self): + pass def ev_enter(self, other): pass @@ -106,11 +109,92 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu #self.draw_pos = (tx, ty) self.do_move(tx, ty) +class Equippable: + def __init__(self, hands = 0, hp_healing = 0, damage = 0, defense = 0, zap_damage = 1, zap_cooldown = 10, sprite = 129): + self.hands = hands + self.hp_healing = hp_healing + self.damage = damage + self.defense = defense + self.zap_damage = zap_damage + self.zap_cooldown = zap_cooldown + self.zap_cooldown_remaining = 0 + self.sprite = self.sprite + self.quality = 0 + + def tick(self): + if self.zap_cooldown_remaining: + self.zap_cooldown_remaining -= 1 + if self.zap_cooldown_remaining < 0: self.zap_cooldown_remaining = 0 + + def __repr__(self): + cooldown_str = f'({self.zap_cooldown_remaining} rounds until ready)' + return f"" + + def classify(self): + categories = [] + if self.hands==0: + categories.append("consumable") + elif self.damage > 0: + categories.append(f"{self.hands}-handed weapon") + elif self.defense > 0: + categories.append(f"defense") + elif self.zap_damage > 0: + categories.append("{self.hands}-handed magic weapon") + if len(categories) == 0: + return "unclassifiable" + elif len(categories) == 1: + return categories[0] + else: + return "Erratic: " + ', '.join(categories) + + #def compare(self, other): + # my_class = self.classify() + # o_class = other.classify() + # if my_class == "unclassifiable" or o_class == "unclassifiable": + # return None + # if my_class == "consumable": + # return other.hp_healing - self.hp_healing + + class PlayerEntity(COSEntity): def __init__(self, *, game): #print(f"spawn at origin") self.draw_order = 10 super().__init__(game.grid, 0, 0, sprite_num=84, game=game) + self.hp = 10 + self.max_hp = 10 + self.base_damage = 1 + self.base_defense = 0 + self.luck = 0 + self.archetype = None + self.equipped = [] + self.inventory = [] + + def tick(self): + for i in self.equipped: + i.tick() + + def calc_damage(self): + dmg = self.base_damage + for i in self.equipped: + dmg += i.damage + return dmg + + def calc_defense(self): + defense = self.base_defense + for i in self.equipped: + defense += i.damage + return defense + + def do_zap(self): + pass + + def bump(self, other, dx, dy, test=False): + if type(other) == BoulderEntity: + print("Boulder hit w/ knockback!") + return self.game.pull_boulder_move((-dx, -dy), other) + print(f"oof, ouch, {other} bumped the player - {other.base_damage} damage from {other}") + self.hp = max(self.hp - max(other.base_damage - self.calc_defense(), 0), 0) def respawn(self, avoid=None): # find spawn point @@ -142,6 +226,8 @@ class BoulderEntity(COSEntity): if type(other) == BoulderEntity: #print("Boulders can't push boulders") return False + elif type(other) == EnemyEntity: + if not other.can_push: return False #tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) # Is the boulder blocked the same direction as the bumper? If not, let's both move @@ -155,7 +241,7 @@ class BoulderEntity(COSEntity): class ButtonEntity(COSEntity): def __init__(self, x, y, exit_entity, *, game): self.draw_order = 1 - super().__init__(game.grid, x, y, 42, game=game) + super().__init__(game.grid, x, y, 250, game=game) self.exit = exit_entity def ev_enter(self, other): @@ -171,7 +257,8 @@ class ButtonEntity(COSEntity): # self.exit.unlock() # TODO: unlock, and then lock again, when player steps on/off if not test: - other._relative_move(dx, dy) + pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + other.do_move(*pos) return True class ExitEntity(COSEntity): @@ -199,5 +286,108 @@ class ExitEntity(COSEntity): other._relative_move(dx, dy) #TODO - player go down a level logic if type(other) == PlayerEntity: - self.game.create_level(self.game.depth + 1) + self.game.depth += 1 + print(f"welcome to level {self.game.depth}") + self.game.create_level(self.game.depth) self.game.swap_level(self.game.level, self.game.spawn_point) + +class EnemyEntity(COSEntity): + def __init__(self, x, y, hp=2, base_damage=1, base_defense=0, sprite=123, can_push=False, crushable=True, sight=8, move_cooldown=1, *, game): + self.draw_order = 7 + super().__init__(game.grid, x, y, sprite, game=game) + self.hp = hp + self.base_damage = base_damage + self.base_defense = base_defense + self.base_sprite = sprite + self.can_push = can_push + self.crushable = crushable + self.sight = sight + self.move_cooldown = move_cooldown + self.moved_last = 0 + + def bump(self, other, dx, dy, test=False): + if self.hp == 0: + if not test: + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + other.do_move(*old_pos) + return True + if type(other) == PlayerEntity: + # TODO - get damage from player, take damage, decide to die or not + d = other.calc_damage() + self.hp -= d + self.hp = max(self.hp, 0) + if self.hp == 0: + self._entity.sprite_number = self.base_sprite + 246 + self.draw_order = 1 + print(f"Player hit for {d}. HP = {self.hp}") + #self.hp = 0 + return False + elif type(other) == BoulderEntity: + if not self.crushable and self.hp > 0: + print("Uncrushable!") + return False + if self.hp > 0: + print("Ouch, my entire body!!") + self._entity.sprite_number = self.base_sprite + 246 + self.hp = 0 + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + if not test: + other.do_move(*old_pos) + return True + + def act(self): + if self.hp > 0: + # if player nearby: attack + x, y = self.draw_pos + px, py = self.game.player.draw_pos + for d in ((1, 0), (0, 1), (-1, 0), (1, 0)): + if int(x + d[0]) == int(px) and int(y + d[1]) == int(py): + self.try_move(*d) + return + + # slow movement (doesn't affect ability to attack) + if self.moved_last < 0: + self.moved_last -= 1 + return + else: + self.moved_last = self.move_cooldown + + # if player is not nearby, wander + if abs(x - px) + abs(y - py) > self.sight: + d = random.choice(((1, 0), (0, 1), (-1, 0), (1, 0))) + self.try_move(*d) + + # if can_push and player in a line: KICK + if self.can_push: + if int(x) == int(px): + pass # vertical kick + elif int(y) == int(py): + pass # horizontal kick + + # else, nearby pursue + towards = [] + dist = lambda dx, dy: abs(px - (x + dx)) + abs(py - (y + dy)) + current_dist = dist(0, 0) + for d in ((1, 0), (0, 1), (-1, 0), (1, 0)): + if dist(*d) <= current_dist + 0.75: towards.append(d) + print(current_dist, towards) + target_dir = random.choice(towards) + self.try_move(*target_dir) + + +class TreasureEntity(COSEntity): + def __init__(self, x, y, treasure_table=None, *, game): + self.draw_order = 6 + super().__init__(game.grid, x, y, 89, game=game) + self.popped = False + + def bump(self, other, dx, dy, test=False): + if type(other) != PlayerEntity: + return False + if self.popped: + print("It's already open.") + return + print("Take me, I'm yours!") + self._entity.sprite_number = 91 + self.popped = True + diff --git a/src/scripts/cos_level.py b/src/scripts/cos_level.py index 32392b0..b7509b9 100644 --- a/src/scripts/cos_level.py +++ b/src/scripts/cos_level.py @@ -25,6 +25,10 @@ class BinaryRoomNode: def __repr__(self): return f"" + def center(self): + x, y, w, h = self.data + return (x + w // 2, y + h // 2) + def split(self): new_data = binary_space_partition(*self.data) self.left = BinaryRoomNode(new_data[0]) @@ -35,6 +39,11 @@ class BinaryRoomNode: return self.left.walk() + self.right.walk() return [self] + def contains(self, pt): + x, y, w, h = self.data + tx, ty = pt + return x <= tx <= x + w and y <= ty <= y + h + class RoomGraph: def __init__(self, xywh): self.root = BinaryRoomNode(xywh) @@ -49,6 +58,7 @@ class RoomGraph: def room_coord(room, margin=0): x, y, w, h = room.data + #print(x,y,w,h, f'{margin=}', end=';') w -= 1 h -= 1 margin += 1 @@ -58,10 +68,37 @@ def room_coord(room, margin=0): h -= margin if w < 0: w = 0 if h < 0: h = 0 + #print(x,y,w,h, end=' -> ') tx = x if w==0 else random.randint(x, x+w) ty = y if h==0 else random.randint(y, y+h) + #print((tx, ty)) return (tx, ty) +def adjacent_rooms(r, rooms): + x, y, w, h = r.data + adjacents = {} + + for i, other_r in enumerate(rooms): + rx, ry, rw, rh = other_r.data + if (rx, ry, rw, rh) == r: + continue # Skip self + + # Check vertical adjacency (above or below) + if rx < x + w and x < rx + rw: # Overlapping width + if ry + rh == y: # Above + adjacents[i] = (x + w // 2, y - 1) + elif y + h == ry: # Below + adjacents[i] = (x + w // 2, y + h + 1) + + # Check horizontal adjacency (left or right) + if ry < y + h and y < ry + rh: # Overlapping height + if rx + rw == x: # Left + adjacents[i] = (x - 1, y + h // 2) + elif x + w == rx: # Right + adjacents[i] = (x + w + 1, y + h // 2) + + return adjacents + class Level: def __init__(self, width, height): self.width = width @@ -70,6 +107,7 @@ class Level: self.graph = RoomGraph( (0, 0, width, height) ) self.grid = mcrfpy.Grid(width, height, t, (10, 10), (1014, 758)) self.highlighted = -1 #debug view feature + self.walled_rooms = [] # for tracking "hallway rooms" vs "walled rooms" def fill(self, xywh, highlight = False): if highlight: @@ -84,7 +122,7 @@ class Level: def highlight(self, delta): rooms = self.graph.walk() if self.highlighted < len(rooms): - print(f"reset {self.highlighted}") + #print(f"reset {self.highlighted}") self.fill(rooms[self.highlighted].data) # reset self.highlighted += delta print(f"highlight {self.highlighted}") @@ -110,7 +148,7 @@ class Level: def fill_rooms(self, features=None): rooms = self.graph.walk() - print(f"rooms: {len(rooms)}") + #print(f"rooms: {len(rooms)}") for i, g in enumerate(rooms): X, Y, W, H = g.data #c = [random.randint(0, 255) for _ in range(3)] @@ -120,9 +158,14 @@ class Level: self.grid.at((x, y)).tilesprite = ts def wall_rooms(self): + self.walled_rooms = [] rooms = self.graph.walk() - for g in rooms: - #if random.random() > 0.66: continue + for i, g in enumerate(rooms): + # unwalled / hallways: not selected for small dungeons, first, last, and 65% of all other rooms + if len(rooms) > 3 and i > 1 and i < len(rooms) - 2 and random.random() < 0.35: + self.walled_rooms.append(False) + continue + self.walled_rooms.append(True) X, Y, W, H = g.data for x in range(X, X+W): self.grid.at((x, Y)).walkable = False @@ -138,13 +181,45 @@ class Level: # self.grid.at((0, y)).walkable = False self.grid.at((self.width-1, y)).walkable = False - def generate(self, target_rooms = 5, features=None): - if features is None: - shuffled = ["boulder", "button"] - random.shuffle(shuffled) - features = ["spawn"] + shuffled + ["exit", "treasure"] - # Binary space partition to reach given # of rooms + def dig_path(self, start:"Tuple[int, int]", end:"Tuple[int, int]", walkable=True, color=None, sprite=None): + print(f"Digging: {start} -> {end}") + # get x1,y1 and x2,y2 coordinates: top left and bottom right points on the rect formed by two random points, one from each of the 2 rooms + x1 = min([start[0], end[0]]) + x2 = max([start[0], end[0]]) + dw = x2 - x1 + y1 = min([start[1], end[1]]) + y2 = max([start[1], end[1]]) + dh = y2 - y1 + + # random: top left or bottom right as the corner between the paths + tx, ty = (x1, y1) if random.random() >= 0.5 else (x2, y2) + + for x in range(x1, x1+dw): + try: + if walkable: + self.grid.at((x, ty)).walkable = walkable + if color: + self.grid.at((x, ty)).color = color + if sprite is not None: + self.grid.at((x, ty)).tilesprite = sprite + except: + pass + for y in range(y1, y1+dh): + try: + if walkable: + self.grid.at((tx, y)).walkable = True + if color: + self.grid.at((tx, y)).color = color + if sprite is not None: + self.grid.at((tx, y)).tilesprite = sprite + except: + pass + + def generate(self, level_plan): #target_rooms = 5, features=None): self.reset() + target_rooms = len(level_plan) + if type(level_plan) is set: + level_plan = random.choice(list(level_plan)) while len(self.graph.walk()) < target_rooms: self.split(single=len(self.graph.walk()) > target_rooms * .5) @@ -152,44 +227,59 @@ class Level: #self.fill_rooms() self.wall_rooms() rooms = self.graph.walk() - feature_coords = {} + feature_coords = [] prev_room = None - for room in rooms: - if not features: break - f = features.pop(0) - feature_coords[f] = room_coord(room, margin=4 if f in ("boulder",) else 1) + print(level_plan) + for room_num, room in enumerate(rooms): + room_plan = level_plan[room_num] + if type(room_plan) == str: room_plan = [room_plan] # single item plans became single-character plans... + for f in room_plan: + #feature_coords.append((f, room_coord(room, margin=4 if f in ("boulder",) else 1))) + # boulders are breaking my brain. If I can't get boulders away from walls with margin, I'm just going to dig them out. + if f == "boulder": + x, y = room_coord(room, margin=0) + if x < 2: x += 1 + if y < 2: y += 1 + if x > self.grid.grid_size[0] - 2: x -= 1 + if y > self.grid.grid_size[1] - 2: y -= 1 + for _x in (1, 0, -1): + for _y in (1, 0, -1): + self.grid.at((x + _x, y + _y)).walkable = True + feature_coords.append((f, (x, y))) + else: + feature_coords.append((f, room_coord(room, margin=0))) + print(feature_coords[-1]) ## Hallway generation # plow an inelegant path if prev_room: start = room_coord(prev_room, margin=2) end = room_coord(room, margin=2) - # get x1,y1 and x2,y2 coordinates: top left and bottom right points on the rect formed by two random points, one from each of the 2 rooms - x1 = min([start[0], end[0]]) - x2 = max([start[0], end[0]]) - dw = x2 - x1 - y1 = min([start[1], end[1]]) - y2 = max([start[1], end[1]]) - dh = y2 - y1 - #print(x1, y1, x2, y2, dw, dh) - - # random: top left or bottom right as the corner between the paths - tx, ty = (x1, y1) if random.random() >= 0.5 else (x2, y2) - - for x in range(x1, x1+dw): - self.grid.at((x, ty)).walkable = True - #self.grid.at((x, ty)).color = (255, 0, 0) - #self.grid.at((x, ty)).tilesprite = -1 - for y in range(y1, y1+dh): - self.grid.at((tx, y)).walkable = True - #self.grid.at((tx, y)).color = (0, 255, 0) - #self.grid.at((tx, y)).tilesprite = -1 + self.dig_path(start, end, color=(0, 64, 0)) prev_room = room - # Tile painting possibilities = None while possibilities or possibilities is None: possibilities = ct.wfc_pass(self.grid, possibilities) + ## "hallway room" repainting + #for i, hall_room in enumerate(rooms): + # if self.walled_rooms[i]: + # print(f"walled room: {hall_room}") + # continue + # print(f"hall room: {hall_room}") + # x, y, w, h = hall_room.data + # for _x in range(x+1, x+w-1): + # for _y in range(y+1, y+h-1): + # self.grid.at((_x, _y)).walkable = False + # self.grid.at((_x, _y)).tilesprite = -1 + # self.grid.at((_x, _y)).color = (0, 0, 0) # pit! + # targets = adjacent_rooms(hall_room, rooms) + # print(targets) + # for k, v in targets.items(): + # self.dig_path(hall_room.center(), v, color=(64, 32, 32)) + # for _, p in feature_coords: + # if hall_room.contains(p): self.dig_path(hall_room.center(), p, color=(92, 48, 48)) + return feature_coords diff --git a/src/scripts/cos_tiles.py b/src/scripts/cos_tiles.py index d7cb57b..4b80785 100644 --- a/src/scripts/cos_tiles.py +++ b/src/scripts/cos_tiles.py @@ -129,7 +129,7 @@ def wfc_pass(grid, possibilities=None): for v in possibilities.values(): if len(v) in counts: counts[len(v)] += 1 else: counts[len(v)] = 1 - print(counts) + #print(counts) return possibilities elif len(possibilities) == 0: print("We're done!") diff --git a/src/scripts/game.py b/src/scripts/game.py index b6ce058..6b7fab5 100644 --- a/src/scripts/game.py +++ b/src/scripts/game.py @@ -1,6 +1,7 @@ import mcrfpy #t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11) -#t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) # 12, 11) +t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) # 12, 11) +btn_tex = mcrfpy.Texture("assets/48px_ui_icons-KenneyNL.png", 48, 48) font = mcrfpy.Font("assets/JetbrainsMono.ttf") frame_color = (64, 64, 128) @@ -10,12 +11,36 @@ import cos_entities as ce import cos_level as cl #import cos_tiles as ct +class Resources: + def __init__(self): + self.music_enabled = True + self.music_volume = 40 + self.sfx_enabled = True + self.sfx_volume = 100 + self.master_volume = 100 + + # load the music/sfx files here + self.splats = [] + for i in range(1, 10): + mcrfpy.createSoundBuffer(f"assets/sfx/splat{i}.ogg") + + + def play_sfx(self, sfx_id): + if self.sfx_enabled and self.sfx_volume and self.master_volume: + mcrfpy.setSoundVolume(self.master_volume/100 * self.sfx_volume) + mcrfpy.playSound(sfx_id) + + def play_music(self, track_id): + if self.music_enabled and self.music_volume and self.master_volume: + mcrfpy.setMusicVolume(self.master_volume/100 * self.music_volume) + mcrfpy.playMusic(...) + +resources = Resources() + class Crypt: def __init__(self): mcrfpy.createScene("play") self.ui = mcrfpy.sceneUI("play") - mcrfpy.setScene("play") - mcrfpy.keypressScene(self.cos_keys) entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color) inventory_frame = mcrfpy.Frame(10, 610, 800, 143, fill_color = frame_color) @@ -24,9 +49,66 @@ class Crypt: #self.level = cl.Level(30, 23) self.entities = [] self.depth=1 + self.stuck_btn = SweetButton(self.ui, (810, 700), "Stuck", icon=19, box_width=150, box_height = 60, click=self.stuck) + + self.level_plan = { + 1: [("spawn", "button", "boulder"), ("exit")], + 2: [("spawn", "button", "boulder"), ("rat"), ("exit")], + 3: [("spawn", "button", "boulder"), ("rat"), ("exit")], + 4: [("spawn", "button", "rat"), ("boulder", "rat", "treasure"), ("exit")], + 5: [("spawn", "button", "rat"), ("boulder", "rat"), ("exit")], + 6: {(("spawn", "button"), ("boulder", "treasure", "exit")), + (("spawn", "boulder"), ("button", "treasure", "exit"))}, + 7: {(("spawn", "button"), ("boulder", "treasure", "exit")), + (("spawn", "boulder"), ("button", "treasure", "exit"))}, + 8: {(("spawn", "treasure", "button"), ("boulder", "treasure", "exit")), + (("spawn", "treasure", "boulder"), ("button", "treasure", "exit"))} + #9: self.lv_planner + } + + # empty void for the player to initialize into + self.headsup = mcrfpy.Frame(10, 684, 320, 64, fill_color = (0, 0, 0, 0)) + self.sidebar = mcrfpy.Frame(860, 4, 160, 600, fill_color = (96, 96, 160)) + + # Heads Up (health bar, armor bar) config + self.health_bar = [mcrfpy.Sprite(32*i, 2, t, 659, 2) for i in range(10)] + [self.headsup.children.append(i) for i in self.health_bar] + self.armor_bar = [mcrfpy.Sprite(32*i, 42, t, 659, 2) for i in range(10)] + [self.headsup.children.append(i) for i in self.armor_bar] + # (40, 3), caption, font, fill_color=font_color + self.stat_captions = mcrfpy.Caption((325,0), "HP:10\nDef:0(+0)", font, fill_color=(255, 255, 255)) + self.stat_captions.outline = 3 + self.stat_captions.outline_color = (0, 0, 0) + self.headsup.children.append(self.stat_captions) + + # Side Bar (inventory, level info) config + self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255)) + self.level_caption.size = 26 + self.level_caption.outline = 3 + self.level_caption.outline_color = (0, 0, 0) + self.sidebar.children.append(self.level_caption) + self.inv_sprites = [mcrfpy.Sprite(15, 70 + 95*i, t, 659, 6.0) for i in range(5)] + for i in self.inv_sprites: + self.sidebar.children.append(i) + self.key_captions = [ + mcrfpy.Sprite(75, 130 + (95*2) + 95 * i, t, 384 + i, 3.0) for i in range(3) + ] + for i in self.key_captions: + self.sidebar.children.append(i) + self.inv_captions = [ + mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5) + ] + for i in self.inv_captions: + self.sidebar.children.append(i) + + liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16)) + self.grid = liminal_void + self.player = ce.PlayerEntity(game=self) + self.spawn_point = (0, 0) + + # level creation moves player to the game level at the generated spawn point self.create_level(self.depth) #self.grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758)) - self.player = ce.PlayerEntity(game=self) self.swap_level(self.level, self.spawn_point) # Test Entities @@ -35,10 +117,146 @@ class Crypt: #ce.ExitEntity(12, 6, 14, 4, game=self) # scene setup - - [self.ui.append(e) for e in (self.grid,)] # entity_frame, inventory_frame, stats_frame)] + ## might be done by self.swap_level + #[self.ui.append(e) for e in (self.grid, self.stuck_btn.base_frame)] # entity_frame, inventory_frame, stats_frame)] self.possibilities = None # track WFC possibilities between rounds + self.enemies = [] + #mcrfpy.setTimer("enemy_test", self.enemy_movement, 750) + + #mcrfpy.Frame(x, y, box_width+shadow_offset, box_height, fill_color = (0, 0, 0, 255)) + #Sprite(0, 3, btn_tex, icon, icon_scale) + + #def enemy_movement(self, *args): + # for e in self.enemies: e.act() + + #def spawn_test_rat(self): + # success = False + # while not success: + # x, y = [random.randint(0, i-1) for i in self.grid.grid_size] + # success = self.grid.at((x,y)).walkable + # self.enemies.append(ce.EnemyEntity(x, y, game=self)) + + def gui_update(self): + self.stat_captions.text = f"HP:{self.player.hp}\nDef:{self.player.calc_defense()}(+{self.player.calc_defense() - self.player.base_defense})" + for i, hs in enumerate(self.health_bar): + full_hearts = self.player.hp - (i*2) + empty_hearts = self.player.max_hp - (i*2) + hs.sprite_number = 659 + if empty_hearts >= 2: + hs.sprite_number = 208 + if full_hearts >= 2: + hs.sprite_number = 210 + elif full_hearts == 1: + hs.sprite_number = 209 + for i, arm_s in enumerate(self.armor_bar): + full_hearts = self.player.calc_defense() - (i*2) + arm_s.sprite_number = 659 + if full_hearts >= 2: + arm_s.sprite_number = 211 + elif full_hearts == 1: + arm_s.sprite_number = 212 + + #items = self.player.equipped[:] + self.player.inventory[:] + for i in range(5): + if i == 0: + item = self.player.equipped[0] if len(self.player.equipped) > 0 else None + elif i == 1: + item = self.player.equipped[1] if len(self.player.equipped) > 1 else None + elif i == 2: + item = self.player.inventory[0] if len(self.player.inventory) > 0 else None + elif i == 3: + item = self.player.inventory[1] if len(self.player.inventory) > 1 else None + elif i == 4: + item = self.player.inventory[2] if len(self.player.inventory) > 2 else None + if item is None: + self.inv_sprites[i].sprite_number = 659 + if i > 1: self.key_captions[i - 2].sprite_number = 659 + self.inv_captions[i].text = "" + continue + self.inv_sprites[i].sprite_number = item.sprite + if i > 1: + self.key_captions[i - 2].sprite_number = 384 + (i - 2) + self.inv_captions[i].text = "Blah" + + def lv_planner(self, target_level): + """Plan room sequence in levels > 9""" + monsters = (target_level - 6) // 2 + target_rooms = min(int(target_level // 2), 6) + target_treasure = min(int(target_level // 3), 4) + rooms = [] + for i in range(target_rooms): + rooms.append([]) + for o in ("spawn", "boulder", "button", "exit"): + r = random.randint(0, target_rooms-1) + rooms[r].append(o) + monster_table = { + "rat": int(monsters * 0.8) + 2, + "big rat": max(int(monsters * 0.2) - 2, 0), + "cyclops": max(int(monsters * 0.1) - 3, 0) + } + monster_table = {k: v for k, v in monster_table.items() if v > 0} + monster_names = list(monster_table.keys()) + monster_weights = [monster_table[k] for k in monster_names] + for m in range(monsters): + r = random.randint(0, target_rooms - 1) + rooms[r].append(random.choices(monster_names, weights = monster_weights)[0]) + + for t in range(target_treasure): + r = random.randint(0, target_rooms - 1) + rooms[r].append("treasure") + + return rooms + + def treasure_planner(self, treasure_level): + """Plan treasure contents at all levels""" + item_minlv = { + "buckler": 1, + "shield": 2, + "sword": 1, + "sword2": 2, + "sword3": 5, + "axe": 1, + "axe2": 2, + "axe3": 5, + "wand": 1, + "staff": 2, + "staff2": 5, + "red_pot": 3, + "blue_pot": 6, + "green_pot": 6, + "grey_pot": 6, + "sm_grey_pot": 1 + } + base_wts = { + ("buckler", "shield"): 0.25, # defensive items + ("sword", "sword2", "axe", "axe2"): 0.4, #1h weapons + ("sword3", "axe3"): 0.25, #2h weapons + ("wand", "staff", "staff2"): 0.15, #magic weapons + ("red_pot",): 0.25, #health + ("blue_pot", "green_pot", "grey_pot", "sm_grey_pot"): 0.2 #stat upgrade potions + } + + # find item name in base_wts key (base weight of the category) + base_weight = lambda s: base_wts[list([t for t in base_wts.keys() if s in t])[0]] + weights = {d[0]: base_weight(d[0]) for d in item_minlv.items() if treasure_level > d[1]} + if self.player.archetype is None: + prefs = [] + elif self.player.archetype == "viking": + prefs = ["axe2", "axe3", "green_pot"] + elif self.player.archetype == "knight": + prefs = ["sword2", "shield", "grey_pot"] + elif self.player.archetype == "wizard": + prefs = ["staff", "staff2", "blue_pot"] + for i in prefs: + if i in weights: weights[i] *= 3 + + return weights + + def start(self): + resources.play_sfx(1) + mcrfpy.setScene("play") + mcrfpy.keypressScene(self.cos_keys) def add_entity(self, e:ce.COSEntity): self.entities.append(e) @@ -49,22 +267,52 @@ class Crypt: for e in self.entities: self.grid.entities.append(e._entity) - def create_level(self, depth): + def create_level(self, depth, _luck = 0): #if depth < 3: # features = None - self.level = cl.Level(30, 23) + self.level = cl.Level(20, 20) self.grid = self.level.grid - coords = self.level.generate() + if depth in self.level_plan: + plan = self.level_plan[depth] + else: + plan = self.lv_planner(depth) + coords = self.level.generate(plan) self.entities = [] - for k, v in coords.items(): + if self.player: + luck = self.player.luck + else: + luck = 0 + buttons = [] + for k, v in sorted(coords, key=lambda i: i[0]): # "button" before "exit"; "button", "button", "door", "exit" -> alphabetical is correct sequence if k == "spawn": - self.spawn_point = v + if self.player: + self.add_entity(self.player) + #self.player.draw_pos = v + self.spawn_point = v elif k == "boulder": ce.BoulderEntity(v[0], v[1], game=self) + elif k == "treasure": + ce.TreasureEntity(v[0], v[1], treasure_table = self.treasure_planner(depth + luck), game=self) elif k == "button": - pass + buttons.append(v) elif k == "exit": - ce.ExitEntity(v[0], v[1], coords["button"][0], coords["button"][1], game=self) + btn = buttons.pop(0) + ce.ExitEntity(v[0], v[1], btn[0], btn[1], game=self) + elif k == "rat": + ce.EnemyEntity(*v, game=self) + elif k == "big rat": + ce.EnemyEntity(*v, game=self, base_damage=2, hp=4, sprite=130) + elif k == "cyclops": + ce.EnemyEntity(*v, game=self, base_damage=3, hp=8, sprite=109, base_defense=2) + + #if self.depth > 2: + #for i in range(10): + # self.spawn_test_rat() + + def stuck(self, sweet_btn, args): + if args[3] == "end": return + self.create_level(self.depth) + self.swap_level(self.level, self.spawn_point) def cos_keys(self, key, state): d = None @@ -73,37 +321,286 @@ class Crypt: elif key == "A": d = (-1, 0) elif key == "S": d = (0, 1) elif key == "D": d = (1, 0) - elif key == "M": self.level.generate() - elif key == "R": - self.level.reset() - self.possibilities = None - elif key == "T": - self.level.split() - self.possibilities = None - elif key == "Y": self.level.split(single=True) - elif key == "U": self.level.highlight(+1) - elif key == "I": self.level.highlight(-1) - elif key == "O": - self.level.wall_rooms() - self.possibilities = None + #elif key == "M": self.level.generate() + #elif key == "R": + # self.level.reset() + # self.possibilities = None + #elif key == "T": + # self.level.split() + # self.possibilities = None + #elif key == "Y": self.level.split(single=True) + #elif key == "U": self.level.highlight(+1) + #elif key == "I": self.level.highlight(-1) + #elif key == "O": + # self.level.wall_rooms() + # self.possibilities = None #elif key == "P": ct.format_tiles(self.grid) + #elif key == "P": + #self.possibilities = ct.wfc_pass(self.grid, self.possibilities) elif key == "P": - self.possibilities = ct.wfc_pass(self.grid, self.possibilities) - if d: self.player.try_move(*d) + self.depth += 1 + print(f"Descending: lv {self.depth}") + self.stuck(None, [1,2,3,4]) + elif key == "Period": + self.enemy_turn() + elif key == "X": + self.pull_boulder_search() + #else: + # print(key) + if d: + self.entities.sort(key = lambda e: e.draw_order, reverse=False) + self.player.try_move(*d) + self.enemy_turn() + + def enemy_turn(self): + self.entities.sort(key = lambda e: e.draw_order, reverse=False) + for e in self.entities: + e.act() + self.gui_update() + + def pull_boulder_search(self): + for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ): + for e in self.entities: + if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + dy): continue + if type(e) == ce.BoulderEntity: + self.pull_boulder_move((dx, dy), e) + return self.enemy_turn() + else: + print("No boulder found to pull.") + + def pull_boulder_move(self, p, target_boulder): + print(p, target_boulder) + self.entities.sort(key = lambda e: e.draw_order, reverse=False) + if self.player.try_move(-p[0], -p[1], test=True): + old_pos = self.player.draw_pos + self.player.try_move(-p[0], -p[1]) + target_boulder.do_move(*old_pos) def swap_level(self, new_level, spawn_point): self.level = new_level self.grid = self.level.grid self.grid.zoom = 2.0 # TODO, make an entity mover function - self.add_entity(self.player) + #self.add_entity(self.player) self.player.grid = self.grid self.player.draw_pos = spawn_point - self.grid.entities.append(self.player._entity) - try: - self.ui.remove(0) - except: - pass - self.ui.append(self.grid) + #self.grid.entities.append(self.player._entity) -crypt = Crypt() + # reform UI (workaround to ui collection iterators crashing) + while len(self.ui) > 0: + try: + self.ui.remove(0) + except: + pass + self.ui.append(self.grid) + self.ui.append(self.stuck_btn.base_frame) + self.ui.append(self.headsup) + + self.level_caption.text = f"Level: {self.depth}" + self.ui.append(self.sidebar) + self.gui_update() + +class SweetButton: + def __init__(self, ui:mcrfpy.UICollection, + pos:"Tuple[int, int]", + caption:str, font=font, font_size=24, font_color=(255,255,255), font_outline_color=(0, 0, 0), font_outline_width=2, + shadow_offset = 8, box_width=200, box_height = 80, shadow_color=(64, 64, 86), box_color=(96, 96, 160), + icon=4, icon_scale=1.75, click=lambda *args: None): + self.ui = ui + self.shadow_box = mcrfpy.Frame + x, y = pos + + # box w/ drop shadow + self.shadow_offset = shadow_offset + self.base_frame = mcrfpy.Frame(x, y, box_width+shadow_offset, box_height, fill_color = (0, 0, 0, 255)) + self.base_frame.click = self.do_click + + # drop shadow won't need configured, append directly + self.base_frame.children.append(mcrfpy.Frame(0, 0, box_width, box_height, fill_color = shadow_color)) + + # main button is where the content lives + self.main_button = mcrfpy.Frame(shadow_offset, shadow_offset, box_width, box_height, fill_color = box_color) + self.click = click + self.base_frame.children.append(self.main_button) + + # main button icon + self.icon = mcrfpy.Sprite(0, 3, btn_tex, icon, icon_scale) + self.main_button.children.append(self.icon) + + # main button caption + self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color) + self.caption.size = font_size + self.caption.outline_color=font_outline_color + self.caption.outline=font_outline_width + self.main_button.children.append(self.caption) + + def unpress(self): + """Helper func for when graphics changes or glitches make the button stuck down""" + self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset) + + def do_click(self, x, y, mousebtn, event): + if event == "start": + self.main_button.x, self.main_button.y = (0, 0) + elif event == "end": + self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset) + result = self.click(self, (x, y, mousebtn, event)) + if result: # return True from event function to instantly un-pop + self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset) + + @property + def text(self): + return self.caption.text + + @text.setter + def text(self, value): + self.caption.text = value + + @property + def sprite_number(self): + return self.icon.sprite_number + + @sprite_number.setter + def sprite_number(self, value): + self.icon.sprite_number = value + +class MainMenu: + def __init__(self): + mcrfpy.createScene("menu") + self.ui = mcrfpy.sceneUI("menu") + mcrfpy.setScene("menu") + self.crypt = None + + components = [] + # demo grid + #components.append( + # ) + + # title text + drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0)) + drop_shadow.outline = 3 + drop_shadow.size = 64 + components.append( + drop_shadow + ) + + title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255)) + title_txt.size = 64 + components.append( + title_txt + ) + + # toast: text over the demo grid that fades out on a timer + self.toast = mcrfpy.Caption((150, 400), "", font, fill_color=(0, 0, 0)) + self.toast.size = 28 + self.toast.outline = 2 + self.toast.outline_color = (255, 255, 255) + self.toast_event = None + components.append(self.toast) + + # button - PLAY + #playbtn = mcrfpy.Frame(284, 548, 456, 120, fill_color = + play_btn = SweetButton(self.ui, (284, 548), "PLAY", box_width=456, box_height=110, icon=1, icon_scale=2.0, click=self.play) + components.append(play_btn.base_frame) + + # button - config menu pane + #self.config = lambda self, sweet_btn, *args: print(f"boop, sweet button {sweet_btn} config {args}") + config_btn = SweetButton(self.ui, (10, 678), "Settings", icon=2, click=self.show_config) + components.append(config_btn.base_frame) + + # button - insta-1080p scaling + scale_btn = SweetButton(self.ui, (10+256, 678), "Scale up\nto 1080p", icon=15, click=self.scale) + self.scaled = False + components.append(scale_btn.base_frame) + + # button - music toggle + music_btn = SweetButton(self.ui, (10+256*2, 678), "Music\nON", icon=12, click=self.music_toggle) + self.music_enabled = True + self.music_volume = 40 + components.append(music_btn.base_frame) + + # button - sfx toggle + sfx_btn = SweetButton(self.ui, (10+256*3, 678), "SFX\nON", icon=0, click=self.sfx_toggle) + self.sfx_enabled = True + self.sfx_volume = 40 + components.append(sfx_btn.base_frame) + + [self.ui.append(e) for e in components] + + def toast_say(self, txt, delay=10): + "kick off a toast event" + if self.toast_event is not None: + mcrfpy.delTimer("toast_timer") + self.toast.text = txt + self.toast_event = 350 + self.toast.fill_color = (255, 255, 255, 255) + self.toast.outline = 2 + self.toast.outline_color = (0, 0, 0, 255) + mcrfpy.setTimer("toast_timer", self.toast_callback, 100) + + def toast_callback(self, *args): + "fade out the toast text" + self.toast_event -= 5 + if self.toast_event < 0: + self.toast_event = None + mcrfpy.delTimer("toast_timer") + mcrfpy.text = "" + return + a = min(self.toast_event, 255) + self.toast.fill_color = (255, 255, 255, a) + self.toast.outline_color = (0, 0, 0, a) + + def show_config(self, sweet_btn, args): + self.toast_say("Beep, Boop! Configurations will go here.") + + def play(self, sweet_btn, args): + #if args[3] == "start": return # DRAMATIC on release action! + if args[3] == "end": return + self.crypt = Crypt() + #mcrfpy.setScene("play") + self.crypt.start() + + def scale(self, sweet_btn, args, window_scale=None): + if args[3] == "end": return + if not window_scale: + self.scaled = not self.scaled + window_scale = 1.3 + else: + self.scaled = True + sweet_btn.unpress() + if self.scaled: + self.toast_say("Windowed mode only, sorry!\nCheck Settings for for fine-tuned controls.") + mcrfpy.setScale(window_scale) + sweet_btn.text = "Scale down\n to 1.0x" + else: + mcrfpy.setScale(1.0) + sweet_btn.text = "Scale up\nto 1080p" + + def music_toggle(self, sweet_btn, args): + if args[3] == "end": return + self.music_enabled = not self.music_enabled + print(f"music: {self.music_enabled}") + if self.music_enabled: + mcrfpy.setMusicVolume(self.music_volume) + sweet_btn.text = "Music is ON" + sweet_btn.sprite_number = 12 + else: + self.toast_say("Use your volume keys or\nlook in Settings for a volume meter.") + mcrfpy.setMusicVolume(0) + sweet_btn.text = "Music is OFF" + sweet_btn.sprite_number = 17 + + def sfx_toggle(self, sweet_btn, args): + if args[3] == "end": return + self.sfx_enabled = not self.sfx_enabled + print(f"sfx: {self.sfx_enabled}") + if self.sfx_enabled: + mcrfpy.setSoundVolume(self.sfx_volume) + sweet_btn.text = "SFX are ON" + sweet_btn.sprite_number = 0 + else: + self.toast_say("Use your volume keys or\nlook in Settings for a volume meter.") + mcrfpy.setSoundVolume(0) + sweet_btn.text = "SFX are OFF" + sweet_btn.sprite_number = 17 + +mainmenu = MainMenu()