closes #18 commitb114ec3085Author: John McCardle <mccardle.john@gmail.com> Date: Thu Mar 21 22:22:35 2024 -0400 cleaning up for merge commitd7228172c4Author: John McCardle <mccardle.john@gmail.com> Date: Thu Mar 21 21:39:15 2024 -0400 Messy, but monumental: PyTexture::pyObject works this also coincidentally fixes a weird bug I encountered while (mis?)using tp_alloc: by using PyType_GenericAlloc, I avoid the segfault that tp_alloc sometimes causes. See the horrible UIDrawable retrieval macro that I use in UI.h for a workaround that can probably be replaced with this technique commit2cf8f94310Author: John McCardle <mccardle.john@gmail.com> Date: Wed Mar 20 21:16:52 2024 -0400 Radical new example pattern for exposing a C++ class to Python commit84a8886da2Author: John McCardle <mccardle.john@gmail.com> Date: Sun Mar 17 16:29:33 2024 -0400 Fixed render issue with UIGrid / PyTexture: wasn't positioning or scaling properly after fetching sprite commit20f80c4114Author: John McCardle <mccardle.john@gmail.com> Date: Sun Mar 17 16:23:52 2024 -0400 Fixed sprite indexing error in PyTexture; needs non-square sprite tests, but feeling confident! commitafd4ff1925Author: John McCardle <mccardle.john@gmail.com> Date: Sat Mar 16 21:53:24 2024 -0400 good progress, we're building again. Issue with Grid (tile sprite) textures and I think the sprite indexes are being calculated wrong (x and y transposed?) commitbfd33102d1Author: John McCardle <mccardle.john@gmail.com> Date: Sat Mar 16 14:52:35 2024 -0400 Squashed basically all the compile bugs in UISprite, but UIEntity and UIGrid use textures as well, so they need to be fixed too before the project will build again commit47d0e34a17Author: John McCardle <mccardle.john@gmail.com> Date: Sat Mar 16 11:31:39 2024 -0400 Initial PyTexture class no testing done. should enable rectangular (non-square) textures "sprite" method; let's just overwrite sprites with texture coords Hoping to replace awful code like: `self->data->sprite.sprite.setTextureRect(self->data->sprite.itex->spriteCoordinates(val));` with something like: `self->data->sprite = self->data->texture->sprite(val);`
300 lines
10 KiB
Python
300 lines
10 KiB
Python
import mcrfpy
|
|
mcrfpy.createScene("play")
|
|
ui = mcrfpy.sceneUI("play")
|
|
t = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) # 12, 11)
|
|
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
|
|
|
|
frame_color = (64, 64, 128)
|
|
|
|
grid = mcrfpy.Grid(20, 15, t, 10, 10, 800, 595)
|
|
grid.zoom = 2.0
|
|
entity_frame = mcrfpy.Frame(815, 10, 194, 595, fill_color = frame_color)
|
|
inventory_frame = mcrfpy.Frame(10, 610, 800, 143, fill_color = frame_color)
|
|
stats_frame = mcrfpy.Frame(815, 610, 194, 143, fill_color = frame_color)
|
|
|
|
begin_btn = mcrfpy.Frame(350,250,100,100, fill_color = (255,0,0))
|
|
begin_btn.children.append(mcrfpy.Caption(5, 5, "Begin", font))
|
|
def cos_keys(key, state):
|
|
if key == 'M' and state == 'start':
|
|
mapgen()
|
|
elif state == "end": return
|
|
elif key == "W":
|
|
player.move("N")
|
|
elif key == "A":
|
|
player.move("W")
|
|
elif key == "S":
|
|
player.move("S")
|
|
elif key == "D":
|
|
player.move("E")
|
|
|
|
|
|
def cos_init(*args):
|
|
if args[3] != "start": return
|
|
mcrfpy.keypressScene(cos_keys)
|
|
ui.remove(4)
|
|
|
|
begin_btn.click = cos_init
|
|
|
|
[ui.append(e) for e in (grid, entity_frame, inventory_frame, stats_frame, begin_btn)]
|
|
|
|
import random
|
|
def rcolor():
|
|
return tuple([random.randint(0, 255) for i in range(3)]) # TODO list won't work with GridPoint.color, so had to cast to tuple
|
|
|
|
x_max, y_max = grid.grid_size
|
|
for x in range(x_max):
|
|
for y in range(y_max):
|
|
grid.at((x,y)).color = rcolor()
|
|
|
|
from math import pi, cos, sin
|
|
def mapgen(room_size_max = 7, room_size_min = 3, room_count = 4):
|
|
# reset map
|
|
for x in range(x_max):
|
|
for y in range(y_max):
|
|
grid.at((x, y)).walkable = False
|
|
grid.at((x, y)).transparent= False
|
|
grid.at((x,y)).tilesprite = random.choices([40, 28], weights=[.8, .2])[0]
|
|
global cos_entities
|
|
for e in cos_entities:
|
|
e.e.position = (999,999) # TODO
|
|
e.die()
|
|
cos_entities = []
|
|
|
|
#Dungeon generation
|
|
centers = []
|
|
attempts = 0
|
|
while len(centers) < room_count:
|
|
# Leaving this attempt here for later comparison. These rooms sucked.
|
|
# overlapping, uninteresting hallways, crowded into the corners sometimes, etc.
|
|
attempts += 1
|
|
if attempts > room_count * 15: break
|
|
# room_left = random.randint(1, x_max)
|
|
# room_top = random.randint(1, y_max)
|
|
|
|
# Take 2 - circular distribution of rooms
|
|
angle_mid = (len(centers) / room_count) * 2 * pi + 0.785
|
|
angle = random.uniform(angle_mid - 0.25, angle_mid + 0.25)
|
|
radius = random.uniform(3, 14)
|
|
room_left = int(radius * cos(angle)) + int(x_max/2)
|
|
if room_left <= 1: room_left = 1
|
|
if room_left > x_max - 1: room_left = x_max - 2
|
|
room_top = int(radius * sin(angle)) + int(y_max/2)
|
|
if room_top <= 1: room_top = 1
|
|
if room_top > y_max - 1: room_top = y_max - 2
|
|
room_w = random.randint(room_size_min, room_size_max)
|
|
if room_w + room_left >= x_max: room_w = x_max - room_left - 2
|
|
room_h = random.randint(room_size_min, room_size_max)
|
|
if room_h + room_top >= y_max: room_h = y_max - room_top - 2
|
|
#print(room_left, room_top, room_left + room_w, room_top + room_h)
|
|
if any( # centers contained in this randomly generated room
|
|
[c[0] >= room_left and c[0] <= room_left + room_w and c[1] >= room_top and c[1] <= room_top + room_h for c in centers]
|
|
):
|
|
continue # re-randomize the room position
|
|
centers.append(
|
|
(int(room_left + (room_w/2)), int(room_top + (room_h/2)))
|
|
)
|
|
|
|
for x in range(room_w):
|
|
for y in range(room_h):
|
|
grid.at((room_left+x, room_top+y)).walkable=True
|
|
grid.at((room_left+x, room_top+y)).transparent=True
|
|
grid.at((room_left+x, room_top+y)).tilesprite = random.choice([48, 49, 50, 51, 52, 53])
|
|
|
|
# generate a boulder
|
|
if (room_w > 2 and room_h > 2):
|
|
room_boulder_x, room_boulder_y = random.randint(room_left+1, room_left+room_w-1), random.randint(room_top+1, room_top+room_h-1)
|
|
cos_entities.append(BoulderEntity(room_boulder_x, room_boulder_y))
|
|
|
|
print(f"{room_count} rooms generated after {attempts} attempts.")
|
|
#print(centers)
|
|
# hallways
|
|
pairs = []
|
|
for c1 in centers:
|
|
for c2 in centers:
|
|
if c1 == c2: continue
|
|
if (c2, c1) in pairs or (c1, c2) in pairs: continue
|
|
left = min(c1[0], c2[0])
|
|
right = max(c1[0], c2[0])
|
|
top = min(c1[1], c2[1])
|
|
bottom = max(c1[1], c2[1])
|
|
|
|
corners = [(left, top), (left, bottom), (right, top), (right, bottom)]
|
|
corners.remove(c1)
|
|
corners.remove(c2)
|
|
random.shuffle(corners)
|
|
target, other = corners
|
|
for x in range(target[0], other[0], -1 if target[0] > other[0] else 1):
|
|
was_walkable = grid.at((x, target[1])).walkable
|
|
grid.at((x, target[1])).walkable=True
|
|
grid.at((x, target[1])).transparent=True
|
|
if not was_walkable:
|
|
grid.at((x, target[1])).tilesprite = random.choices([0, 12, 24], weights=[.6, .3, .1])[0]
|
|
for y in range(target[1], other[1], -1 if target[1] > other[1] else 1):
|
|
was_walkable = grid.at((target[0], y)).walkable
|
|
grid.at((target[0], y)).walkable=True
|
|
grid.at((target[0], y)).transparent=True
|
|
if not was_walkable:
|
|
grid.at((target[0], y)).tilesprite = random.choices([0, 12, 24], weights=[0.4, 0.3, 0.3])[0]
|
|
pairs.append((c1, c2))
|
|
|
|
|
|
# spawn exit and button
|
|
spawn_points = []
|
|
for x in range(x_max):
|
|
for y in range(y_max):
|
|
if grid.at((x, y)).walkable:
|
|
spawn_points.append((x, y))
|
|
random.shuffle(spawn_points)
|
|
door_spawn, button_spawn = spawn_points[:2]
|
|
cos_entities.append(ExitEntity(*door_spawn, *button_spawn))
|
|
|
|
# respawn player
|
|
global player
|
|
if player:
|
|
player.position = (999,999) # TODO - die() is broken and I don't know why
|
|
player = PlayerEntity()
|
|
|
|
|
|
#for x in range(x_max):
|
|
# for y in range(y_max):
|
|
# if grid.at((x, y)).walkable:
|
|
# #grid.at((x,y)).tilesprite = random.choice([48, 49, 50, 51, 52, 53])
|
|
# pass
|
|
# else:
|
|
# #grid.at((x,y)).tilesprite = random.choices([40, 28], weights=[.8, .2])[0]
|
|
|
|
#131 - last sprite
|
|
#123, 124 - brown, grey rats
|
|
#121 - ghost
|
|
#114, 115, 116 - green, red, blue potion
|
|
#102 - shield
|
|
#98 - low armor guy, #97 - high armor guy
|
|
#89 - chest, #91 - empty chest
|
|
#84 - wizard
|
|
#82 - barrel
|
|
#66 - boulder
|
|
#64, 65 - graves
|
|
#48 - 53: ground (not going to figure out how they fit together tonight)
|
|
#42 - button-looking ground
|
|
#40 - basic solid wall
|
|
#36, 37, 38 - wall (left, middle, right)
|
|
#28 solid wall but with a grate
|
|
#21 - wide open door, 33 medium open, 45 closed door
|
|
#0 - basic dirt
|
|
class MyEntity:
|
|
def __init__(self, x=0, y=0, sprite_num=86):
|
|
self.e = mcrfpy.Entity(x, y, t, sprite_num)
|
|
grid.entities.append(self.e)
|
|
def die(self):
|
|
for i in range(len(grid.entities)):
|
|
e = grid.entities[i]
|
|
if e == self.e:
|
|
grid.entities.remove(i)
|
|
break
|
|
def bump(self, other, dx, dy):
|
|
raise NotImplementedError
|
|
|
|
def try_move(self, dx, dy):
|
|
tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy)
|
|
for e in cos_entities:
|
|
if e.e.position == (tx, ty):
|
|
#print(f"bumping {e}")
|
|
return e.bump(self, dx, dy)
|
|
if tx < 0 or tx >= x_max:
|
|
#print("out of bounds horizontally")
|
|
return False
|
|
if ty < 0 or ty >= y_max:
|
|
#print("out of bounds vertically")
|
|
return False
|
|
if grid.at((tx, ty)).walkable == True:
|
|
#print("Motion!")
|
|
self.e.position = (tx, ty)
|
|
return True
|
|
else:
|
|
#print("Bonk")
|
|
return False
|
|
|
|
def _relative_move(self, dx, dy):
|
|
tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy)
|
|
self.e.position = (tx, ty)
|
|
|
|
|
|
def move(self, direction):
|
|
if direction == "N":
|
|
self.try_move(0, -1)
|
|
elif direction == "E":
|
|
self.try_move(1, 0)
|
|
elif direction == "S":
|
|
self.try_move(0, 1)
|
|
elif direction == "W":
|
|
self.try_move(-1, 0)
|
|
|
|
cos_entities = []
|
|
|
|
class PlayerEntity(MyEntity):
|
|
def __init__(self):
|
|
# find spawn point
|
|
spawn_points = []
|
|
for x in range(x_max):
|
|
for y in range(y_max):
|
|
if grid.at((x, y)).walkable:
|
|
spawn_points.append((x, y))
|
|
random.shuffle(spawn_points)
|
|
for spawn in spawn_points:
|
|
for e in cos_entities:
|
|
if e.e.position == spawn: break
|
|
else:
|
|
break
|
|
|
|
#print(f"spawn at {spawn}")
|
|
super().__init__(spawn[0], spawn[1], sprite_num=84)
|
|
|
|
class BoulderEntity(MyEntity):
|
|
def __init__(self, x, y):
|
|
super().__init__(x, y, 66)
|
|
|
|
def bump(self, other, dx, dy):
|
|
if type(other) == BoulderEntity:
|
|
#print("Boulders can't push boulders")
|
|
return False
|
|
tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy)
|
|
# Is the boulder blocked the same direction as the bumper? If not, let's both move
|
|
old_pos = int(self.e.position[0]), int(self.e.position[1])
|
|
if self.try_move(dx, dy):
|
|
other.e.position = old_pos
|
|
return True
|
|
|
|
class ButtonEntity(MyEntity):
|
|
def __init__(self, x, y, exit):
|
|
super().__init__(x, y, 42)
|
|
self.exit = exit
|
|
|
|
def bump(self, other, dx, dy):
|
|
if type(other) == BoulderEntity:
|
|
self.exit.unlock()
|
|
other._relative_move(dx, dy)
|
|
return True
|
|
|
|
class ExitEntity(MyEntity):
|
|
def __init__(self, x, y, bx, by):
|
|
super().__init__(x, y, 45)
|
|
self.my_button = ButtonEntity(bx, by, self)
|
|
self.unlocked = False
|
|
global cos_entities
|
|
cos_entities.append(self.my_button)
|
|
|
|
def unlock(self):
|
|
self.e.sprite_number = 21
|
|
self.unlocked = True
|
|
|
|
def lock(self):
|
|
self.e.sprite_number = 45
|
|
self.unlocked = True
|
|
|
|
def bump(self, other, dx, dy):
|
|
if type(other) == BoulderEntity:
|
|
return False
|
|
if self.unlocked:
|
|
other._relative_move(dx, dy)
|
|
|
|
player = None
|