OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / arcade / games / pong / pong_modal.py

559 lines · python ref:
import bpy, time, random
from mathutils import Vector
import random
from ....utility.screen import dpi_factor
from .... utility import addon
from .... utility.base_modal_controls import Base_Modal_Controls
from .... utility import method_handler
from .... import bl_info

# Video
from .... operators.third_party.video_window import Video_Controller
from ....ui_framework.master import Modal_Communications

# Tips
from ....operators.misc.screen_saver_tips import tips
from ....ui_framework.graphics.draw import render_text
from ....ui_framework.utils.geo import get_blf_text_dims

# Engine
from ... engine import *


class HOPS_OT_Arcade_Pong(bpy.types.Operator):
    bl_idname = "hops.pong"
    bl_label = "Play Pong"
    bl_description = "Play pong in the 3D view"
    bl_options = {"REGISTER", "UNDO", "BLOCKING"}


    def invoke(self, context, event):

        ###########################
        # <--- Video Window ---> #
        ###########################
        addon.preference().ui.Hops_modal_video_window_bottom_left = (20,30)
        addon.preference().ui.Hops_modal_video_window_scale = (462,100)
        self.video = Video_Controller(context)
        self.scroll_time = time.time()
        self.scroll_duration = .725
        self.scrolling_up = True

        ###########################
        # <--- Setup Windows ---> #
        ###########################
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        space.show_region_toolbar = False
                        space.show_region_ui = False

        self.scale_factor = dpi_factor()
        self.window_padding = 160 * self.scale_factor

        context.window.cursor_modal_set('HAND')

        ########################
        # <--- Game Props ---> #
        ########################
        self.screen_width = context.area.width - self.window_padding
        self.screen_height = context.area.height - self.window_padding
        self.delta_time = time.time()
        self.last_frame_end_time = time.time()
        self.mouse_pos = (event.mouse_region_x, event.mouse_region_y)
        self.ball_vec = (random.uniform(500, 1000), random.uniform(-1000, 1000))
        self.max_speed = self.screen_width
        self.paddle_padding = 20
        self.player_score = 0
        self.npc_score = 0

        ###########################
        # <--- Tips Display ---> #
        ###########################
        self.tip_duration = 4
        self.tip_time = time.time()
        self.tip_index = random.randint(0, len(tips) - 1)
        self.tip = tips[self.tip_index]
        self.tip_pos = (context.area.width * .55, context.area.height - self.window_padding)
        self.tip_alpha = 1
        self.tip_color = (.5,.5,.5,1)
        self.tip_header_size = 16
        self.tip_size = 14

        ###########################
        # <--- Drawing Props ---> #
        ###########################
        self.hops_logo_scale = self.screen_height / (100 * self.scale_factor)

        ########################
        # <--- Components ---> #
        ########################

        # Background
        self.background = Rectangle_Comp(use_scale_factor=False)
        self.background.height = context.area.height - self.window_padding * 2
        self.background.width = context.area.width - self.window_padding * 2
        self.background.location = (context.area.width * .5, context.area.height * .5)
        self.background.line_color = (.5,.5,.5,1)
        self.background.poly_color = (0,0,0,.375)

        # Background Lines
        self.bg_lines = Lines_Comp()
        self.bg_lines.line_color = (.5,.5,.5,1)
        self.bg_lines.line_width = 6
        self.bg_lines.vertices = [
            (context.area.width * .5, self.window_padding + self.hops_logo_scale * (12 * self.scale_factor)),
            (context.area.width * .5, self.screen_height)]

        # HOPS Label Text Release Name
        self.hops_label_text = Text_Comp()
        self.hops_label_text.center = False
        self.hops_label_text.font_color = (.5,.5,.5,1)
        self.hops_label_text.font_size = 24
        self.hops_label_text.location = (self.window_padding, self.screen_height + 10 * self.scale_factor)
        self.hops_label_text.text = f"{bl_info['description']}"

        # HOPS Label Text Version Number
        self.hops_label_text_v_number = Text_Comp()
        self.hops_label_text_v_number.center = False
        self.hops_label_text_v_number.font_color = (.5,.5,.5,1)
        self.hops_label_text_v_number.font_size = 24
        self.hops_label_text_v_number.location = (self.screen_width - 220 * self.scale_factor, self.window_padding - 24 * self.scale_factor)
        self.hops_label_text_v_number.text = f"HOps: {bl_info['version'][0]}.{bl_info['version'][1]}.{bl_info['version'][2]}"

        # Hot Key
        self.hot_key = Text_Comp()
        self.hot_key.center = False
        self.hot_key.font_color = (.5,.5,.5,1)
        self.hot_key.font_size = 16
        self.hot_key.location = (self.screen_width - 220 * self.scale_factor, self.window_padding - 48 * self.scale_factor)
        self.hot_key.text = "A : Toggle Animation"

        # Hot Key
        self.hot_key_scroll = Text_Comp()
        self.hot_key_scroll.center = False
        self.hot_key_scroll.font_color = (.5,.5,.5,1)
        self.hot_key_scroll.font_size = 16
        self.hot_key_scroll.location = (self.screen_width - 220 * self.scale_factor, self.window_padding - 72 * self.scale_factor)
        self.hot_key_scroll.text = "Scroll : Adjust Paddle Height"

        # Hot Key
        self.hot_key_links = Text_Comp()
        self.hot_key_links.center = False
        self.hot_key_links.font_color = (.5,.5,.5,1)
        self.hot_key_links.font_size = 16
        self.hot_key_links.location = (self.screen_width - 220 * self.scale_factor, self.window_padding - 96 * self.scale_factor)
        self.hot_key_links.text = "V : Toggle Video Links"

        # Credits Marquee
        self.marquee = Marquee_Comp()
        self.marquee.top_left = (self.window_padding, self.window_padding - 6 * self.scale_factor)
        self.marquee.marquee_text = {} 
        self.marquee.header_font_color = (.5,.5,.5,1)
        self.marquee.body_font_color = (.5,.5,.5,.75)
        self.setup_marquee_text()
        self.marquee.setup()

        # Player Combo Text
        self.player_combo_text = Combo_Counter_Comp()
        self.player_combo_text.lead_font_size = 26
        self.player_combo_text.lead_font_color = (.5,.5,.5,1)
        self.player_combo_text.font_size = 42
        self.player_combo_text.font_color = (0,0,1,1)
        self.player_combo_text.particle_color = (0,0,1,1)
        self.player_combo_text.location = (self.player_combo_text.font_size * 2 + self.window_padding, self.screen_height - self.player_combo_text.font_size * 2)
        self.player_combo_text.current_text = str(self.player_score)

        # NPC Combo Text
        self.npc_combo_text = Combo_Counter_Comp()
        self.npc_combo_text.lead_font_size = 26
        self.npc_combo_text.lead_font_color = (.5,.5,.5,1)
        self.npc_combo_text.font_size = 42
        self.npc_combo_text.font_color = (1,0,0,1)
        self.npc_combo_text.particle_color = (1,0,0,1)
        self.npc_combo_text.location = (self.screen_width - self.player_combo_text.font_size * 3, self.screen_height - self.player_combo_text.font_size * 2)
        self.npc_combo_text.current_text = str(self.npc_score)

        # Main Player
        self.main_player = Rectangle_Comp()
        self.main_player.height = 70
        self.main_player.width = 20
        self.main_player.line_color = (0,0,1,1)
        self.main_player.poly_color = (0,0,.5,1)

        # NPC
        self.npc = Rectangle_Comp()
        self.npc.height = 70
        self.npc.width = 20
        self.npc.line_color = (1,0,0,1)
        self.npc.poly_color = (.5,0,0,1)

        # Ball Trail Render
        self.trail_render = Trail_Render_Comp()
        self.trail_render.line_color = (.5,.5,.5,1)

        # Ball
        self.ball = Circle_Comp()
        self.ball.location = (context.area.width * .5, context.area.height * .5)
        self.ball.radius = 10
        self.ball.line_color = (1,1,1,1)
        self.ball.poly_color = (.5,.5,.5,1)

        # Particles
        self.particles = Explosion_Particle_Effect()

        # Store all the objects
        self.components = [
            self.hops_label_text,
            self.hops_label_text_v_number,
            self.hot_key,
            self.hot_key_scroll,
            self.hot_key_links,
            self.marquee,
            self.background,
            self.bg_lines,
            self.player_combo_text,
            self.npc_combo_text,
            self.main_player,
            self.npc,
            self.trail_render,
            self.ball,
            self.particles]

        ##########################
        # <--- Base Systems ---> #
        ##########################
        self.base_controls = Base_Modal_Controls(context, event)
        self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_shader, (context,), 'WINDOW', 'POST_PIXEL')
        self.timer = context.window_manager.event_timer_add(0.0166, window=context.window)

        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}


    def modal(self, context, event):

        # FPS
        self.delta_time = time.time() - self.last_frame_end_time

        ##########################
        # <--- Base Systems ---> #
        ##########################
        self.base_controls.update(context, event)

        ##########################
        # <--- Tips Display ---> #
        ##########################
        self.update_tips()

        ##########################
        # <--- Video Window ---> #
        ##########################
        self.video.update(context, event)
        if time.time() - self.scroll_time > self.scroll_duration:
            self.scroll_time = time.time()           
            self.update_video()

        ########################
        # <--- Game Props ---> #
        ########################
        self.mouse_pos = (event.mouse_region_x, event.mouse_region_y)

        ##########################
        # <--- Game Updates ---> #
        ##########################
        self.update(context, event)

        # Base controls
        if self.base_controls.cancel:
            self.remove_shader()
            self.video.shut_down()
            context.window_manager.event_timer_remove(self.timer)
            context.area.tag_redraw()
            context.window.cursor_modal_set('DEFAULT')
            return {'CANCELLED'}

        elif self.base_controls.confirm:
            self.remove_shader()
            self.video.shut_down()
            context.window_manager.event_timer_remove(self.timer)
            context.area.tag_redraw()
            context.window.cursor_modal_set('DEFAULT')
            return {'FINISHED'}

        # Toggle the viewport animation
        if event.type == "A" and event.value == "PRESS":
            bpy.ops.screen.animation_play('INVOKE_DEFAULT')

        # Toggle video links
        if event.type == "V" and event.value == "PRESS":
            self.toggle_video_display()

        if self.base_controls.scroll:
            if self.base_controls.scroll > 0:
                self.adjust_player_paddle_size(scale_factor=5)
            else:
                self.adjust_player_paddle_size(scale_factor=-5)

        self.last_frame_end_time = time.time()
        context.area.tag_redraw()
        return {"RUNNING_MODAL"}

    
    def update(self, context, event):
        '''Setup game objects.'''

        self.main_player_update(context)
        self.npc_update(context)
        self.ball_location_update(context)
        self.trail_render.update(new_point=self.ball.location)
        self.particles.update(self.delta_time)


    def safe_draw_shader(self, context):
        method_handler(self.draw_shader,
            arguments = (context,),
            identifier = 'Pong Game',
            exit_method = self.remove_shader)


    def remove_shader(self):
        '''Remove shader handle.'''

        if self.draw_handle:
            self.draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")


    def draw_shader(self, context):
        '''Draw shader handle.'''

        # Draw the hops logo
        color = (0,0,0,.5)
        if self.player_score > self.npc_score:
            color = (0,0,1,.5)
        elif self.player_score < self.npc_score:
            color = (1,0,0,.5)

        draw_arcade_hops_logo(
            color=color,
            size=self.hops_logo_scale, 
            x=(context.area.width * .5) - (self.hops_logo_scale * .5) * (10 * self.scale_factor),
            y=self.window_padding + (10 * self.scale_factor))

        # Draw game
        for component in self.components:
            component.draw(context)

        self.draw_tips()


    def draw_tips(self):
        y_offset = 8 * dpi_factor()
        for index, tip in enumerate(reversed(self.tip)):
            size = self.tip_header_size if index == len(self.tip) - 1 else self.tip_size
            render_text(tip, (self.tip_pos[0], self.tip_pos[1] + y_offset), size, self.tip_color)
            y_offset += get_blf_text_dims(tip, size)[1] + 8 * dpi_factor()


    def update_tips(self):
        t = time.time()
        diff = t - self.tip_time

        # Reset
        if diff > self.tip_duration:
            self.tip_time = t
            self.tip_index = random.randint(0, len(tips) - 1)
            self.tip = tips[self.tip_index]
            self.tip_alpha = 1
            self.tip_color = (.5,.5,.5,1)

        # Fade
        elif diff >= self.tip_duration * .75:
            hold_dur = self.tip_duration * .75
            fade_dur = self.tip_duration * .25
            fade_amt = diff - hold_dur
            self.tip_alpha = 1 - fade_amt
            self.tip_alpha = self.tip_alpha if self.tip_alpha <= 1 else 1 
            self.tip_color = (.5,.5,.5,self.tip_alpha)


    def main_player_update(self, context):
        '''Update the player paddle.'''

        self.main_player.location = (self.window_padding + self.paddle_padding, self.mouse_pos[1])

        # Paddle Y Clamp
        if self.main_player.location[1] + self.main_player.height * .5 >= self.screen_height:
            self.main_player.location = (self.main_player.location[0], self.screen_height - self.main_player.height * .5)

        elif self.main_player.location[1] - self.main_player.height * .5 <= self.window_padding:
            self.main_player.location = (self.main_player.location[0], self.window_padding + self.main_player.height * .5)


    def npc_update(self, context):
        '''Update the player paddle.'''

        chase_increment = (self.ball.location[1] - self.npc.location[1]) * self.delta_time * 15
        idle_increment = (context.area.height * .5 - self.npc.location[1]) * self.delta_time * 5

        # Chase ball when vec is coming towards
        if self.ball_vec[0] > 0:
            self.npc.location = (
                context.area.width - self.window_padding - self.paddle_padding, 
                self.npc.location[1] + chase_increment)
        # Start towards center
        else:
            self.npc.location = (
                context.area.width - self.window_padding - self.paddle_padding, 
                self.npc.location[1] + idle_increment)


        # Paddle Y Clamp
        if self.npc.location[1] + self.npc.height * .5 >= self.screen_height:
            self.npc.location = (self.npc.location[0], self.screen_height - self.npc.height * .5)

        elif self.npc.location[1] - self.npc.height * .5 <= self.window_padding:
            self.npc.location = (self.npc.location[0], self.window_padding + self.npc.height * .5)


    def ball_location_update(self, context):
        '''Update the location of the main ball.'''

        # Move the ball with the ball vec
        self.ball.location = (
            self.ball.location[0] + self.ball_vec[0] * self.delta_time, 
            self.ball.location[1] + self.ball_vec[1] * self.delta_time)

        # Player Paddle
        if self.ball.location[0] + self.ball.radius <= self.main_player.location[0] + self.main_player.width * .5:
            if self.ball.location[1] + self.ball.radius >= self.main_player.location[1] - self.main_player.height * .5:
                if self.ball.location[1] - self.ball.radius <= self.main_player.location[1] + self.main_player.height * .5:
                    x = random.uniform(self.max_speed * .75, self.max_speed)
                    y = random.uniform(-self.max_speed, self.max_speed)
                    self.ball_vec = (x, y)
                    self.ball.location = (
                        self.main_player.location[0] + self.ball.radius + (2 * self.scale_factor) + self.main_player.width * .5, 
                        self.ball.location[1])
                    
        # NPC Paddle
        if self.ball.location[0] + self.ball.radius >= self.npc.location[0] - self.npc.width * .5:
            if self.ball.location[1] + self.ball.radius >= self.npc.location[1] - self.npc.height * .5:
                if self.ball.location[1] - self.ball.radius <= self.npc.location[1] + self.npc.height * .5:
                    x = -random.uniform(self.max_speed * .75, self.max_speed)
                    y = random.uniform(-self.max_speed, self.max_speed)
                    self.ball_vec = (x, y)
                    self.ball.location = (
                        self.npc.location[0] - self.ball.radius - (2 * self.scale_factor) - self.npc.width * .5, 
                        self.ball.location[1])

        # Clamp ball speed
        if self.ball_vec[0] > self.max_speed or self.ball_vec[1] > self.max_speed:
            x_sign = 1 if self.ball_vec[0] >= 0 else -1
            y_sign = 1 if self.ball_vec[1] >= 0 else -1
            self.ball_vec = (self.max_speed * x_sign, self.max_speed * y_sign)

        # Window X Clamp
        if self.ball.location[0] + self.ball.radius >= self.screen_width:
            self.player_scored_point(context)

        elif self.ball.location[0] - self.ball.radius <= self.window_padding:
            self.npc_scored_point(context)

        # Window Y Clamp
        if self.ball.location[1] + self.ball.radius >= self.screen_height:
            self.ball_vec = (self.ball_vec[0], -abs(self.ball_vec[1]))
            self.ball.location = (self.ball.location[0], self.screen_height - self.ball.radius)

        elif self.ball.location[1] - self.ball.radius <= self.window_padding:
            self.ball_vec = (self.ball_vec[0], abs(self.ball_vec[1]))
            self.ball.location = (self.ball.location[0], self.window_padding + self.ball.radius)


    def player_scored_point(self, context):
        '''The ball hit the opposite sides wall.'''

        x = -random.uniform(500, self.max_speed)
        y = random.uniform(-1000, self.max_speed)
        self.player_score += 1
        self.player_combo_text.update_combo(location=self.player_combo_text.location, new_text=str(self.player_score))
        self.ball_vec = (x, y)
        hit_loc = (self.screen_width - self.ball.radius - 2, self.ball.location[1])
        self.particles.spawn_emitter(location=hit_loc, color=(0,0,1,1,), gravity=-1000, force=10, point_size=5, particle_count=26)
        self.ball.location = (context.area.width * .5, context.area.height * .5)
        self.particles.spawn_emitter(location=self.ball.location, color=(.5,.5,.5,1), gravity=-500, force=5, point_size=5, particle_count=26)


    def npc_scored_point(self, context):
        '''The ball hit the opposite sides wall.'''

        x = random.uniform(500, self.max_speed)
        y = random.uniform(-1000, self.max_speed)
        self.npc_score += 1
        self.npc_combo_text.update_combo(location=self.npc_combo_text.location, new_text=str(self.npc_score))
        self.ball_vec = (x, y)
        hit_loc = (self.window_padding + self.ball.radius + 2, self.ball.location[1])
        self.particles.spawn_emitter(location=hit_loc, color=(1,0,0,1,), gravity=-1000, force=10, point_size=5, particle_count=26)
        self.ball.location = (context.area.width * .5, context.area.height * .5)
        self.particles.spawn_emitter(location=self.ball.location, color=(.5,.5,.5,1), gravity=-500, force=5, point_size=5, particle_count=26)


    def setup_marquee_text(self):
        '''Setups all the marquee text.'''

        # Authors
        self.marquee.marquee_text["Authors"] = []
        
        authors = bl_info['author'].split(",")
        for author in authors:
            author = author.strip()
            self.marquee.marquee_text["Authors"].append(author)

        # Addons
        self.marquee.marquee_text["Addons"] = []

        addons = bpy.context.preferences.addons.keys()
        new_addons = []
        for addon in addons:
            addon = addon.strip()
            if addon[:2] != "io":
                self.marquee.marquee_text["Addons"].append(addon)


    def adjust_player_paddle_size(self, scale_factor=1):
        '''Increase the player paddle size by the scale_factor.'''

        if scale_factor > 0:
            if self.main_player.height + scale_factor < self.screen_height * .5:
                self.main_player.adjust_width_height(add_height=scale_factor)
        elif scale_factor < 0:
            if self.main_player.height + scale_factor > 70 * self.scale_factor:
                self.main_player.adjust_width_height(add_height=scale_factor)
            else:
                self.main_player.height = 70

    # --- Video --- #
    def update_video(self):
        if self.video.master.unique_id not in Modal_Communications.coms: return

        db = Modal_Communications.coms[self.video.master.unique_id].db_ref
        window = db.windows['Video_Page']
        panel = window.panels[-1]
        widget = panel.widget

        scroll = 1 if self.scrolling_up else -1
        widget.scroll_bar.bottom_offset += scroll
        widget.scroll_bar.clamp_offset()

        if widget.scroll_bar.max_val:
            self.scrolling_up = False
        elif widget.scroll_bar.min_val:
            self.scrolling_up = True


    def toggle_video_display(self):
        if self.video.master.unique_id not in Modal_Communications.coms: return

        shader = Modal_Communications.coms[self.video.master.unique_id].shader_ref
        if shader: shader.draw_shader = not shader.draw_shader