init
This commit is contained in:
commit
42c5f87a1d
BIN
audio-recorder/cache/segment_01.mp3
vendored
Normal file
BIN
audio-recorder/cache/segment_01.mp3
vendored
Normal file
Binary file not shown.
BIN
audio-recorder/cache/segment_02.mp3
vendored
Normal file
BIN
audio-recorder/cache/segment_02.mp3
vendored
Normal file
Binary file not shown.
BIN
audio-recorder/cache/segment_02.wav
vendored
Normal file
BIN
audio-recorder/cache/segment_02.wav
vendored
Normal file
Binary file not shown.
BIN
audio-recorder/cache/segment_03.wav
vendored
Normal file
BIN
audio-recorder/cache/segment_03.wav
vendored
Normal file
Binary file not shown.
221
audio-recorder/record.py
Normal file
221
audio-recorder/record.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import sounddevice as sd
|
||||||
|
import soundfile as sf
|
||||||
|
import keyboard
|
||||||
|
|
||||||
|
# PARAMETERS – adjust as needed
|
||||||
|
CHANNELS = 2
|
||||||
|
SAMPLE_RATE = 44100
|
||||||
|
BLOCKSIZE = 1024 # frames per chunk
|
||||||
|
|
||||||
|
# Look for "Stereo Mix" which is a common device for capturing system audio
|
||||||
|
devices = sd.query_devices()
|
||||||
|
DEVICE_ID = 14 # Will be set dynamically
|
||||||
|
WASAPI_METHOD = False # First try with Stereo Mix, then try WASAPI if available
|
||||||
|
|
||||||
|
print("Available audio devices:")
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
print(f" {i} {device['name']}, {device['hostapi']} ({device['max_input_channels']} in, {device['max_output_channels']} out)")
|
||||||
|
# First priority: Stereo Mix
|
||||||
|
if "Stereo Mix" in device['name'] and device['max_input_channels'] > 0:
|
||||||
|
DEVICE_ID = i
|
||||||
|
print(f"Selected device {i}: {device['name']} for system audio recording")
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no Stereo Mix, try to find WASAPI devices
|
||||||
|
if DEVICE_ID is None:
|
||||||
|
hostapis = sd.query_hostapis()
|
||||||
|
wasapi_index = None
|
||||||
|
|
||||||
|
for i, api in enumerate(hostapis):
|
||||||
|
if 'WASAPI' in api['name']:
|
||||||
|
wasapi_index = i
|
||||||
|
print(f"Found WASAPI at index {i}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if wasapi_index is not None:
|
||||||
|
# Try to find a WASAPI output device
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
if device['hostapi'] == wasapi_index and device['max_output_channels'] > 0 and 'Speakers' in device['name']:
|
||||||
|
DEVICE_ID = i
|
||||||
|
print(f"Selected device {i}: {device['name']} (will try WASAPI method)")
|
||||||
|
WASAPI_METHOD = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback to any WASAPI output device
|
||||||
|
if DEVICE_ID is None:
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
if device['hostapi'] == wasapi_index and device['max_output_channels'] > 0:
|
||||||
|
DEVICE_ID = i
|
||||||
|
print(f"Selected device {i}: {device['name']} (will try WASAPI method)")
|
||||||
|
WASAPI_METHOD = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# If still no device found, prompt user
|
||||||
|
if DEVICE_ID is None:
|
||||||
|
print("No suitable recording device found. Please select a device manually:")
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
print(f" {i} {device['name']}, {device['hostapi']} ({device['max_input_channels']} in, {device['max_output_channels']} out)")
|
||||||
|
device_id_input = input("Enter device ID to use: ")
|
||||||
|
try:
|
||||||
|
DEVICE_ID = int(device_id_input)
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid device ID")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def record_loop(cut_event: threading.Event):
|
||||||
|
"""
|
||||||
|
Records audio into successive WAV files.
|
||||||
|
When cut_event is set, closes current file, converts it, and starts a new one.
|
||||||
|
"""
|
||||||
|
q = queue.Queue()
|
||||||
|
recording_index = 1
|
||||||
|
current_wav = None
|
||||||
|
wav_file = None
|
||||||
|
|
||||||
|
def callback(indata, frames, time, status):
|
||||||
|
if status:
|
||||||
|
print(f"⚠️ {status}")
|
||||||
|
q.put(indata.copy())
|
||||||
|
|
||||||
|
# Set up recording mode
|
||||||
|
device_info = sd.query_devices(DEVICE_ID)
|
||||||
|
print(f"Using device: {device_info['name']}")
|
||||||
|
|
||||||
|
# Open the input stream - different approach based on whether it's a capture device or WASAPI
|
||||||
|
try:
|
||||||
|
# Use either regular input stream or customized parameters
|
||||||
|
if WASAPI_METHOD:
|
||||||
|
print("Using WASAPI device - if this doesn't work, try enabling 'Stereo Mix' in Windows sound settings")
|
||||||
|
# Use standard input stream with potential extra settings for newer versions
|
||||||
|
stream_kwargs = {
|
||||||
|
'device': DEVICE_ID,
|
||||||
|
'channels': CHANNELS,
|
||||||
|
'samplerate': SAMPLE_RATE,
|
||||||
|
'blocksize': BLOCKSIZE,
|
||||||
|
'callback': callback,
|
||||||
|
'dtype': 'float32'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try adding WASAPI loopback option if available in this version
|
||||||
|
try:
|
||||||
|
stream = sd.InputStream(**stream_kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Standard InputStream failed: {e}")
|
||||||
|
print("Trying with loopback option...")
|
||||||
|
try:
|
||||||
|
# Try with extra settings dictionary instead
|
||||||
|
stream_kwargs['extra_settings'] = {'wasapi_loopback': True}
|
||||||
|
stream = sd.InputStream(**stream_kwargs)
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"WASAPI loopback failed: {e2}")
|
||||||
|
raise Exception("Could not initialize audio stream with WASAPI device") from e2
|
||||||
|
else:
|
||||||
|
# Regular input device like Stereo Mix
|
||||||
|
stream = sd.InputStream(
|
||||||
|
device=DEVICE_ID,
|
||||||
|
channels=CHANNELS,
|
||||||
|
samplerate=SAMPLE_RATE,
|
||||||
|
blocksize=BLOCKSIZE,
|
||||||
|
callback=callback,
|
||||||
|
dtype='float32'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start the stream
|
||||||
|
stream.start()
|
||||||
|
print("▶️ Recording... Press 'c' to cut.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Start a new WAV file
|
||||||
|
current_wav = tempfile.NamedTemporaryFile(
|
||||||
|
prefix=f"segment_{recording_index:02d}_", suffix=".wav", delete=False
|
||||||
|
)
|
||||||
|
wav_file = sf.SoundFile(
|
||||||
|
current_wav.name, mode='w',
|
||||||
|
samplerate=SAMPLE_RATE, channels=CHANNELS, subtype='FLOAT'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write until cut_event
|
||||||
|
while not cut_event.is_set():
|
||||||
|
try:
|
||||||
|
data = q.get(timeout=0.1)
|
||||||
|
wav_file.write(data)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Cut triggered
|
||||||
|
wav_file.close()
|
||||||
|
print(f"✂️ Segment {recording_index} captured: {current_wav.name}")
|
||||||
|
|
||||||
|
# Convert to MP3
|
||||||
|
mp3_path = current_wav.name.replace('.wav', '.mp3')
|
||||||
|
cmd = [
|
||||||
|
'ffmpeg', '-y',
|
||||||
|
'-i', current_wav.name,
|
||||||
|
'-codec:a', 'libmp3lame',
|
||||||
|
'-qscale:a', '2',
|
||||||
|
mp3_path
|
||||||
|
]
|
||||||
|
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
print(f"🔊 Converted to MP3: {mp3_path}")
|
||||||
|
|
||||||
|
# Prompt for rename/move
|
||||||
|
new_path = input("Enter new filename (with .mp3) or full path (leave blank to keep): ").strip()
|
||||||
|
if new_path:
|
||||||
|
try:
|
||||||
|
os.replace(mp3_path, new_path)
|
||||||
|
print(f"✅ Moved to {new_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Could not move file: {e}")
|
||||||
|
else:
|
||||||
|
print(f"➔ Keeping as {mp3_path}")
|
||||||
|
|
||||||
|
# Clean up WAV
|
||||||
|
os.remove(current_wav.name)
|
||||||
|
recording_index += 1
|
||||||
|
|
||||||
|
# Reset for next segment
|
||||||
|
cut_event.clear()
|
||||||
|
print("▶️ Recording next segment... Press 'c' to cut.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error occurred setting up audio stream: {e}")
|
||||||
|
print("\nThere are several ways to fix this:")
|
||||||
|
print("1. Try enabling 'Stereo Mix' in Windows Sound settings")
|
||||||
|
print(" - Right-click the speaker icon in system tray")
|
||||||
|
print(" - Select 'Sound settings' -> 'Sound Control Panel'")
|
||||||
|
print(" - Go to 'Recording' tab")
|
||||||
|
print(" - Right-click empty space and check 'Show Disabled Devices'")
|
||||||
|
print(" - Enable 'Stereo Mix' if available")
|
||||||
|
print("2. Try a different device ID")
|
||||||
|
print("\nAvailable devices:")
|
||||||
|
for i, device in enumerate(sd.query_devices()):
|
||||||
|
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
|
||||||
|
print(f" {i} {device['name']}, {hostapi_name} ({device['max_input_channels']} in, {device['max_output_channels']} out)")
|
||||||
|
|
||||||
|
print("\nRun the script again with a specific device ID:")
|
||||||
|
print(" .venv/Scripts/python record.py")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def listen_for_cut(cut_event: threading.Event):
|
||||||
|
"""Waits for the user to press 'c' and sets the event."""
|
||||||
|
while True:
|
||||||
|
keyboard.wait('c')
|
||||||
|
cut_event.set()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cut_flag = threading.Event()
|
||||||
|
# Thread that listens for 'c'
|
||||||
|
listener = threading.Thread(target=listen_for_cut, args=(cut_flag,), daemon=True)
|
||||||
|
listener.start()
|
||||||
|
# Main recording loop
|
||||||
|
try:
|
||||||
|
record_loop(cut_flag)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🛑 Recording stopped by user.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error occurred while recording: {e}")
|
||||||
450
audio-recorder/record_ui.py
Normal file
450
audio-recorder/record_ui.py
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import sounddevice as sd
|
||||||
|
import soundfile as sf
|
||||||
|
import keyboard
|
||||||
|
import pygame
|
||||||
|
import numpy as np
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
# PARAMETERS – adjust as needed
|
||||||
|
CHANNELS = 2
|
||||||
|
SAMPLE_RATE = 44100
|
||||||
|
BLOCKSIZE = 1024 # frames per chunk
|
||||||
|
|
||||||
|
# Look for "Stereo Mix" which is a common device for capturing system audio
|
||||||
|
devices = sd.query_devices()
|
||||||
|
DEVICE_ID = 14 # Will be set dynamically
|
||||||
|
WASAPI_METHOD = False # First try with Stereo Mix, then try WASAPI if available
|
||||||
|
|
||||||
|
print("Available audio devices:")
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
print(f" {i} {device['name']}, {device['hostapi']} ({device['max_input_channels']} in, {device['max_output_channels']} out)")
|
||||||
|
# First priority: Stereo Mix
|
||||||
|
if "Stereo Mix" in device['name'] and device['max_input_channels'] > 0:
|
||||||
|
DEVICE_ID = i
|
||||||
|
print(f"Selected device {i}: {device['name']} for system audio recording")
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no Stereo Mix, try to find WASAPI devices
|
||||||
|
if DEVICE_ID is None:
|
||||||
|
hostapis = sd.query_hostapis()
|
||||||
|
wasapi_index = None
|
||||||
|
|
||||||
|
for i, api in enumerate(hostapis):
|
||||||
|
if 'WASAPI' in api['name']:
|
||||||
|
wasapi_index = i
|
||||||
|
print(f"Found WASAPI at index {i}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if wasapi_index is not None:
|
||||||
|
# Try to find a WASAPI output device
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
if device['hostapi'] == wasapi_index and device['max_output_channels'] > 0 and 'Speakers' in device['name']:
|
||||||
|
DEVICE_ID = i
|
||||||
|
print(f"Selected device {i}: {device['name']} (will try WASAPI method)")
|
||||||
|
WASAPI_METHOD = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback to any WASAPI output device
|
||||||
|
if DEVICE_ID is None:
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
if device['hostapi'] == wasapi_index and device['max_output_channels'] > 0:
|
||||||
|
DEVICE_ID = i
|
||||||
|
print(f"Selected device {i}: {device['name']} (will try WASAPI method)")
|
||||||
|
WASAPI_METHOD = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# If still no device found, prompt user
|
||||||
|
if DEVICE_ID is None:
|
||||||
|
print("No suitable recording device found. Please select a device manually:")
|
||||||
|
for i, device in enumerate(devices):
|
||||||
|
print(f" {i} {device['name']}, {device['hostapi']} ({device['max_input_channels']} in, {device['max_output_channels']} out)")
|
||||||
|
device_id_input = input("Enter device ID to use: ")
|
||||||
|
try:
|
||||||
|
DEVICE_ID = int(device_id_input)
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid device ID")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Create a local cache directory
|
||||||
|
CACHE_DIR = os.path.join(os.getcwd(), "cache")
|
||||||
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Global variables for visualization
|
||||||
|
audio_buffer = deque(maxlen=BLOCKSIZE * 10) # Store more samples for visualization
|
||||||
|
|
||||||
|
# Updated record_loop to use local cache and avoid file access conflicts
|
||||||
|
def record_loop(cut_event: threading.Event):
|
||||||
|
"""
|
||||||
|
Records audio into successive WAV files.
|
||||||
|
When cut_event is set, closes current file, converts it, and starts a new one.
|
||||||
|
"""
|
||||||
|
global audio_buffer
|
||||||
|
q = queue.Queue()
|
||||||
|
recording_index = 1
|
||||||
|
current_wav = None
|
||||||
|
wav_file = None
|
||||||
|
|
||||||
|
def callback(indata, frames, time, status):
|
||||||
|
if status:
|
||||||
|
print(f"⚠️ {status}")
|
||||||
|
|
||||||
|
# Add data to visualizations
|
||||||
|
audio_data = indata.copy()
|
||||||
|
q.put(audio_data)
|
||||||
|
|
||||||
|
# Update audio buffer for visualization
|
||||||
|
for sample in audio_data:
|
||||||
|
audio_buffer.append(sample[0]) # Use first channel for visualization
|
||||||
|
|
||||||
|
# Set up recording mode
|
||||||
|
device_info = sd.query_devices(DEVICE_ID)
|
||||||
|
print(f"Using device: {device_info['name']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
stream = sd.InputStream(
|
||||||
|
device=DEVICE_ID,
|
||||||
|
channels=CHANNELS,
|
||||||
|
samplerate=SAMPLE_RATE,
|
||||||
|
blocksize=BLOCKSIZE,
|
||||||
|
callback=callback,
|
||||||
|
dtype='float32'
|
||||||
|
)
|
||||||
|
|
||||||
|
stream.start()
|
||||||
|
print("▶️ Recording... Press 'c' to cut.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Start a new WAV file in the cache directory
|
||||||
|
current_wav_path = os.path.join(CACHE_DIR, f"segment_{recording_index:02d}.wav")
|
||||||
|
wav_file = sf.SoundFile(
|
||||||
|
current_wav_path, mode='w',
|
||||||
|
samplerate=SAMPLE_RATE, channels=CHANNELS, subtype='FLOAT'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write until cut_event
|
||||||
|
while not cut_event.is_set():
|
||||||
|
try:
|
||||||
|
data = q.get(timeout=0.1)
|
||||||
|
wav_file.write(data)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Cut triggered
|
||||||
|
wav_file.close()
|
||||||
|
print(f"✂️ Segment {recording_index} captured: {current_wav_path}")
|
||||||
|
|
||||||
|
# Convert to MP3
|
||||||
|
mp3_path = current_wav_path.replace('.wav', '.mp3')
|
||||||
|
cmd = [
|
||||||
|
'ffmpeg', '-y',
|
||||||
|
'-i', current_wav_path,
|
||||||
|
'-codec:a', 'libmp3lame',
|
||||||
|
'-qscale:a', '2',
|
||||||
|
mp3_path
|
||||||
|
]
|
||||||
|
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
print(f"🔊 Converted to MP3: {mp3_path}")
|
||||||
|
|
||||||
|
# Clean up WAV
|
||||||
|
os.remove(current_wav_path)
|
||||||
|
recording_index += 1
|
||||||
|
|
||||||
|
# Reset for next segment
|
||||||
|
cut_event.clear()
|
||||||
|
print("▶️ Recording next segment... Press 'c' to cut.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error occurred setting up audio stream: {e}")
|
||||||
|
print("\nThere are several ways to fix this:")
|
||||||
|
print("1. Try enabling 'Stereo Mix' in Windows Sound settings")
|
||||||
|
print(" - Right-click the speaker icon in system tray")
|
||||||
|
print(" - Select 'Sound settings' -> 'Sound Control Panel'")
|
||||||
|
print(" - Go to 'Recording' tab")
|
||||||
|
print(" - Right-click empty space and check 'Show Disabled Devices'")
|
||||||
|
print(" - Enable 'Stereo Mix' if available")
|
||||||
|
print("2. Try a different device ID")
|
||||||
|
print("\nAvailable devices:")
|
||||||
|
for i, device in enumerate(sd.query_devices()):
|
||||||
|
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
|
||||||
|
print(f" {i} {device['name']}, {hostapi_name} ({device['max_input_channels']} in, {device['max_output_channels']} out)")
|
||||||
|
|
||||||
|
print("\nRun the script again with a specific device ID:")
|
||||||
|
print(" .venv/Scripts/python record.py")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def listen_for_cut(cut_event: threading.Event):
|
||||||
|
"""Waits for the user to press 'c' and sets the event."""
|
||||||
|
while True:
|
||||||
|
keyboard.wait('c')
|
||||||
|
cut_event.set()
|
||||||
|
|
||||||
|
# Initialize pygame
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Screen dimensions
|
||||||
|
SCREEN_WIDTH = 1000
|
||||||
|
SCREEN_HEIGHT = 700
|
||||||
|
|
||||||
|
# Modern Colors
|
||||||
|
BACKGROUND = (30, 30, 38)
|
||||||
|
TEXT_COLOR = (230, 230, 230)
|
||||||
|
ACCENT1 = (75, 207, 250) # Light blue
|
||||||
|
ACCENT2 = (255, 110, 110) # Soft red
|
||||||
|
ACCENT3 = (131, 232, 90) # Green
|
||||||
|
ACCENT4 = (245, 194, 66) # Gold
|
||||||
|
PANEL_BG = (45, 45, 60)
|
||||||
|
|
||||||
|
# Create the screen with a modern resolution
|
||||||
|
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||||
|
pygame.display.set_caption("Modern Audio Recorder")
|
||||||
|
|
||||||
|
# Try to load a nicer font
|
||||||
|
try:
|
||||||
|
font_file = pygame.font.get_default_font()
|
||||||
|
main_font = pygame.font.Font(font_file, 18)
|
||||||
|
title_font = pygame.font.Font(font_file, 24)
|
||||||
|
button_font = pygame.font.Font(font_file, 20)
|
||||||
|
except:
|
||||||
|
main_font = pygame.font.Font(None, 24)
|
||||||
|
title_font = pygame.font.Font(None, 36)
|
||||||
|
button_font = pygame.font.Font(None, 28)
|
||||||
|
|
||||||
|
# Modern UI Layout
|
||||||
|
header_rect = pygame.Rect(0, 0, SCREEN_WIDTH, 60)
|
||||||
|
footer_rect = pygame.Rect(0, SCREEN_HEIGHT - 80, SCREEN_WIDTH, 80)
|
||||||
|
waveform_rect = pygame.Rect(20, 80, SCREEN_WIDTH - 40, 200)
|
||||||
|
|
||||||
|
# Modern rounded button class
|
||||||
|
class RoundedButton:
|
||||||
|
def __init__(self, x, y, width, height, text, color, hover_color):
|
||||||
|
self.rect = pygame.Rect(x, y, width, height)
|
||||||
|
self.text = text
|
||||||
|
self.color = color
|
||||||
|
self.hover_color = hover_color
|
||||||
|
self.is_hovered = False
|
||||||
|
self.radius = 10
|
||||||
|
|
||||||
|
def draw(self, surface):
|
||||||
|
color = self.hover_color if self.is_hovered else self.color
|
||||||
|
pygame.draw.rect(surface, color, self.rect, border_radius=self.radius)
|
||||||
|
|
||||||
|
# Add a slight 3D effect
|
||||||
|
highlight = (min(color[0] + 40, 255), min(color[1] + 40, 255), min(color[2] + 40, 255))
|
||||||
|
shadow = (max(color[0] - 40, 0), max(color[1] - 40, 0), max(color[2] - 40, 0))
|
||||||
|
|
||||||
|
# Top highlight
|
||||||
|
pygame.draw.rect(surface, highlight,
|
||||||
|
(self.rect.x, self.rect.y, self.rect.width, 3),
|
||||||
|
border_top_left_radius=self.radius, border_top_right_radius=self.radius)
|
||||||
|
|
||||||
|
# Bottom shadow
|
||||||
|
pygame.draw.rect(surface, shadow,
|
||||||
|
(self.rect.x, self.rect.y + self.rect.height - 3, self.rect.width, 3),
|
||||||
|
border_bottom_left_radius=self.radius, border_bottom_right_radius=self.radius)
|
||||||
|
|
||||||
|
text_surface = button_font.render(self.text, True, TEXT_COLOR)
|
||||||
|
text_rect = text_surface.get_rect(center=self.rect.center)
|
||||||
|
surface.blit(text_surface, text_rect)
|
||||||
|
|
||||||
|
def update(self, mouse_pos):
|
||||||
|
self.is_hovered = self.rect.collidepoint(mouse_pos)
|
||||||
|
|
||||||
|
def is_clicked(self, mouse_pos, mouse_click):
|
||||||
|
return self.rect.collidepoint(mouse_pos) and mouse_click
|
||||||
|
|
||||||
|
# Modern UI elements
|
||||||
|
start_button = RoundedButton(SCREEN_WIDTH//2 - 160, SCREEN_HEIGHT - 65, 100, 40, "START", ACCENT3, (160, 255, 130))
|
||||||
|
cut_button = RoundedButton(SCREEN_WIDTH//2 - 45, SCREEN_HEIGHT - 65, 90, 40, "CUT", ACCENT4, (255, 220, 100))
|
||||||
|
stop_button = RoundedButton(SCREEN_WIDTH//2 + 60, SCREEN_HEIGHT - 65, 100, 40, "STOP", ACCENT2, (255, 150, 150))
|
||||||
|
|
||||||
|
# Modern input box
|
||||||
|
class ModernInputBox:
|
||||||
|
def __init__(self, x, y, width, height, placeholder="Enter filename..."):
|
||||||
|
self.rect = pygame.Rect(x, y, width, height)
|
||||||
|
self.color_inactive = (100, 100, 120)
|
||||||
|
self.color_active = ACCENT1
|
||||||
|
self.color = self.color_inactive
|
||||||
|
self.text = ""
|
||||||
|
self.placeholder = placeholder
|
||||||
|
self.active = False
|
||||||
|
self.radius = 5
|
||||||
|
|
||||||
|
def handle_event(self, event):
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
self.active = self.rect.collidepoint(event.pos)
|
||||||
|
self.color = self.color_active if self.active else self.color_inactive
|
||||||
|
if event.type == pygame.KEYDOWN and self.active:
|
||||||
|
if event.key == pygame.K_RETURN:
|
||||||
|
print(f"File name set to: {self.text}")
|
||||||
|
return self.text
|
||||||
|
elif event.key == pygame.K_BACKSPACE:
|
||||||
|
self.text = self.text[:-1]
|
||||||
|
else:
|
||||||
|
self.text += event.unicode
|
||||||
|
return None
|
||||||
|
|
||||||
|
def draw(self, surface):
|
||||||
|
pygame.draw.rect(surface, PANEL_BG, self.rect, border_radius=self.radius)
|
||||||
|
pygame.draw.rect(surface, self.color, self.rect, 2, border_radius=self.radius)
|
||||||
|
|
||||||
|
if self.text:
|
||||||
|
text_surface = main_font.render(self.text, True, TEXT_COLOR)
|
||||||
|
else:
|
||||||
|
text_surface = main_font.render(self.placeholder, True, (130, 130, 140))
|
||||||
|
|
||||||
|
surface.blit(text_surface, (self.rect.x + 10, self.rect.y + (self.rect.height - text_surface.get_height()) // 2))
|
||||||
|
|
||||||
|
# Create modern input box
|
||||||
|
input_box = ModernInputBox(SCREEN_WIDTH - 260, SCREEN_HEIGHT - 65, 240, 40)
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
running = True
|
||||||
|
recording = False
|
||||||
|
cut_event = threading.Event()
|
||||||
|
recording_thread = None
|
||||||
|
silence_counter = 0
|
||||||
|
|
||||||
|
# Function to draw modern waveform
|
||||||
|
def draw_waveform():
|
||||||
|
global audio_buffer
|
||||||
|
pygame.draw.rect(screen, PANEL_BG, waveform_rect, border_radius=10)
|
||||||
|
pygame.draw.rect(screen, ACCENT1, waveform_rect, 2, border_radius=10)
|
||||||
|
|
||||||
|
# Add waveform title
|
||||||
|
title = title_font.render("Waveform", True, TEXT_COLOR)
|
||||||
|
screen.blit(title, (waveform_rect.x + 10, waveform_rect.y + 10))
|
||||||
|
|
||||||
|
if len(audio_buffer) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get data for visualization
|
||||||
|
buffer_data = np.array(list(audio_buffer))
|
||||||
|
max_amplitude = np.max(np.abs(buffer_data))
|
||||||
|
if max_amplitude > 0:
|
||||||
|
buffer_data = buffer_data / max_amplitude # Normalize to [-1, 1]
|
||||||
|
samples_to_show = min(len(buffer_data), 1000)
|
||||||
|
|
||||||
|
# Determine step size
|
||||||
|
step = max(1, len(buffer_data) // samples_to_show)
|
||||||
|
points = []
|
||||||
|
|
||||||
|
# Scale factor to fit within our display area
|
||||||
|
scale = 80 # Adjust amplitude scale
|
||||||
|
|
||||||
|
# Adjust waveform rendering to ensure it fits within the screen
|
||||||
|
for i in range(0, len(buffer_data) - step, step):
|
||||||
|
x = waveform_rect.x + 10 + (i // step) * ((waveform_rect.width - 20) / samples_to_show)
|
||||||
|
y = waveform_rect.y + waveform_rect.height // 2 - buffer_data[i] * scale
|
||||||
|
points.append((x, y))
|
||||||
|
|
||||||
|
# Apply a more effective compressor to cut low volume (silence threshold)
|
||||||
|
silence_threshold = 0.05 # Increased threshold to reduce noise
|
||||||
|
buffer_data = np.where(np.abs(buffer_data) < silence_threshold, 0, buffer_data)
|
||||||
|
|
||||||
|
# Improved auto-cut logic to handle silence detection
|
||||||
|
silence_duration = 3 # seconds
|
||||||
|
silence_frames = int(silence_duration * SAMPLE_RATE)
|
||||||
|
global silence_counter
|
||||||
|
|
||||||
|
if recording:
|
||||||
|
if np.all(np.abs(buffer_data[-BLOCKSIZE:]) < silence_threshold):
|
||||||
|
silence_counter += BLOCKSIZE
|
||||||
|
else:
|
||||||
|
silence_counter = 0
|
||||||
|
|
||||||
|
if silence_counter >= silence_frames:
|
||||||
|
cut_event.set()
|
||||||
|
silence_counter = 0
|
||||||
|
|
||||||
|
# Ensure points are tuples of two floats
|
||||||
|
points = [(float(x), float(y)) for x, y in points]
|
||||||
|
|
||||||
|
# Draw waveform lines
|
||||||
|
if len(points) > 1:
|
||||||
|
pygame.draw.lines(screen, ACCENT1, False, points, 2)
|
||||||
|
|
||||||
|
# Draw header with title and status
|
||||||
|
def draw_header():
|
||||||
|
pygame.draw.rect(screen, PANEL_BG, header_rect)
|
||||||
|
|
||||||
|
# App Title
|
||||||
|
title = title_font.render("MODERN AUDIO RECORDER", True, TEXT_COLOR)
|
||||||
|
screen.blit(title, (20, header_rect.height // 2 - title.get_height() // 2))
|
||||||
|
|
||||||
|
# Status indicator
|
||||||
|
status_text = "⚫ RECORDING" if recording else "⚪ READY"
|
||||||
|
status_color = ACCENT3 if recording else TEXT_COLOR
|
||||||
|
status = main_font.render(status_text, True, status_color)
|
||||||
|
screen.blit(status, (SCREEN_WIDTH - status.get_width() - 20, header_rect.height // 2 - status.get_height() // 2))
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
while running:
|
||||||
|
screen.fill(BACKGROUND)
|
||||||
|
|
||||||
|
mouse_pos = pygame.mouse.get_pos()
|
||||||
|
mouse_clicked = False
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
running = False
|
||||||
|
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
mouse_clicked = True
|
||||||
|
input_box.handle_event(event)
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
input_box.handle_event(event)
|
||||||
|
|
||||||
|
# Update buttons
|
||||||
|
start_button.update(mouse_pos)
|
||||||
|
cut_button.update(mouse_pos)
|
||||||
|
stop_button.update(mouse_pos)
|
||||||
|
|
||||||
|
# Handle button clicks
|
||||||
|
if mouse_clicked:
|
||||||
|
if start_button.is_clicked(mouse_pos, mouse_clicked) and not recording:
|
||||||
|
recording = True
|
||||||
|
cut_event.clear()
|
||||||
|
recording_thread = threading.Thread(target=record_loop, args=(cut_event,), daemon=True)
|
||||||
|
recording_thread.start()
|
||||||
|
elif cut_button.is_clicked(mouse_pos, mouse_clicked) and recording:
|
||||||
|
cut_event.set()
|
||||||
|
elif stop_button.is_clicked(mouse_pos, mouse_clicked) and recording:
|
||||||
|
recording = False
|
||||||
|
cut_event.set()
|
||||||
|
# Ensure the recording thread is properly terminated
|
||||||
|
if recording_thread and recording_thread.is_alive():
|
||||||
|
recording_thread.join(timeout=5) # Wait for the thread to finish with a timeout
|
||||||
|
if recording_thread.is_alive():
|
||||||
|
print("⚠️ Recording thread did not terminate in time.")
|
||||||
|
|
||||||
|
# Stop the pygame loop safely
|
||||||
|
running = False
|
||||||
|
# Clear visualization data when stopping
|
||||||
|
audio_buffer.clear()
|
||||||
|
|
||||||
|
# Update waveform to reflect actual audio data
|
||||||
|
if len(audio_buffer) > 0:
|
||||||
|
buffer_data = np.array(list(audio_buffer))
|
||||||
|
max_amplitude = np.max(np.abs(buffer_data))
|
||||||
|
if max_amplitude > 0:
|
||||||
|
buffer_data = buffer_data / max_amplitude # Normalize to [-1, 1]
|
||||||
|
audio_buffer = deque(buffer_data, maxlen=BLOCKSIZE * 10)
|
||||||
|
|
||||||
|
# Draw UI components
|
||||||
|
draw_header()
|
||||||
|
draw_waveform()
|
||||||
|
|
||||||
|
# Draw controls in the footer area
|
||||||
|
pygame.draw.rect(screen, PANEL_BG, footer_rect)
|
||||||
|
start_button.draw(screen)
|
||||||
|
cut_button.draw(screen)
|
||||||
|
stop_button.draw(screen)
|
||||||
|
input_box.draw(screen)
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
pygame.time.delay(30) # Cap at roughly 30 fps for UI
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
7
audio-recorder/requirements.txt
Normal file
7
audio-recorder/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pygame
|
||||||
|
numpy
|
||||||
|
matplotlib
|
||||||
|
scipy
|
||||||
|
sounddevice
|
||||||
|
soundfile
|
||||||
|
keyboard
|
||||||
BIN
audio-recorder/test.mp3
Normal file
BIN
audio-recorder/test.mp3
Normal file
Binary file not shown.
6
ssug-meeting-summarizer/conversation_20250918-145905.txt
Normal file
6
ssug-meeting-summarizer/conversation_20250918-145905.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Conversation started at 20250918-145905
|
||||||
|
Model: gpt-oss:20b
|
||||||
|
|
||||||
|
You: guide me in making a simple Davinici Resolve Fusion composition to use in a godot game. or explain animation players and how to utilize them in godot
|
||||||
|
AI:
|
||||||
|
|
||||||
705
ssug-meeting-summarizer/convo.md
Normal file
705
ssug-meeting-summarizer/convo.md
Normal file
@ -0,0 +1,705 @@
|
|||||||
|
You: generate a list of technologies and resources that can be used to create an interesting idle game that revolves around unlocking letters and typing randomized sequences of the unlocked letters to generate/collect resources which are then spent
|
||||||
|
Thinking:
|
||||||
|
The user requests: generate a list of technologies and resources that can be used to create an interesting idle game that revolves around unlocking letters and typing randomized sequences of the unlocked letters to generate/collect resources which are then spent.
|
||||||
|
|
||||||
|
So we need to give technologies (programming languages, frameworks, engines) and resources (tutorials, libraries, assets, etc.) that can help build such an idle game. Should include front-end, back-end, database, UI, physics? But it's idle game: less physics. Maybe HTML5/JavaScript frameworks (Phaser, PixiJS, React + Redux). For typing mechanics, maybe use React + typing library, or pure JS. For idle logic: incremental engine. For unlocking letters: use state management. For random sequences: random generator.
|
||||||
|
|
||||||
|
Also resources: code libraries for idle/incremental logic (e.g., Incremental.js, GameSparks, GameSparks, or custom). Use open source code. For letter unlocking: maybe store letter array and manage progress.
|
||||||
|
|
||||||
|
Also asset resources: fonts, icons, audio, etc. Use open resources: Google Fonts, FontAwesome.
|
||||||
|
|
||||||
|
Game engines: Unity (C#), Godot (GDScript), Construct 3, GameMaker Studio. For web: Phaser 3, PlayCanvas, PixiJS. For cross-platform: Unity or Godot.
|
||||||
|
|
||||||
|
Also server side: Node.js, Python, Django, Firebase, PlayFab, GameSparks. For storage: localStorage, IndexedDB, Firebase Realtime Database, Firestore.
|
||||||
|
|
||||||
|
Resources: tutorials on idle games: e.g., Idle Game Development with Unity by Justin, etc. For letter typing: building a typing test game: tutorials. For incremental logic: "How to build an incremental game in JavaScript".
|
||||||
|
|
||||||
|
Also provide design patterns: state machine, command pattern, resource generation logic.
|
||||||
|
|
||||||
|
Also mention UI frameworks: React, Vue, Svelte.
|
||||||
|
|
||||||
|
Also mention audio: Tone.js or Howler.js.
|
||||||
|
|
||||||
|
Also mention asset store: OpenGameArt, Kenney assets.
|
||||||
|
|
||||||
|
Also mention community: Indie Game Dev Discord, GameDev StackExchange.
|
||||||
|
|
||||||
|
Also mention project management: Git, GitHub, Trello.
|
||||||
|
|
||||||
|
Also mention performance: requestAnimationFrame, optimizing.
|
||||||
|
|
||||||
|
Also mention using WebGL for better graphics.
|
||||||
|
|
||||||
|
Also mention using WebAssembly for performance.
|
||||||
|
|
||||||
|
Also mention using TypeScript for type safety.
|
||||||
|
|
||||||
|
Also mention using Tiled map editor for UI.
|
||||||
|
|
||||||
|
Also mention using WebSocket for multiplayer? Not necessary.
|
||||||
|
|
||||||
|
Also mention monetization: ads, IAP.
|
||||||
|
|
||||||
|
Also mention analytics: Google Analytics, Amplitude.
|
||||||
|
|
||||||
|
Also mention packaging: electron, NW.js, itch.io.
|
||||||
|
|
||||||
|
Also mention versioning: npm.
|
||||||
|
|
||||||
|
Also mention testing: Jest, Cypress.
|
||||||
|
|
||||||
|
Also mention UI design: Figma.
|
||||||
|
|
||||||
|
Also mention design docs: Confluence.
|
||||||
|
|
||||||
|
So produce a structured list: categories: Game Engine / Framework, Programming Languages, Libraries / SDKs, Tools, Asset Resources, Tutorials & Guides, Community & Support, Deployment & Distribution, Analytics & Monetization, Development Workflow.
|
||||||
|
|
||||||
|
In each category, list items. Provide brief description.
|
||||||
|
|
||||||
|
Make sure it's a list of technologies and resources that can be used to create an interesting idle game that revolves around unlocking letters and typing random sequences of the unlocked letters to generate resources which are then spent.
|
||||||
|
|
||||||
|
Let's produce it.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
Below is a “starter‑kit” of everything you’ll need – from engines and languages to libraries, tutorials, assets, and deployment tools – to build a compelling idle game that lets players unlock letters, type random letter‑sequences, generate resources, and spend those resources on upgrades.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ Game Engine / Framework
|
||||||
|
|
||||||
|
| Engine / Framework | Why it fits | Key Features |
|
||||||
|
|--------------------|-------------|--------------|
|
||||||
|
| **Phaser 3** (JS) | Lightweight 2D canvas engine, great for idle mechanics, no compile step. | Built‑in tweens, timers, physics (optional), asset loader. |
|
||||||
|
| **PixiJS** (JS) | Pure rendering engine; combine with a state manager for idle logic. | High‑performance WebGL, plugin ecosystem. |
|
||||||
|
| **React + Redux** (JS) | Treat the whole game as a UI; perfect for “clicker” logic. | Virtual DOM, declarative UI, easy state persistence. |
|
||||||
|
| **Unity** (C#) | Full‑blown engine, excellent for cross‑platform (iOS, Android, web). | Rich asset pipeline, physics optional, powerful editor. |
|
||||||
|
| **Godot** (GDScript / C#) | Open‑source, lightweight, free. | Scene system, signal‑based architecture. |
|
||||||
|
| **Construct 3** | No‑code visual editor; ideal for rapid prototyping. | Built‑in idle game templates, export to HTML5. |
|
||||||
|
|
||||||
|
> **Recommendation**: If you’re aiming for a web‑only idle game, Phaser 3 + React (or just vanilla JS) is the fastest route. For multi‑platform or richer graphics, Unity or Godot are better bets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ Programming Languages & Tooling
|
||||||
|
|
||||||
|
| Language | Why it’s useful | Tooling |
|
||||||
|
|----------|-----------------|---------|
|
||||||
|
| **TypeScript** | Strong typing, helps with large state trees (letters, resources). | `tsc`, `ts-node`, IDE support. |
|
||||||
|
| **JavaScript** | Universal for web engines (Phaser, Pixi). | Babel, Webpack, Parcel. |
|
||||||
|
| **C#** | Unity / Godot’s default (Godot also supports GDScript). | Visual Studio, Rider, VS Code. |
|
||||||
|
| **GDScript** | Godot’s Python‑like scripting language. | Godot editor, debugger. |
|
||||||
|
| **Python** | Backend (Flask/FastAPI) if you need a server for persistence. | Flask, FastAPI, Docker. |
|
||||||
|
| **Node.js** | Full‑stack JS (frontend + backend). | Express, NestJS, Socket.io. |
|
||||||
|
|
||||||
|
> **Tip**: Start with TypeScript + Phaser 3. It gives you type safety and a solid rendering engine while keeping the development cycle short.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ Core Libraries & SDKs
|
||||||
|
|
||||||
|
| Library | Purpose | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| **incremental-game** (npm) | A tiny library that handles clicker/idle logic. | Customizable timers, resource rates. |
|
||||||
|
| **game-state-machine** | For managing unlock states (letters, upgrades). | FSM or simple state trees. |
|
||||||
|
| **react-virtualized** | Render long lists of letters without performance hits. | Useful for scrollable “letter book”. |
|
||||||
|
| **howler.js** | Audio playback for typing sounds, click sounds. | Works on browsers & mobile. |
|
||||||
|
| **Tone.js** | Procedural sound generation (typing sound synthesis). | Adds depth if you want a musical touch. |
|
||||||
|
| **seedrandom** | Deterministic random generator for sequence generation. | Helps with reproducible gameplay & debugging. |
|
||||||
|
| **localForage** | IndexedDB wrapper for persisting game state. | Works offline, sync to cloud if needed. |
|
||||||
|
| **firebase/Firestore** | Cloud persistence, multiplayer or cross‑device sync. | Real‑time database + Auth. |
|
||||||
|
| **socket.io** | If you plan a multiplayer or leader‑board sync. | Real‑time events. |
|
||||||
|
| **Amplitude / Mixpanel** | Analytics for player engagement. | Tracks unlocks, clicks, purchase funnel. |
|
||||||
|
| **React‑Game‑Pad** | Keyboard input handling (including key combos). | Good for typing mechanics. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4️⃣ Development Workflow & Tools
|
||||||
|
|
||||||
|
| Tool | What it does |
|
||||||
|
|------|--------------|
|
||||||
|
| **Git + GitHub** | Version control, collaboration. |
|
||||||
|
| **VS Code** | Lightweight IDE, great extensions for TypeScript, ESLint, Prettier. |
|
||||||
|
| **ESLint + Prettier** | Code quality. |
|
||||||
|
| **Jest / Vitest** | Unit testing for game logic. |
|
||||||
|
| **Cypress** | End‑to‑end testing for UI flow. |
|
||||||
|
| **Figma / Sketch** | UI/UX design; prototype the “letter book” and upgrade menu. |
|
||||||
|
| **Trello / Notion** | Task management & design docs. |
|
||||||
|
| **Docker** | For local dev server / backend isolation. |
|
||||||
|
| **Webpack / Vite** | Module bundler / dev server. |
|
||||||
|
| **GitHub Actions** | CI/CD: build, test, deploy. |
|
||||||
|
| **Netlify / Vercel** | Easy hosting for static web games. |
|
||||||
|
| **Itch.io** | Distribution & community feedback. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5️⃣ Asset Resources
|
||||||
|
|
||||||
|
| Category | Where to Find | Licensing |
|
||||||
|
|----------|---------------|-----------|
|
||||||
|
| **Fonts** | Google Fonts, Font Squirrel | Open source / commercial |
|
||||||
|
| **UI Icons** | FontAwesome, Feather Icons | Free for commercial |
|
||||||
|
| **Buttons & Sprites** | Kenney Assets, OpenGameArt | CC0 / CC BY |
|
||||||
|
| **Backgrounds** | Texture Haven, Unsplash | Royalty‑free |
|
||||||
|
| **Typing Sounds** | Freesound.org | Creative Commons |
|
||||||
|
| **Music** | Incompetech, Bensound | Creative Commons |
|
||||||
|
|
||||||
|
> **Tip**: Keep the UI minimalistic – idle games thrive on simple visuals and clear progress bars.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6️⃣ Learning & Reference Resources
|
||||||
|
|
||||||
|
| Resource | Type | Why it’s useful |
|
||||||
|
|----------|------|-----------------|
|
||||||
|
| **“How to Make an Incremental Game” (freeCodeCamp)** | Blog + Video | Step‑by‑step guide to building a clicker in JS. |
|
||||||
|
| **“Idle Game Design Patterns” (GameDev.net)** | Article | Explains common loops: *click → resource → upgrade → more clicks*. |
|
||||||
|
| **“Unity Idle Game Tutorial” (Brackeys)** | Video | Unity‑specific, shows resource counters and UI. |
|
||||||
|
| **Phaser 3 Docs** | Official | Core APIs, timers, tweens. |
|
||||||
|
| **React Docs** | Official | Hooks, context, state persistence. |
|
||||||
|
| **Godot Docs** | Official | Signals, scenes, timers. |
|
||||||
|
| **GameDev StackExchange** | Q&A | Quick answers to engine‑specific questions. |
|
||||||
|
| **Game Jolt Dev Community** | Forum | Share progress, get feedback. |
|
||||||
|
| **GitHub Repos** | Code examples | e.g., `joshtronic/incremental.js`, `brycedell/react-game-state` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7️⃣ Monetization & Analytics (Optional)
|
||||||
|
|
||||||
|
| Service | Use Case |
|
||||||
|
|---------|----------|
|
||||||
|
| **Google AdMob / AdSense** | Display ads on web or mobile. |
|
||||||
|
| **Unity IAP / In-App Purchases** | Sell “premium letters” or ad‑free upgrades. |
|
||||||
|
| **Facebook Instant Games** | Reach mobile players. |
|
||||||
|
| **Steam** | Desktop distribution if you go Unity/Godot. |
|
||||||
|
| **Analytics** | Amplitude, Unity Analytics, Firebase Analytics | Track retention, unlock rate, revenue per user. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8️⃣ Deployment & Distribution
|
||||||
|
|
||||||
|
| Platform | How to Deploy |
|
||||||
|
|----------|---------------|
|
||||||
|
| **Web** | Netlify, Vercel, GitHub Pages (static build). |
|
||||||
|
| **Mobile (WebView)** | Cordova / Capacitor, build as a PWA. |
|
||||||
|
| **iOS / Android** | Unity Build (iOS/Android), Godot Export, React Native with Expo + WebView. |
|
||||||
|
| **Desktop** | Unity (Windows/macOS/Linux), Godot, Electron. |
|
||||||
|
| **Itch.io** | Upload built package; includes auto‑update & analytics. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9️⃣ High‑Level Architecture (What the Game Looks Like)
|
||||||
|
|
||||||
|
1. **State Store** (Redux / Context / Godot Nodes)
|
||||||
|
- `lettersUnlocked: Set<string>`
|
||||||
|
- `resourceAmount: number`
|
||||||
|
- `upgradeLevels: { [upgradeName]: number }`
|
||||||
|
|
||||||
|
2. **Timer Loop** (Idle generation)
|
||||||
|
- Every `X` seconds: `resourceAmount += baseRate * (1 + upgradesFactor)`
|
||||||
|
|
||||||
|
3. **Typing Engine**
|
||||||
|
- Generate a random sequence of currently unlocked letters.
|
||||||
|
- On keypress, validate against sequence.
|
||||||
|
- On success, award bonus resources (`+50 * comboFactor`).
|
||||||
|
|
||||||
|
4. **Unlock System**
|
||||||
|
- When `resourceAmount` hits threshold, reveal next letter.
|
||||||
|
- UI shows “unlock” button that spends resources.
|
||||||
|
|
||||||
|
5. **Upgrade System**
|
||||||
|
- Upgrade UI that shows cost, benefit, and current level.
|
||||||
|
- Clicking upgrades consumes resources and modifies timers/combos.
|
||||||
|
|
||||||
|
6. **Persistence**
|
||||||
|
- Store state in `localStorage` / IndexedDB.
|
||||||
|
- Optionally sync to Firebase for cross‑device play.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Quick “Start‑Up” Checklist
|
||||||
|
|
||||||
|
| Step | What to do |
|
||||||
|
|------|------------|
|
||||||
|
| 1 | Pick an engine (Phaser 3 + TypeScript is our default). |
|
||||||
|
| 2 | Scaffold with Vite + TS. |
|
||||||
|
| 3 | Install `incremental-game`, `howler.js`, `localforage`. |
|
||||||
|
| 4 | Create a basic UI: resource counter, unlock button, typing area. |
|
||||||
|
| 5 | Implement timer loop (idle resource gain). |
|
||||||
|
| 6 | Add typing logic: random sequence generator + key event handler. |
|
||||||
|
| 7 | Add unlock logic: check resources, unlock new letter. |
|
||||||
|
| 8 | Add upgrade logic: create a simple upgrade tree. |
|
||||||
|
| 9 | Persist state with `localforage`. |
|
||||||
|
| 10 | Polish UI with Figma assets, add sounds with Howler. |
|
||||||
|
| 11 | Deploy to Netlify; publish on itch.io. |
|
||||||
|
| 12 | Add analytics & monetize if desired. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Final Thought
|
||||||
|
|
||||||
|
The heart of an idle game is the *progress loop* – click, generate, spend, repeat. With the stack above, you’ll have all the plumbing to implement a letter‑unlocking, typing‑based idle game in just a few weeks, while keeping the code clean, scalable, and portable across platforms. Happy coding!
|
||||||
|
You: generate the plan using godot as the engine with gdscript. what reources and technologies should our game have in it for t
|
||||||
|
he tech trees and unlock system. I'm imaging an efficieny optimizing game so that players try to get the most out of every lett
|
||||||
|
er pressed
|
||||||
|
Thinking:
|
||||||
|
We need to provide a plan for a game using Godot and GDScript. The plan should include resources and technologies for tech trees and unlock system, focusing on efficiency optimizing game where players try to get most out of every letter pressed. We need to think of game concept maybe a text-based or type-based game. But the question: "generate the plan using godot as the engine with gdscript. what resources and technologies should our game have in it for the tech trees and unlock system." So we need to outline: project structure, GDScript architecture, resources (scene files, UI, data files), data storage (JSON, ScriptableObjects?), maybe using Godot's built-in Resource system. Tech trees: data-driven using resource files or dictionaries, nodes for UI, unlocks: achievements, progress, persistent storage. Also need to consider performance: letter input handling, optimizing. Tools: Godot 4, GDScript 2.0, maybe use Godot's new "Resource" system, "ScriptableObject" or "JSON" for tech tree definitions. Use signals, state machines, caching, preloading, object pooling. Use a dedicated manager for input and for tracking efficiency metrics. Use a database? For offline: use Godot's FileAccess with JSON or .tres files. For cross-platform: use Godot's high-level networking or maybe use GDScript's built-in persistence. For optimization: use static typed GDScript? In Godot 4, GDScript is typed. Use typed signals. Use memory management. Use UI virtualization for long lists.
|
||||||
|
|
||||||
|
We need to produce a plan that includes resources and technologies: Godot 4.0, GDScript 2.0, GDNative? For tech tree: Resource files (.tres), perhaps using ScriptableObject like "TechTree.tres" with nested arrays. Or use "JSON" or "CSV" imported via autoload. Unlock system: use "PlayerData" resource, "Scoreboard" etc.
|
||||||
|
|
||||||
|
We also might want to incorporate data-driven UI: use "ItemList" or "Tree" control to display tech tree. Use "GraphEdit" for debugging. Use "Godot Analytics" or custom metrics for efficiency.
|
||||||
|
|
||||||
|
Also talk about architecture: autoload singletons: GameManager, InputManager, UnlockManager, TechTreeManager, PersistenceManager. Use Scenes: MainMenu, GameScene, Settings, etc.
|
||||||
|
|
||||||
|
Resources: Scenes (.tscn), Scripts (.gd), Texture resources (.png), Fonts (.ttf), Audio (.wav/.ogg). Data: JSON or .tres for tech tree config. Use "ResourceLoader" to load them. Use "ObjectPool" for UI nodes. Use "Signal" to update UI when unlocks.
|
||||||
|
|
||||||
|
Optimization: Use "InputEventKey" and check key mapping. Use "Input.is_action_pressed" for multiple key detection. Use "InputEventKey" events. Use "set_process_input(false)" for idle.
|
||||||
|
|
||||||
|
Also talk about "tech tree unlock logic" like prerequisites, costs, unlocking effect, incremental improvements.
|
||||||
|
|
||||||
|
Use "GDNative" if needed for performance-critical code.
|
||||||
|
|
||||||
|
Also incorporate "resource path mapping" for assets.
|
||||||
|
|
||||||
|
Add "Testing" using Unit tests: GUT, Godot Unit tests.
|
||||||
|
|
||||||
|
Use "Continuous Integration" with GitHub Actions.
|
||||||
|
|
||||||
|
Provide a table of resources and tech tree nodes. Provide example code for tech tree data structure.
|
||||||
|
|
||||||
|
Also maybe mention "Godot's built-in "GraphEdit" and "GraphNode" for debugging tech tree. Use "Godot's built-in "Signal" to notify.
|
||||||
|
|
||||||
|
Also mention "Godot's built-in "AnimationPlayer" for unlocking animations.
|
||||||
|
|
||||||
|
Add "User Interface" design: using "Control" nodes, "VBoxContainer" for lists.
|
||||||
|
|
||||||
|
We need to answer: "generate the plan using godot as the engine with gdscript. what resources and technologies should our game have in it for the tech trees and unlock system." So answer should contain a plan: overview, system architecture, resources needed, technologies, example code.
|
||||||
|
|
||||||
|
Let's write a structured plan. Provide a high-level overview: game concept, key systems, core loop.
|
||||||
|
|
||||||
|
Then architecture: autoloads, scene tree.
|
||||||
|
|
||||||
|
Then Tech Tree system: data-driven representation, UI, unlocking logic.
|
||||||
|
|
||||||
|
Then Unlock system: storing progress, achievements, unlocking abilities.
|
||||||
|
|
||||||
|
Then Resources: asset types, data files, script files.
|
||||||
|
|
||||||
|
Then Technologies: Godot 4.0, GDScript 2.0 typed, ResourceLoader, FileAccess, Signal system, Control nodes, AnimationPlayer, AudioStreamPlayer, Godot's high-level networking, maybe JSON, maybe Godot's "Project Settings" for key mapping.
|
||||||
|
|
||||||
|
Also mention "Performance" optimizations: typed GDScript, caching, object pooling, input handling.
|
||||||
|
|
||||||
|
Also mention "Testing" with GUT, CI.
|
||||||
|
|
||||||
|
Wrap up with a timeline or milestones.
|
||||||
|
|
||||||
|
We should provide example snippet for tech tree structure, e.g.:
|
||||||
|
|
||||||
|
var tech_tree = {
|
||||||
|
"nodes": {
|
||||||
|
"basic_attack": {
|
||||||
|
"name": "Basic Attack",
|
||||||
|
"cost": 1,
|
||||||
|
"prereqs": [],
|
||||||
|
"effect": {...}
|
||||||
|
},
|
||||||
|
"advanced_attack": {
|
||||||
|
"name": "Advanced Attack",
|
||||||
|
"cost": 2,
|
||||||
|
"prereqs": ["basic_attack"],
|
||||||
|
"effect": {...}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Then code to check if can unlock etc.
|
||||||
|
|
||||||
|
Also mention using "Godot's built-in "Resource" type: "TechNode.tres" that extends Resource. Each node can be a separate resource.
|
||||||
|
|
||||||
|
Also mention using "Singleton" for persistent data.
|
||||||
|
|
||||||
|
Also mention "Data storage" using "JSON" or "Godot's Resource".
|
||||||
|
|
||||||
|
Also mention "Optimization" for "letter pressed" usage: use "InputMap" mapping to actions, "Input.is_action_pressed", track efficiency metrics, use "Process" for performance.
|
||||||
|
|
||||||
|
Also talk about "tech tree UI" with "Tree" node.
|
||||||
|
|
||||||
|
Also talk about "Unlock system" like "Unlockable" base class with method "apply" and "on_unlock".
|
||||||
|
|
||||||
|
Also mention "Audio" for unlocking, "Animation" for UI.
|
||||||
|
|
||||||
|
Let's produce final answer.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
## 1. Game Vision
|
||||||
|
|
||||||
|
* **Genre** – Efficiency‑oriented typing / rhythm game.
|
||||||
|
- **Core Loop** – The player hits keys as fast as possible. Every correct press gives “efficiency points”.
|
||||||
|
- **Goal** – Spend those points on a **tech‑tree** that unlocks better key‑press mechanics, auto‑completion helpers, UI enhancements, etc.
|
||||||
|
- **Result** – The player feels the tension of “maximising every letter” and is constantly chasing a higher score by unlocking more sophisticated helpers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Technology Stack (Godot‑centric)
|
||||||
|
|
||||||
|
| Category | Technology | Why |
|
||||||
|
|----------|------------|-----|
|
||||||
|
| **Engine** | **Godot 4.0+** | Full 2D/3D support, GDScript 2.0 (strong typing), built‑in AnimationPlayer, UI, networking. |
|
||||||
|
| **Scripting** | **GDScript (typed)** | Easy to read, Godot‑native, no external JIT needed. |
|
||||||
|
| **Data Serialization** | **.tres / .res** (Godot Resource) **+ JSON** | Resources are reusable, cacheable, type‑safe. JSON gives quick editing outside Godot. |
|
||||||
|
| **Dependency Injection / Singleton** | **Autoloads** (e.g. `Game`, `InputMgr`, `TechTreeMgr`, `SaveMgr`) | Centralised state, signals for cross‑node communication. |
|
||||||
|
| **Persistence** | **FileAccess + JSON** | Simple file‑based save (works offline) and can be migrated to cloud later. |
|
||||||
|
| **UI Toolkit** | **Control nodes (Tree, Button, ProgressBar, Label)** + **StyleBox** | Built‑in, flexible and GPU‑accelerated. |
|
||||||
|
| **Animation / Audio** | **AnimationPlayer**, **AudioStreamPlayer** | Unlock animations, sound feedback. |
|
||||||
|
| **Performance** | **Object pooling** for UI nodes, **pre‑loading** resources, **typed GDScript** | Keeps input‑latency low. |
|
||||||
|
| **Testing** | **GUT (Godot Unit Test)** + **GodotCI** | Ensure data integrity, unlock logic, persistence. |
|
||||||
|
| **CI / Git** | **GitHub Actions** | Automated builds & tests. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/res
|
||||||
|
│
|
||||||
|
├── scenes/
|
||||||
|
│ ├── MainMenu.tscn
|
||||||
|
│ ├── Game.tscn
|
||||||
|
│ ├── UI/
|
||||||
|
│ │ ├── TechTreeUI.tscn
|
||||||
|
│ │ └── EfficiencyPanel.tscn
|
||||||
|
│ └── Prefabs/
|
||||||
|
│ ├── TechNode.tscn
|
||||||
|
│ └── UnlockEffect.tscn
|
||||||
|
│
|
||||||
|
├── scripts/
|
||||||
|
│ ├── autoload/
|
||||||
|
│ │ ├── Game.gd
|
||||||
|
│ │ ├── InputMgr.gd
|
||||||
|
│ │ ├── TechTreeMgr.gd
|
||||||
|
│ │ ├── UnlockMgr.gd
|
||||||
|
│ │ └── SaveMgr.gd
|
||||||
|
│ ├── ui/
|
||||||
|
│ │ ├── TechTreeUI.gd
|
||||||
|
│ │ └── EfficiencyPanel.gd
|
||||||
|
│ └── logic/
|
||||||
|
│ ├── TechNode.gd
|
||||||
|
│ └── Unlockable.gd
|
||||||
|
│
|
||||||
|
├── resources/
|
||||||
|
│ ├── tech_tree.tres # Root tech‑tree Resource
|
||||||
|
│ ├── tech_nodes/
|
||||||
|
│ │ ├── basic_attack.tres
|
||||||
|
│ │ ├── advanced_attack.tres
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── fonts/
|
||||||
|
│ └── main_font.ttf
|
||||||
|
│
|
||||||
|
├── data/
|
||||||
|
│ └── player_save.json # runtime save file
|
||||||
|
│
|
||||||
|
└── assets/
|
||||||
|
├── textures/
|
||||||
|
├── sounds/
|
||||||
|
└── music/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data‑Driven Tech‑Tree Design
|
||||||
|
|
||||||
|
### 4.1. TechNode Resource
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# TechNode.tres (extends Resource)
|
||||||
|
@tool
|
||||||
|
class_name TechNode
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var id: String
|
||||||
|
@export var name: String
|
||||||
|
@export var description: String
|
||||||
|
@export var cost: int
|
||||||
|
@export var prerequisites: Array[String] = []
|
||||||
|
@export var effect: Dictionary # e.g. {"auto_completion": 0.1}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a `.tres` for each node in the Godot editor.
|
||||||
|
You can also generate them programmatically from a JSON file on first run.
|
||||||
|
|
||||||
|
### 4.2. TechTree Resource
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tech_tree.tres (extends Resource)
|
||||||
|
@tool
|
||||||
|
class_name TechTree
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var nodes: Array[TechNode] = []
|
||||||
|
|
||||||
|
func get_node_by_id(id: String) -> TechNode:
|
||||||
|
for node in nodes:
|
||||||
|
if node.id == id:
|
||||||
|
return node
|
||||||
|
return null
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. Unlock Manager
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# UnlockMgr.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal unlocked(node_id)
|
||||||
|
|
||||||
|
var unlocked_nodes: Set[String] = {}
|
||||||
|
var tech_tree: TechTree
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
tech_tree = preload("res://resources/tech_tree.tres")
|
||||||
|
load_unlocks()
|
||||||
|
|
||||||
|
func can_unlock(node_id: String) -> bool:
|
||||||
|
var node = tech_tree.get_node_by_id(node_id)
|
||||||
|
if not node: return false
|
||||||
|
# Already unlocked?
|
||||||
|
if unlocked_nodes.has(node_id): return false
|
||||||
|
# Check prerequisites
|
||||||
|
for prereq in node.prerequisites:
|
||||||
|
if not unlocked_nodes.has(prereq):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
func unlock(node_id: String):
|
||||||
|
var node = tech_tree.get_node_by_id(node_id)
|
||||||
|
if node and can_unlock(node_id):
|
||||||
|
unlocked_nodes.insert(node_id)
|
||||||
|
emit_signal("unlocked", node_id)
|
||||||
|
# Apply node.effect...
|
||||||
|
SaveMgr.save_unlocks()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Persistence
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# SaveMgr.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var save_path = "user://player_save.json"
|
||||||
|
|
||||||
|
func save_unlocks():
|
||||||
|
var data = {"unlocked": Array(unlock_mgr.unlocked_nodes)}
|
||||||
|
var file = FileAccess.open(save_path, FileAccess.WRITE)
|
||||||
|
file.store_string(JSON.stringify(data))
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
func load_unlocks():
|
||||||
|
if FileAccess.file_exists(save_path):
|
||||||
|
var file = FileAccess.open(save_path, FileAccess.READ)
|
||||||
|
var data = JSON.parse_string(file.get_as_text())
|
||||||
|
unlock_mgr.unlocked_nodes = Set(data.unlocked)
|
||||||
|
file.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI & Interaction
|
||||||
|
|
||||||
|
### 5.1. Tech Tree UI
|
||||||
|
|
||||||
|
- **Tree** node for hierarchical display.
|
||||||
|
- Each row is a `TechNode.tscn` instance.
|
||||||
|
- Use `Button` to click an unlockable node.
|
||||||
|
- Disable button when `can_unlock()` returns false.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# TechTreeUI.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
_populate_tree()
|
||||||
|
unlock_mgr.connect("unlocked", self, "_on_node_unlocked")
|
||||||
|
|
||||||
|
func _populate_tree():
|
||||||
|
var tree = $Tree
|
||||||
|
for node in tech_tree.nodes:
|
||||||
|
var item = tree.create_item()
|
||||||
|
item.set_text(0, node.name)
|
||||||
|
item.set_meta("node_id", node.id)
|
||||||
|
item.set_meta("cost", node.cost)
|
||||||
|
item.set_meta("prereqs", node.prerequisites)
|
||||||
|
|
||||||
|
# Style based on unlock state
|
||||||
|
if unlock_mgr.can_unlock(node.id):
|
||||||
|
item.set_custom_color(0, Color(0, 1, 0))
|
||||||
|
else:
|
||||||
|
item.set_custom_color(0, Color(1, 0, 0))
|
||||||
|
|
||||||
|
func _on_tree_item_selected(item):
|
||||||
|
var node_id = item.get_meta("node_id")
|
||||||
|
if unlock_mgr.can_unlock(node_id):
|
||||||
|
# Show confirm dialog / cost UI
|
||||||
|
_attempt_unlock(node_id)
|
||||||
|
|
||||||
|
func _attempt_unlock(node_id):
|
||||||
|
# Check if player has enough points
|
||||||
|
if Game.efficiency_points >= tech_tree.get_node_by_id(node_id).cost:
|
||||||
|
Game.efficiency_points -= tech_tree.get_node_by_id(node_id).cost
|
||||||
|
unlock_mgr.unlock(node_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2. Efficiency Panel
|
||||||
|
|
||||||
|
Shows current points, last press, best streak, etc.
|
||||||
|
Update it via signals from `InputMgr`.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# EfficiencyPanel.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
InputMgr.connect("letter_pressed", self, "_on_letter_pressed")
|
||||||
|
|
||||||
|
func _on_letter_pressed():
|
||||||
|
$PointsLabel.text = str(Game.efficiency_points)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Input & Performance
|
||||||
|
|
||||||
|
### 6.1. Input Manager
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# InputMgr.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal letter_pressed
|
||||||
|
|
||||||
|
var last_press_time: float = 0.0
|
||||||
|
var press_interval: float = 0.0
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
|
letter_pressed.emit()
|
||||||
|
var now = OS.get_ticks_msec()
|
||||||
|
press_interval = now - last_press_time
|
||||||
|
last_press_time = now
|
||||||
|
# Update efficiency
|
||||||
|
Game.update_efficiency(press_interval)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2. Efficiency Calculation
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Game.gd
|
||||||
|
var efficiency_points: int = 0
|
||||||
|
|
||||||
|
func update_efficiency(interval_ms: int):
|
||||||
|
# Basic formula: the faster the press, the more points
|
||||||
|
var bonus = 1000 / max(interval_ms, 1)
|
||||||
|
efficiency_points += int(bonus)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Keep the calculation simple; heavy math is avoided to preserve low latency.
|
||||||
|
- All heavy UI updates happen after input is processed (in `_process`), not in `_input`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Unlock Effects & Extensibility
|
||||||
|
|
||||||
|
Each `TechNode` can point to a **script** that implements `Unlockable`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Unlockable.gd
|
||||||
|
class_name Unlockable
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func apply():
|
||||||
|
# Override in subclasses
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: auto‑completion unlocker
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# AutoCompletionUnlock.gd
|
||||||
|
extends Unlockable
|
||||||
|
|
||||||
|
func apply():
|
||||||
|
Game.auto_completion_enabled = true
|
||||||
|
Game.auto_completion_rate = 0.15 # 15% of letters auto‑filled
|
||||||
|
```
|
||||||
|
|
||||||
|
When unlocking, the manager loads the script from a `preload` path stored in the node’s `effect` dictionary and calls `apply()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Audio / Animation Feedback
|
||||||
|
|
||||||
|
- **Unlock animation**: play a short tween on the node in the tree.
|
||||||
|
- **Unlock sound**: `AudioStreamPlayer.play()` triggered in `unlock()`.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func unlock(node_id):
|
||||||
|
...
|
||||||
|
var node = tech_tree.get_node_by_id(node_id)
|
||||||
|
var effect_path = node.effect.get("unlock_effect", null)
|
||||||
|
if effect_path:
|
||||||
|
var effect = preload(effect_path).instantiate()
|
||||||
|
get_parent().add_child(effect)
|
||||||
|
effect.play()
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Testing & Continuous Integration
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| **GUT** | Unit tests for `TechTreeMgr`, `UnlockMgr`, `InputMgr`. |
|
||||||
|
| **GodotCI** | Run headless builds on every push to ensure build stability. |
|
||||||
|
| **GitHub Actions** | Auto‑run tests on multiple OS (Windows, macOS, Linux). |
|
||||||
|
|
||||||
|
Example GUT test for unlock logic:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class TestUnlockMgr:
|
||||||
|
func test_cannot_unlock_without_prereqs():
|
||||||
|
var mgr = preload("res://scripts/autoload/UnlockMgr.gd").new()
|
||||||
|
mgr.tech_tree = preload("res://resources/tech_tree.tres")
|
||||||
|
mgr.unlocked_nodes = {}
|
||||||
|
assert_false(mgr.can_unlock("advanced_attack"))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Milestones & Timeline (6‑week sprint)
|
||||||
|
|
||||||
|
| Week | Focus |
|
||||||
|
|------|-------|
|
||||||
|
| 1 | Set up Godot project, autoloads, core game loop, basic input handling. |
|
||||||
|
| 2 | Implement TechNode/TechTree resources, UI skeleton, placeholder nodes. |
|
||||||
|
| 3 | Unlock logic, persistence, test harness. |
|
||||||
|
| 4 | Audio / animation feedback, performance profiling, input optimization. |
|
||||||
|
| 5 | Polish UI, add more unlock effects, balance efficiency calculations. |
|
||||||
|
| 6 | QA, automated tests, build, and deployment to test devices. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Key Deliverables
|
||||||
|
|
||||||
|
1. **Tech‑Tree System** – Data‑driven, resource‑based, UI‑integrated.
|
||||||
|
2. **Unlock Engine** – Handles prerequisites, cost, persistence, effect application.
|
||||||
|
3. **Efficiency Core** – Real‑time point system, high‑frequency input, minimal latency.
|
||||||
|
3. **Extensible Unlock Effects** – Scriptable unlockables for future expansion.
|
||||||
|
4. **Audio/Visual Feedback** – Engaging unlock animations & sounds.
|
||||||
|
5. **Automated Test Suite** – Ensures code quality for every future feature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Bottom line
|
||||||
|
|
||||||
|
By modeling each unlockable node as a Godot `Resource`, wiring it to a simple UI tree, and persisting state with a lightweight JSON file, the tech‑tree and unlock system stay **modular, maintainable, and fast**.
|
||||||
|
The input manager focuses solely on capturing key events, while all heavy UI updates are deferred, guaranteeing the pressing rhythm feels snappy—exactly what the game demands.
|
||||||
131
ssug-meeting-summarizer/main.py
Normal file
131
ssug-meeting-summarizer/main.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# makes a request to a local ollama server and streams the response
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
OLLAMA_SERVER = "10.48.9.106"
|
||||||
|
OLLAMA_PORT = 11434
|
||||||
|
SYSTEM_PROMPTS = [
|
||||||
|
"Always answer in a CLI-friendly, plain text format. Use Markdown code blocks for code, and ensure code is easy to copy and paste. Do not use colors or special formatting that breaks copy-paste. Only use plain Markdown for code.",
|
||||||
|
]
|
||||||
|
HIST_CONTEXT = []
|
||||||
|
|
||||||
|
def ping_server():
|
||||||
|
url = f"http://{OLLAMA_SERVER}:{OLLAMA_PORT}"
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("Ollama server is reachable.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Failed to reach Ollama server: {response.status_code}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error connecting to Ollama server: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stream_ollama_response(model, prompt, output_format="cli"):
|
||||||
|
url = f"http://{OLLAMA_SERVER}:{OLLAMA_PORT}/api/chat"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [],
|
||||||
|
"stream": True
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg in SYSTEM_PROMPTS:
|
||||||
|
data["messages"].append({"system": msg})
|
||||||
|
|
||||||
|
for hist in HIST_CONTEXT:
|
||||||
|
if hist["prompt"] and hist["response"]:
|
||||||
|
data["messages"].append({"role": "user", "content": hist["prompt"]})
|
||||||
|
if hist["thought"]:
|
||||||
|
data["messages"].append({"role": "assistant", "content": hist["thought"]})
|
||||||
|
data["messages"].append({"role": "assistant", "content": hist["response"]})
|
||||||
|
|
||||||
|
data["messages"].append({"role": "user", "content": prompt})
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, json=data, stream=True)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"Error: {response.status_code} - {response.text}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
print("Thinking (plain text, code blocks are copy-paste ready):")
|
||||||
|
in_thinking = True
|
||||||
|
thoughts = ""
|
||||||
|
response_text = ""
|
||||||
|
message = {}
|
||||||
|
|
||||||
|
for line in response.iter_lines():
|
||||||
|
if line:
|
||||||
|
decoded_line = line.decode('utf-8')
|
||||||
|
if decoded_line.startswith("data: "):
|
||||||
|
decoded_line = decoded_line[6:]
|
||||||
|
if decoded_line == "[DONE]":
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
chunk = json.loads(decoded_line)
|
||||||
|
if 'message' in chunk:
|
||||||
|
message = chunk['message']
|
||||||
|
# Stream 'thinking' tokens live
|
||||||
|
if 'thinking' in message and message['thinking']:
|
||||||
|
sys.stdout.write(message['thinking'])
|
||||||
|
sys.stdout.flush()
|
||||||
|
thoughts += message['thinking']
|
||||||
|
# When content starts, print a newline and stream content live
|
||||||
|
if 'content' in message and message['content']:
|
||||||
|
if in_thinking:
|
||||||
|
print("\n\nResponse (copy-paste code blocks as needed):")
|
||||||
|
in_thinking = False
|
||||||
|
sys.stdout.write(message['content'])
|
||||||
|
sys.stdout.flush()
|
||||||
|
response_text += message['content']
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
print() # for newline after completion
|
||||||
|
|
||||||
|
# add to history context
|
||||||
|
HIST_CONTEXT.append({
|
||||||
|
"response_time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"prompt": prompt,
|
||||||
|
"thought": thoughts,
|
||||||
|
"response": response_text
|
||||||
|
})
|
||||||
|
return response_text
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Stream response from local Ollama server.")
|
||||||
|
parser.add_argument("model", type=str, help="The model to use (e.g., 'llama2', 'gpt4o').")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.model:
|
||||||
|
print("Model is required.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not ping_server():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conversation_start_time = time.strftime("%Y%m%d-%H%M%S")
|
||||||
|
output_filename = f"conversation_{conversation_start_time}.txt"
|
||||||
|
print(f"\nEnter your prompt below. Type 'q', 'quit', 'done', or 'bye' to end the conversation.\nAll responses will be saved to {output_filename}.\n")
|
||||||
|
|
||||||
|
with open(output_filename, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"Conversation started at {conversation_start_time}\nModel: {args.model}\n\n")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
prompt = input("You: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nConversation ended.")
|
||||||
|
break
|
||||||
|
if prompt.lower() in {"q", "quit", "done", "bye"}:
|
||||||
|
print("Conversation ended.")
|
||||||
|
break
|
||||||
|
if not prompt:
|
||||||
|
continue
|
||||||
|
f.write(f"You: {prompt}\n")
|
||||||
|
response_text = stream_ollama_response(args.model, prompt, output_format="cli")
|
||||||
|
f.write(f"AI: {response_text}\n\n")
|
||||||
Loading…
Reference in New Issue
Block a user