7DRL 2025 progress

This commit is contained in:
John McCardle 2025-03-08 10:42:17 -05:00
parent e928dda4b3
commit 6be474da08
16 changed files with 856 additions and 80 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

After

Width:  |  Height:  |  Size: 674 KiB

BIN
assets/sfx/splat1.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat2.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat3.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat4.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat5.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat6.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat7.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat8.ogg Normal file

Binary file not shown.

BIN
assets/sfx/splat9.ogg Normal file

Binary file not shown.

View File

@ -28,7 +28,6 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
PyObject* PyTexture::pyObject() PyObject* PyTexture::pyObject()
{ {
std::cout << "Find type" << std::endl;
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"); auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture");
PyObject* obj = PyTexture::pynew(type, Py_None, Py_None); PyObject* obj = PyTexture::pynew(type, Py_None, Py_None);

View File

@ -249,7 +249,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
self->data->text.setPosition(pos_result->data); self->data->text.setPosition(pos_result->data);
// check types for font, fill_color, outline_color // 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)*/)){ 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"); PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
return -1; return -1;

View File

@ -1,4 +1,5 @@
import mcrfpy import mcrfpy
import random
#t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) #t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16) t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
#def iterable_entities(grid): #def iterable_entities(grid):
@ -37,7 +38,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
self._entity.sprite_number = value self._entity.sprite_number = value
def __repr__(self): def __repr__(self):
return f"<COSEntity ({self.draw_pos}) on {self.grid}>" return f"<{self.__class__.__name__} ({self.draw_pos})>"
def die(self): def die(self):
# ugly workaround! grid.entities isn't really iterable (segfaults) # 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: for e in self.game.entities:
if e is self: continue if e is self: continue
if e.draw_pos == (tx, ty): e.ev_enter(self) if e.draw_pos == (tx, ty): e.ev_enter(self)
def act(self):
pass
def ev_enter(self, other): def ev_enter(self, other):
pass pass
@ -106,11 +109,92 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
#self.draw_pos = (tx, ty) #self.draw_pos = (tx, ty)
self.do_move(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"<Equippable hands={self.hands}, hp_healing={self.hp_healing}, damage={self.damage}, defense={self.defense}, zap_damage={self.zap_damage}, zap_cooldown={self.zap_cooldown}{cooldown_str if self.zap_cooldown_remaining else ''}, sprite={self.sprite}>"
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): class PlayerEntity(COSEntity):
def __init__(self, *, game): def __init__(self, *, game):
#print(f"spawn at origin") #print(f"spawn at origin")
self.draw_order = 10 self.draw_order = 10
super().__init__(game.grid, 0, 0, sprite_num=84, game=game) 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): def respawn(self, avoid=None):
# find spawn point # find spawn point
@ -142,6 +226,8 @@ class BoulderEntity(COSEntity):
if type(other) == BoulderEntity: if type(other) == BoulderEntity:
#print("Boulders can't push boulders") #print("Boulders can't push boulders")
return False 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.e.position[0] + dx), int(self.e.position[1] + dy)
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[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 # 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): class ButtonEntity(COSEntity):
def __init__(self, x, y, exit_entity, *, game): def __init__(self, x, y, exit_entity, *, game):
self.draw_order = 1 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 self.exit = exit_entity
def ev_enter(self, other): def ev_enter(self, other):
@ -171,7 +257,8 @@ class ButtonEntity(COSEntity):
# self.exit.unlock() # self.exit.unlock()
# TODO: unlock, and then lock again, when player steps on/off # TODO: unlock, and then lock again, when player steps on/off
if not test: 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 return True
class ExitEntity(COSEntity): class ExitEntity(COSEntity):
@ -199,5 +286,108 @@ class ExitEntity(COSEntity):
other._relative_move(dx, dy) other._relative_move(dx, dy)
#TODO - player go down a level logic #TODO - player go down a level logic
if type(other) == PlayerEntity: 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) 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

View File

@ -25,6 +25,10 @@ class BinaryRoomNode:
def __repr__(self): def __repr__(self):
return f"<RoomNode {self.data}>" return f"<RoomNode {self.data}>"
def center(self):
x, y, w, h = self.data
return (x + w // 2, y + h // 2)
def split(self): def split(self):
new_data = binary_space_partition(*self.data) new_data = binary_space_partition(*self.data)
self.left = BinaryRoomNode(new_data[0]) self.left = BinaryRoomNode(new_data[0])
@ -35,6 +39,11 @@ class BinaryRoomNode:
return self.left.walk() + self.right.walk() return self.left.walk() + self.right.walk()
return [self] 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: class RoomGraph:
def __init__(self, xywh): def __init__(self, xywh):
self.root = BinaryRoomNode(xywh) self.root = BinaryRoomNode(xywh)
@ -49,6 +58,7 @@ class RoomGraph:
def room_coord(room, margin=0): def room_coord(room, margin=0):
x, y, w, h = room.data x, y, w, h = room.data
#print(x,y,w,h, f'{margin=}', end=';')
w -= 1 w -= 1
h -= 1 h -= 1
margin += 1 margin += 1
@ -58,10 +68,37 @@ def room_coord(room, margin=0):
h -= margin h -= margin
if w < 0: w = 0 if w < 0: w = 0
if h < 0: h = 0 if h < 0: h = 0
#print(x,y,w,h, end=' -> ')
tx = x if w==0 else random.randint(x, x+w) tx = x if w==0 else random.randint(x, x+w)
ty = y if h==0 else random.randint(y, y+h) ty = y if h==0 else random.randint(y, y+h)
#print((tx, ty))
return (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: class Level:
def __init__(self, width, height): def __init__(self, width, height):
self.width = width self.width = width
@ -70,6 +107,7 @@ class Level:
self.graph = RoomGraph( (0, 0, width, height) ) self.graph = RoomGraph( (0, 0, width, height) )
self.grid = mcrfpy.Grid(width, height, t, (10, 10), (1014, 758)) self.grid = mcrfpy.Grid(width, height, t, (10, 10), (1014, 758))
self.highlighted = -1 #debug view feature self.highlighted = -1 #debug view feature
self.walled_rooms = [] # for tracking "hallway rooms" vs "walled rooms"
def fill(self, xywh, highlight = False): def fill(self, xywh, highlight = False):
if highlight: if highlight:
@ -84,7 +122,7 @@ class Level:
def highlight(self, delta): def highlight(self, delta):
rooms = self.graph.walk() rooms = self.graph.walk()
if self.highlighted < len(rooms): if self.highlighted < len(rooms):
print(f"reset {self.highlighted}") #print(f"reset {self.highlighted}")
self.fill(rooms[self.highlighted].data) # reset self.fill(rooms[self.highlighted].data) # reset
self.highlighted += delta self.highlighted += delta
print(f"highlight {self.highlighted}") print(f"highlight {self.highlighted}")
@ -110,7 +148,7 @@ class Level:
def fill_rooms(self, features=None): def fill_rooms(self, features=None):
rooms = self.graph.walk() rooms = self.graph.walk()
print(f"rooms: {len(rooms)}") #print(f"rooms: {len(rooms)}")
for i, g in enumerate(rooms): for i, g in enumerate(rooms):
X, Y, W, H = g.data X, Y, W, H = g.data
#c = [random.randint(0, 255) for _ in range(3)] #c = [random.randint(0, 255) for _ in range(3)]
@ -120,9 +158,14 @@ class Level:
self.grid.at((x, y)).tilesprite = ts self.grid.at((x, y)).tilesprite = ts
def wall_rooms(self): def wall_rooms(self):
self.walled_rooms = []
rooms = self.graph.walk() rooms = self.graph.walk()
for g in rooms: for i, g in enumerate(rooms):
#if random.random() > 0.66: continue # 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 X, Y, W, H = g.data
for x in range(X, X+W): for x in range(X, X+W):
self.grid.at((x, Y)).walkable = False self.grid.at((x, Y)).walkable = False
@ -138,13 +181,45 @@ class Level:
# self.grid.at((0, y)).walkable = False # self.grid.at((0, y)).walkable = False
self.grid.at((self.width-1, y)).walkable = False self.grid.at((self.width-1, y)).walkable = False
def generate(self, target_rooms = 5, features=None): def dig_path(self, start:"Tuple[int, int]", end:"Tuple[int, int]", walkable=True, color=None, sprite=None):
if features is None: print(f"Digging: {start} -> {end}")
shuffled = ["boulder", "button"] # 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
random.shuffle(shuffled) x1 = min([start[0], end[0]])
features = ["spawn"] + shuffled + ["exit", "treasure"] x2 = max([start[0], end[0]])
# Binary space partition to reach given # of rooms 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() 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: while len(self.graph.walk()) < target_rooms:
self.split(single=len(self.graph.walk()) > target_rooms * .5) self.split(single=len(self.graph.walk()) > target_rooms * .5)
@ -152,44 +227,59 @@ class Level:
#self.fill_rooms() #self.fill_rooms()
self.wall_rooms() self.wall_rooms()
rooms = self.graph.walk() rooms = self.graph.walk()
feature_coords = {} feature_coords = []
prev_room = None prev_room = None
for room in rooms: print(level_plan)
if not features: break for room_num, room in enumerate(rooms):
f = features.pop(0) room_plan = level_plan[room_num]
feature_coords[f] = room_coord(room, margin=4 if f in ("boulder",) else 1) 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 ## Hallway generation
# plow an inelegant path # plow an inelegant path
if prev_room: if prev_room:
start = room_coord(prev_room, margin=2) start = room_coord(prev_room, margin=2)
end = room_coord(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 self.dig_path(start, end, color=(0, 64, 0))
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
prev_room = room prev_room = room
# Tile painting # Tile painting
possibilities = None possibilities = None
while possibilities or possibilities is None: while possibilities or possibilities is None:
possibilities = ct.wfc_pass(self.grid, possibilities) 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 return feature_coords

View File

@ -129,7 +129,7 @@ def wfc_pass(grid, possibilities=None):
for v in possibilities.values(): for v in possibilities.values():
if len(v) in counts: counts[len(v)] += 1 if len(v) in counts: counts[len(v)] += 1
else: counts[len(v)] = 1 else: counts[len(v)] = 1
print(counts) #print(counts)
return possibilities return possibilities
elif len(possibilities) == 0: elif len(possibilities) == 0:
print("We're done!") print("We're done!")

View File

@ -1,6 +1,7 @@
import mcrfpy import mcrfpy
#t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11) #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") font = mcrfpy.Font("assets/JetbrainsMono.ttf")
frame_color = (64, 64, 128) frame_color = (64, 64, 128)
@ -10,12 +11,36 @@ import cos_entities as ce
import cos_level as cl import cos_level as cl
#import cos_tiles as ct #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: class Crypt:
def __init__(self): def __init__(self):
mcrfpy.createScene("play") mcrfpy.createScene("play")
self.ui = mcrfpy.sceneUI("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) entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color)
inventory_frame = mcrfpy.Frame(10, 610, 800, 143, 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.level = cl.Level(30, 23)
self.entities = [] self.entities = []
self.depth=1 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.create_level(self.depth)
#self.grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758)) #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) self.swap_level(self.level, self.spawn_point)
# Test Entities # Test Entities
@ -35,10 +117,146 @@ class Crypt:
#ce.ExitEntity(12, 6, 14, 4, game=self) #ce.ExitEntity(12, 6, 14, 4, game=self)
# scene setup # scene setup
## might be done by self.swap_level
[self.ui.append(e) for e in (self.grid,)] # entity_frame, inventory_frame, stats_frame)] #[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.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): def add_entity(self, e:ce.COSEntity):
self.entities.append(e) self.entities.append(e)
@ -49,22 +267,52 @@ class Crypt:
for e in self.entities: for e in self.entities:
self.grid.entities.append(e._entity) self.grid.entities.append(e._entity)
def create_level(self, depth): def create_level(self, depth, _luck = 0):
#if depth < 3: #if depth < 3:
# features = None # features = None
self.level = cl.Level(30, 23) self.level = cl.Level(20, 20)
self.grid = self.level.grid 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 = [] 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": 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": elif k == "boulder":
ce.BoulderEntity(v[0], v[1], game=self) 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": elif k == "button":
pass buttons.append(v)
elif k == "exit": 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): def cos_keys(self, key, state):
d = None d = None
@ -73,37 +321,286 @@ class Crypt:
elif key == "A": d = (-1, 0) elif key == "A": d = (-1, 0)
elif key == "S": d = (0, 1) elif key == "S": d = (0, 1)
elif key == "D": d = (1, 0) elif key == "D": d = (1, 0)
elif key == "M": self.level.generate() #elif key == "M": self.level.generate()
elif key == "R": #elif key == "R":
self.level.reset() # self.level.reset()
self.possibilities = None # self.possibilities = None
elif key == "T": #elif key == "T":
self.level.split() # self.level.split()
self.possibilities = None # self.possibilities = None
elif key == "Y": self.level.split(single=True) #elif key == "Y": self.level.split(single=True)
elif key == "U": self.level.highlight(+1) #elif key == "U": self.level.highlight(+1)
elif key == "I": self.level.highlight(-1) #elif key == "I": self.level.highlight(-1)
elif key == "O": #elif key == "O":
self.level.wall_rooms() # self.level.wall_rooms()
self.possibilities = None # self.possibilities = None
#elif key == "P": ct.format_tiles(self.grid) #elif key == "P": ct.format_tiles(self.grid)
#elif key == "P":
#self.possibilities = ct.wfc_pass(self.grid, self.possibilities)
elif key == "P": elif key == "P":
self.possibilities = ct.wfc_pass(self.grid, self.possibilities) self.depth += 1
if d: self.player.try_move(*d) 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): def swap_level(self, new_level, spawn_point):
self.level = new_level self.level = new_level
self.grid = self.level.grid self.grid = self.level.grid
self.grid.zoom = 2.0 self.grid.zoom = 2.0
# TODO, make an entity mover function # TODO, make an entity mover function
self.add_entity(self.player) #self.add_entity(self.player)
self.player.grid = self.grid self.player.grid = self.grid
self.player.draw_pos = spawn_point self.player.draw_pos = spawn_point
self.grid.entities.append(self.player._entity) #self.grid.entities.append(self.player._entity)
try:
self.ui.remove(0)
except:
pass
self.ui.append(self.grid)
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()