title texture
CatRoom
{ "packages": ["Pillow"] } import random import math import os import asyncio import json import js from js import document from js import window from enum import IntEnum from pyodide.ffi import create_proxy from PIL import Image, ImageFilter from datetime import datetime ###FISHING VARS fish_div = document.getElementById('fish') local_storage = window.localStorage scenes = {} animation_data = {} fish_data = {} ###OBJECT class Canvas_Object: def __init__(self, x:float=0, y:float=0, width:float=1, height:float=1,sprite=None, animation_data = {}, name:str="Object", dialog_data = {}): self.notify_state_change = {} self.collison_handler = None self.message_handler = None #This is more of an abstract call. it currently for testing dialog. baislcy the mesage is that it want to speak something. (aka say:meow!) #animation_data = {} self.animation = "idle" self.direction : list[float] = [1.0,0.0] #should only be -1, 0, or 1 self.x : float = x self.y : float = y self.width : float = width self.height : float = height self.sprite = sprite self.animation_data = animation_data self.animation_speed : float = 1.0 self.name = name self.dialog_source = dialog_data self.data = {} self.navigation = None def render(self, ctx,x_scale: float = 1.0, y_scale: float = 1.0, delta : float = 1.0): if self.sprite != None: if not self.animation_data: ctx.drawImage(self.sprite, self.x, self.y, self.width, self.height) else: frame_width = self.animation_data["tile_x"] frame_height = self.animation_data["tile_y"] frame_x = 0 frame_y = 0 if self.animation in self.animation_data: frames_data = self.animation_data[self.animation] delay = 0 frame_ticks = 0 if "frame_delay" in frames_data: delay = frames_data["frame_delay"] if delay != 0 and self.animation_speed != 0: #this is a fail check since dividing zero pcs usally hate. also would be pause or max speed in this case delay = frames_data["frame_delay"] / (self.animation_speed * delta) if "frame_ticks" in frames_data: frame_ticks = frames_data["frame_ticks"] current_frame = 0 if "current_frame" in frames_data: current_frame = frames_data["current_frame"] frame_x = frames_data["frames"][current_frame][0] * frame_width frame_y = frames_data["frames"][current_frame][1] * frame_height if self.animation_speed == 0: self.animation_data[self.animation]["current_frame"] = 0 else: if frame_ticks >= delay: if (current_frame + 1) < len(frames_data["frames"]) and current_frame >= 0: self.animation_data[self.animation]["current_frame"] = current_frame + 1 else: self.animation_data[self.animation]["current_frame"] = 0 self.animation_data[self.animation]["frame_ticks"] = 0 else: self.animation_data[self.animation]["frame_ticks"] = frame_ticks + 1 ctx.drawImage( self.sprite, frame_x, frame_y, frame_width, frame_height, self.x, self.y, self.width * x_scale, self.height * y_scale ) def on_clicked(self)->bool: if True: #Note: no logic here, so returning false #below is an example of a change in state and a vaild clickable object return False else: self.notify(self.notify_state_change) return True def ai_update(self, wolrd_ctx = None, delta : float = 1.0): pass def notify(self,notify_dict): for listerner in notify_dict: notify_dict[listerner](self) def set_direction(self,x:float=0.0,y:float=0.0): self.direction[0] = x self.direction[1] = y def move_to(self,x:float=0.0,y:float=0.0, x_speed:float=1.0, y_speed:float=1.0): direction = self.normalize(x - self.x, y - self.y) self.set_direction(direction[0],direction[1]) self.x += self.direction[0] * x_speed self.y += self.direction[1] * y_speed def vector_magnitude(self,x:float=0.0, y:float=0.0) -> float: return math.sqrt(x * x + y * y) def normalize(self,x:float=0.0, y:float=0.0) -> list[float]: return_value = [x,y] if x != 0 and y != 0: magnitude = self.vector_magnitude(x,y) return_value[0] = x/magnitude return_value[1] = y/magnitude return return_value def __str__(self): return ("Canvas_Object") ###FISH class AI_STATE(IntEnum): IDLE = 0 SWIMING = 1 PANICKED = 2 HOOKED = 3 SUBMERGING = 4 SUBMERGED = 5 SURFACING = 6 class Fish(Canvas_Object): def __init__(self, x:float=0, y:float=0, width:float=1, height:float=1,sprite=None, animation_data = {}, name:str="Fish", dialog_data = None): super().__init__(x,y,width,height,sprite,animation_data,name,dialog_data) self.ai_state : AI_STATE = AI_STATE.IDLE self.animation = "swim_right" self.speed = [3.0,1.0] self.speed_mod : float = 1.0 self.move_location = [0,0] self.default_animation_speed : float = 3.0 self.weight : float = 1.0 self.wait_time : float = 0.0 self.panic_time : float = 0.0 self.submerge_time : float = 0.0 self.submerge_depth : float = 0.0 self.target = None #if not hooked, then will use this to try to swim to or decide to ignore. if hooked, then will follow/stick-to it self.stamina : float = 100.0 self.stamina_regen : float = 0.1 #this is base off of seconds. so delta of 0.1 would regen 0.01 per tick def on_clicked(self)->bool: self.set_ai_state(AI_STATE.PANICKED) return True def ai_update(self,wolrd_ctx = None, delta : float = 1.0): if self.ai_state == AI_STATE.IDLE: self.wait_update(delta) pass elif self.ai_state == AI_STATE.SWIMING: if self.swiming_update(delta): if self.target != None: length = self.vector_magnitude(self.target.x - self.x ,self.target.y - self.y) if length < 8: self.message_handler(self,"Bite", self.name) else: self.set_ai_state(AI_STATE.IDLE) else: self.set_ai_state(AI_STATE.IDLE) pass elif self.ai_state == AI_STATE.HOOKED: self.hook_update(delta) elif self.ai_state == AI_STATE.PANICKED: if self.swiming_update(delta): self.set_random_location() self.panic_update(delta) elif self.ai_state == AI_STATE.SUBMERGED: if self.swiming_update(delta): self.set_random_location() self.submerge_update(delta) pass elif self.ai_state == AI_STATE.SURFACING: if self.swiming_update(delta): self.set_random_location() self.surfacing_update(delta) pass elif self.ai_state == AI_STATE.SUBMERGING: if self.swiming_update(delta): self.set_random_location() self.submerging_update(delta) self.animation_speed = self.default_animation_speed * self.speed_mod if wolrd_ctx != None and (self.ai_state == AI_STATE.SWIMING or self.ai_state == AI_STATE.IDLE): if "bobber" in wolrd_ctx: bobber = wolrd_ctx["bobber"] bobber_x = bobber.x - bobber.width/2 bobber_y = bobber.y - bobber.height/2 dist_from_bobber = self.vector_magnitude(bobber_x - self.x ,bobber_y - self.y) if dist_from_bobber < 32: #note this value may be something gain from the bobber data modifed by the fish senses if round(bobber_x) != round(self.x) and round(bobber_y) != round(self.y): self.move_location[0] = bobber_x self.move_location[1] = bobber_y else: self.message_handler(self,"Bite", self.name) def hook_update(self,delta : float = 0.1): if self.target == None: self.set_ai_state(AI_STATE.IDLE) else: length = self.vector_magnitude(self.target.x - self.x , self.target.y - self.y) if length > 32: self.target = None else: self.x = self.target.x - self.target.width self.y = self.target.y - self.target.height def swiming_update(self, delta : float = 0.1) -> bool: if round(self.move_location[0]) != round(self.x) and round(self.move_location[1]) != round(self.y): self.move_to(self.move_location[0],self.move_location[1],self.speed[0]*self.speed_mod,self.speed[1]*self.speed_mod) else: return True return False def wait_update(self,delta : float = 0.1): if self.wait_time > 0.0: self.wait_time = self.wait_time - delta else: if self.target != None: self.move_location[0] = self.target.x - self.target.width self.move_location[1] = self.target.y - self.target.height else: self.set_random_location() self.set_ai_state(AI_STATE.SWIMING) def panic_update(self,delta : float = 0.1): if self.panic_time > 0.0: self.panic_time = self.panic_time - delta else: self.set_ai_state(AI_STATE.IDLE) def submerge_update(self,delta : float = 0.1): if self.submerge_time > 0.0: self.submerge_time = self.submerge_time - delta else: self.set_ai_state(AI_STATE.SURFACING) def surfacing_update(self,delta : float = 0.1): if self.submerge_depth < 0.0: self.submerge_depth = self.submerge_depth + delta * random.random() else: self.set_ai_state(AI_STATE.IDLE) def submerging_update(self,delta : float = 0.1): if self.submerge_depth > -1.0: self.submerge_depth = self.submerge_depth - delta * random.random() else: self.set_ai_state(AI_STATE.SUBMERGED) def set_ai_state(self,new_state:int): if self.ai_state != new_state: self.ai_state = new_state if self.ai_state == AI_STATE.IDLE: self.wait_time = random.randint(1,25) elif self.ai_state == AI_STATE.PANICKED: self.panic_time = random.randint(5,50) self.speed_mod = 2.0 elif self.ai_state == AI_STATE.SUBMERGED: self.submerge_time = random.randint(5,50) self.submerge_depth = -1.0 else: self.speed_mod = 1.0 def set_direction(self,x:float=0.0,y:float=0.0): super().set_direction(x,y) if self.direction[0] > 0: self.animation = "swim_right" elif self.direction[0] < 0: self.animation = "swim_left" def set_random_location(self,bounds = [0,0,0,0]): if self.navigation != None and bounds == [0,0,0,0]: if "bounds" in self.navigation: bounds = self.navigation["bounds"] self.move_location[0] = random.randrange(bounds[0],bounds[2]) self.move_location[1] = random.randrange(bounds[1],bounds[3]) def render(self, ctx,x_scale: float = 1.0, y_scale: float = 1.0, delta : float = 1.0): ctx.globalAlpha = max(self.submerge_depth+1,1) super().render(ctx, x_scale, y_scale, delta) ctx.globalAlpha = 1.0 def __str__(self): return self.name ###SCENE class Scene: def __init__(self, id, events): self._events = {} #moving events here so thet can be unquie print("test") self.source = document.getElementById(id) self.bounds = [0,0,self.source.width,self.source.height] for event_id in events: self.add_event(event_id,events[event_id]) def __del__(self): event_ref = self._events.copy() for event_id in event_ref: self.remove_event(event_id) def add_event(self, event_id, event): event_proxy = create_proxy(event) self.source.addEventListener(event_id, event_proxy) self._events[event_id] = event_proxy def remove_event(self, event_id): event_proxy = self._events[event_id] self.source.removeEventListener(event_id, event_proxy) event_proxy.destroy() del self._events[event_id] def handle_event(self, event): pass def update(self): pass class Canvas_Scene(Scene): def __init__(self, id, events): super().__init__(id, events) self.objects = [] self.is_mousedown = False #this is a shared flag so that mouse down and up events can be handled without recreating the logic def render(self, delta : float = 1.0): ctx = self.source.getContext("2d") width = self.source.width height = self.source.height ctx.reset() for obj in self.objects: obj.ai_update(None,delta) obj.render(ctx, 1.0, 1.0, delta) def message_handler(self,object,type,message): pass def collison_handler(self,x:float=0,y:float=0,z:float=0,data=None)-> dict: max_x =self.bounds[2] max_y = self.bounds[3] data = {"blocked":False} if x >= max_x or x <= self.bounds[0]: data["blocked"] = True data["x_blocked"] = True if y >= max_y or y <= self.bounds[1]: data["blocked"] = True data["y_blocked"] = True return data def assign_object(self,obj): self.objects.append(obj) obj.notify_state_change[self]=self.on_object_state_change obj.collison_handler = self.collison_handler obj.message_handler = self.message_handler def _mousedown(self) -> bool: state_change = self.is_mousedown != True self.is_mousedown = True return state_change def _mouseup(self)-> bool: state_change = self.is_mousedown != False self.is_mousedown = False return state_change ###MAIN SCENE class Main_Scene(Canvas_Scene): def __init__(self, id, events): super().__init__(id, events) self.objects = [] self.title_text:str = "Title" self.title_source = None self.dialog_source = None def set_title(self, title:str): if self.title_source != None: self.title_source.textContent = title def set_dialog(self, dialog:str): if self.dialog_source != None: self.dialog_source.textContent = dialog def on_object_state_change(self,obj): self.render() def handle_event(self, event): time_triggered = datetime.now() if event.type == "mousedown" or event.type == "touchstart": bounds = self.source.getBoundingClientRect() if event.type == "touchstart": if len(event.touches) >= 0: touch = event.touches[0] x = touch.clientX - bounds.left y = touch.clientY - bounds.top else: return else: x = event.clientX - bounds.left y = event.clientY - bounds.top for obj in self.objects: if x >= obj.x and x <= obj.x + obj.width: if y >= obj.y and y <= obj.y + obj.height: if obj.on_clicked(): break def render(self, delta : float = 1.0): ctx = self.source.getContext("2d") width = self.source.width height = self.source.height ctx.reset() for obj in self.objects: obj.ai_update(None,delta) obj.render(ctx,1.0,1.0,delta) def message_handler(self,object,type,message): if type == "Say": self.set_dialog(str(object.data["name"]) + " : " + str(message)) def __str__(self): return ("Main_Scene") class INTERACTION_STATE(IntEnum): READY = 0 REELING = 1 CASTED = 2 HOOKED = 3 CATCH = 4 class Fishing_Scene(Canvas_Scene): def __init__(self, id, events): super().__init__(id, events) self.objects = [] self.title_text:str = "Title" self.title_source = None self.dialog_source = None self.reeling_strength = 0.0 self.bobber = Bobber(0,0,8,8) self.bounds = [0,150,self.source.width,min(300,self.source.height)] self.interaction_state : INTERACTION_STATE = INTERACTION_STATE.READY self.fish_data = {} self.fish_caught : Fish = None self.tagged_fish = [] def set_title(self, title:str): if self.title_source != None: self.title_source.textContent = title def set_dialog(self, dialog:str): if self.dialog_source != None: self.dialog_source.textContent = dialog def handle_event(self, event): ctx = self.source.getContext("2d") if event.type == "mousedown" or event.type == "touchstart": bounds = self.source.getBoundingClientRect() if event.type == "touchstart": if len(event.touches) >= 0: touch = event.touches[0] x = touch.clientX - bounds.left; y = touch.clientY - bounds.top; else: return else: x = event.clientX - bounds.left y = event.clientY - bounds.top self.on_cast(x,y) def on_cast(self, x : float = 0.0, y : float = 0.0): if self.interaction_state == INTERACTION_STATE.READY: for obj in self.objects: dist_x = x - obj.x dist_y = y - obj.y dist = math.sqrt(dist_x * dist_x + dist_y * dist_y ) if dist < 16: obj.set_ai_state(AI_STATE.PANICKED) #obj.on_clicked() if x > self.bounds[0] and x < self.bounds[2] and y > self.bounds[1] and y < self.bounds[3]: self.bobber.x = x self.bobber.y = y self.interaction_state = INTERACTION_STATE.REELING self.set_dialog("") elif self.interaction_state == INTERACTION_STATE.REELING: self.on_reel() def on_hook(self,fish:Fish = None): if fish != None and self.fish_caught == None: self.fish_caught = fish if self.fish_caught.name in self.fish_data.keys(): pass else: self.generate_new_fish(fish) self.fish_caught.set_ai_state(AI_STATE.HOOKED) self.set_dialog(str(fish.name) + " is hooked.") def on_catch(self): if self.fish_caught != None: weight = "{:.2f}".format(self.fish_caught.weight) self.set_dialog(str(self.fish_caught.name) + " caught weighing " + weight +"kg") self.fish_caught.set_ai_state(AI_STATE.SUBMERGED) self.fish_caught = None self.interaction_state = INTERACTION_STATE.READY pass def on_reel(self) : self.reeling_strength = min(self.reeling_strength + 1,25) def reeling_update(self,delta): center = self.source.width/2 fish_strength = 0.0 if self.fish_caught != None: fish_strength = random.random()*self.fish_caught.weight reeling_strength = (self.reeling_strength - fish_strength) * delta clamped_reeling_strength = min(max(reeling_strength,0.01),4) if reeling_strength > 0: self.bobber.move_to(center,self.source.height,clamped_reeling_strength,clamped_reeling_strength) if self.is_reeled_in(): self.interaction_state = INTERACTION_STATE.READY self.reeling_strength = 0 self.on_catch() elif reeling_strength < 0: pass if self.reeling_strength > 0: self.reeling_strength = max(self.reeling_strength - delta,0) def is_reeled_in(self): return self.bobber.y >= self.source.height def is_holding_button(self,x:float=0.0,y:float=0.0) -> bool: if self.is_mousedown: button_bounds = self.get_button_bounds() if x >= button_bounds[0] and x <= button_bounds[0]+button_bounds[2]: if y >= button_bounds[1] and y <= button_bounds[1]+button_bounds[3]: return True return False def render(self, delta : float = 1.0): ctx = self.source.getContext("2d") width = self.source.width height = self.source.height ctx.reset() self.reeling_update(delta) for obj in self.objects: ratio = obj.y / self.bounds[3] if self.interaction_state == INTERACTION_STATE.REELING and self.fish_caught == None and obj.target == None: length = obj.vector_magnitude(self.bobber.x - obj.x ,self.bobber.y - obj.y) if length < 32: obj.target = self.bobber self.tagged_fish.append(obj) else: obj.targer = None obj.ai_update({"bobber":self.bobber},delta) elif self.fish_caught != None and self.tagged_fish: fish = self.tagged_fish.pop() if fish != self.fish_caught: fish.target = None else: obj.ai_update(None,delta) obj.render(ctx,ratio,ratio,delta) if self.interaction_state == INTERACTION_STATE.REELING: ratio = self.bobber.y / self.bounds[3] self.bobber.render(ctx,ratio,ratio) def draw_bobber(self,ctx): self.bobber.render(ctx) def pick_fish_type(self): fish_types = self.fish_data.keys() if fish_types: return random.choice(list(self.fish_data.keys())) return None def generate_new_fish(self,fish): new_name = self.pick_fish_type() if new_name != None: min_size = self.fish_data[new_name]["min_weight"] max_size = self.fish_data[new_name]["max_weight"] fish.name = new_name fish.weight = random.random() * (max_size - min_size) + min_size def message_handler(self,object,type,message): if type == "Bite": self.on_hook(object) def assign_object(self,obj): super().assign_object(obj) obj.navigation = { "bounds": self.bounds} def on_object_state_change(self,obj): pass def __str__(self): return ("Main_Scene") class Bobber(Canvas_Object): def __init__(self, x:float=0, y:float=0, width:float=1, height:float=1,sprite=None, animation_data = {}, name:str="Bobber", dialog_data = None): super().__init__(x,y,width,height,sprite,animation_data,name,dialog_data) self.move_location = [0,0] def render(self, ctx,x_scale=1.0, y_scale=1.0, delta : float = 1.0): if self.sprite != None: super().render(ctx,x_scale,y_scale) else: ctx.fillStyle = 'blue' ctx.fillRect( self.x-self.width/2, self.y-self.height/2, self.width * x_scale, self.height * y_scale ) ###FISHING MAIN #base url to use from loading hosted files SOURCE_PATH = "./" def handle_events(event): event_owner_id = event.target.id if event_owner_id in scenes: owner = scenes[event_owner_id] owner.handle_event(event) #load json from url and convert it to a python dict async def fetch_json(url): response = await js.fetch(url) data = await response.json() return dict(data.to_py()) #The Main Loop function async def update_tick(): delta : float = 0.1 while True: await asyncio.sleep(delta) scenes["canvas_fishing_room"].render(delta) #Declare common events common_events = { "click":handle_events, "mousedown":handle_events, "touchstart":handle_events, "mouseup":handle_events, "touchend":handle_events, "mousemove":handle_events, "touchmove":handle_events } #Load data from JSON if os.path.exists('animation_data.json'): with open('animation_data.json', 'r') as file: animation_data = json.load(file) else: animation_data = await fetch_json(SOURCE_PATH + 'data/animation_data.json') if os.path.exists('fish_data.json'): with open('fish_data.json', 'r') as file: fish_data = json.load(file) else: fish_data = await fetch_json(SOURCE_PATH + 'data/fish_data.json') #Set up scenes scenes["canvas_fishing_room"] = Fishing_Scene("canvas_fishing_room",common_events) scenes["canvas_fishing_room"].title_source = document.getElementById("title_fishing_room") scenes["canvas_fishing_room"].dialog_source = document.getElementById("dialog_fishing_room") scenes["canvas_fishing_room"].set_title("Fishing Room") scenes["canvas_fishing_room"].fish_data = fish_data bounds = scenes["canvas_fishing_room"].bounds #Spawn Fish for i in range(0,10): scenes["canvas_fishing_room"].assign_object( Fish(random.randint(bounds[0],bounds[2]),random.randint(bounds[1],bounds[3]),16,16,document.getElementById("fish_image"), animation_data["fish"]) ) #init Render scenes["canvas_fishing_room"].render() #Start the Main Loop asyncio.ensure_future(update_tick())