Spaces:
Sleeping
Sleeping
import streamlit as st | |
import numpy as np | |
import pandas as pd | |
import json | |
import asyncio | |
import websockets | |
import base64 | |
from io import BytesIO | |
from PIL import Image | |
import pythreejs as three | |
from streamlit.components.v1 import html | |
# --- Shared World State --- | |
def init_world_state(): | |
return { | |
"players": {}, # {player_id: {x, y, z}} | |
"elements": [], # List of placed comic elements {id, type, x, y, content} | |
"bounds": 100, # Radius of the spherical world | |
} | |
world_state = init_world_state() | |
# --- Multiplayer WebSocket Server --- | |
async def websocket_handler(websocket, path): | |
player_id = str(id(websocket)) | |
world_state["players"][player_id] = {"x": 0, "y": 0, "z": 0} | |
try: | |
async for message in websocket: | |
data = json.loads(message) | |
if "position" in data: | |
world_state["players"][player_id] = data["position"] | |
elif "element" in data: | |
world_state["elements"].append(data["element"]) | |
await websocket.send(json.dumps(world_state)) | |
finally: | |
del world_state["players"][player_id] | |
# Start WebSocket server in a separate thread | |
async def start_websocket_server(): | |
server = await websockets.serve(websocket_handler, "localhost", 8765) | |
await asyncio.Future() # Run forever | |
if "websocket_server" not in st.session_state: | |
st.session_state.websocket_server = True | |
asyncio.run_coroutine_threadsafe(start_websocket_server(), asyncio.get_event_loop()) | |
# --- Streamlit App --- | |
st.title("Neon Emissary’s Promise: A Sci-Fi Horror Comic Adventure") | |
# --- Comic Cover Display with Improved Font Styles --- | |
st.header("Comic Cover: Neon Emissary’s Promise") | |
st.markdown(""" | |
A stained glass mosaic artwork of a futuristic envoy, composed of jewel-toned, colored glass fragments arranged into intricate patterns, with refracted light and vibrant, luminous reflections, standing beneath a vault of radiant pink chrome. | |
""") | |
# Custom CSS for improved font styles inspired by horror comic aesthetics | |
st.markdown(""" | |
<style> | |
.comic-title { | |
font-family: 'Creepster', cursive; /* A jagged, horror-inspired font */ | |
font-size: 48px; | |
color: #ff0000; /* Blood red */ | |
text-shadow: 3px 3px 5px #000, -3px -3px 5px #ff00ff; /* Neon pink shadow for depth */ | |
letter-spacing: 2px; | |
transform: rotate(-5deg); /* Slight tilt for dynamic tension */ | |
margin-bottom: 10px; | |
} | |
.comic-subtitle { | |
font-family: 'Nosifer', cursive; /* Distressed, dripping font */ | |
font-size: 24px; | |
color: #00ff00; /* Neon green */ | |
text-shadow: 2px 2px 4px #000; | |
letter-spacing: 1px; | |
transform: rotate(3deg); | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
st.markdown('<div class="comic-title">Neon Emissary’s Promise</div>', unsafe_allow_html=True) | |
st.markdown('<div class="comic-subtitle">A Tale of Love and Rebirth in Know It All</div>', unsafe_allow_html=True) | |
# --- Game Interface --- | |
st.header("Interactive Comic Adventure") | |
# Player ID for multiplayer | |
if "player_id" not in st.session_state: | |
st.session_state.player_id = str(np.random.randint(1000000)) | |
# --- Drag-and-Drop Canvas with JavaScript --- | |
st.subheader("Comic Canvas (Drag and Drop Elements)") | |
canvas_html = """ | |
<canvas id="comicCanvas" width="800" height="400" style="border:1px solid black;"></canvas> | |
<div> | |
<button onclick="addElement('character')">Add Character</button> | |
<button onclick="addElement('speech')">Add Speech Bubble</button> | |
</div> | |
<script> | |
const canvas = document.getElementById('comicCanvas'); | |
const ctx = canvas.getContext('2d'); | |
let elements = []; | |
let selectedElement = null; | |
function drawCanvas() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
elements.forEach(el => { | |
if (el.type === 'character') { | |
ctx.fillStyle = '#ff00ff'; | |
ctx.fillRect(el.x, el.y, 50, 50); | |
} else if (el.type === 'speech') { | |
ctx.fillStyle = '#ffffff'; | |
ctx.fillRect(el.x, el.y, 100, 40); | |
ctx.fillStyle = '#000000'; | |
ctx.font = '12px Arial'; | |
ctx.fillText(el.content || 'Speak...', el.x + 10, el.y + 25); | |
} | |
}); | |
} | |
function addElement(type) { | |
const newElement = { id: Date.now(), type, x: 50, y: 50, content: type === 'speech' ? 'Speak...' : null }; | |
elements.push(newElement); | |
drawCanvas(); | |
// Send to WebSocket | |
const ws = new WebSocket('ws://localhost:8765'); | |
ws.onopen = () => ws.send(JSON.stringify({ element: newElement })); | |
} | |
canvas.addEventListener('mousedown', (e) => { | |
const rect = canvas.getBoundingClientRect(); | |
const x = e.clientX - rect.left; | |
const y = e.clientY - rect.top; | |
selectedElement = elements.find(el => | |
x >= el.x && x <= el.x + (el.type === 'speech' ? 100 : 50) && | |
y >= el.y && y <= el.y + (el.type === 'speech' ? 40 : 50) | |
); | |
}); | |
canvas.addEventListener('mousemove', (e) => { | |
if (selectedElement) { | |
const rect = canvas.getBoundingClientRect(); | |
selectedElement.x = e.clientX - rect.left - (selectedElement.type === 'speech' ? 50 : 25); | |
selectedElement.y = e.clientY - rect.top - (selectedElement.type === 'speech' ? 20 : 25); | |
drawCanvas(); | |
} | |
}); | |
canvas.addEventListener('mouseup', () => { | |
selectedElement = null; | |
}); | |
// WebSocket to receive updates | |
const ws = new WebSocket('ws://localhost:8765'); | |
ws.onmessage = (event) => { | |
const data = JSON.parse(event.data); | |
elements = data.elements; | |
drawCanvas(); | |
}; | |
</script> | |
""" | |
html(canvas_html, height=500) | |
# --- 3D Particle System for Multiplayer --- | |
st.subheader("Multiplayer World (3D Particle System)") | |
threejs_html = """ | |
<script src="https://unpkg.com/[email protected]/build/three.min.js"></script> | |
<div id="threejs-container" style="width:800px; height:400px;"></div> | |
<script> | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, 800 / 400, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer(); | |
renderer.setSize(800, 400); | |
document.getElementById('threejs-container').appendChild(renderer.domElement); | |
// Create a sphere of particles | |
const particles = new THREE.Group(); | |
const particleCount = 100; | |
const radius = 100; | |
for (let i = 0; i < particleCount; i++) { | |
const geometry = new THREE.SphereGeometry(1, 32, 32); | |
const material = new THREE.MeshBasicMaterial({ color: 0xff00ff }); | |
const particle = new THREE.Mesh(geometry, material); | |
const theta = Math.random() * Math.PI * 2; | |
const phi = Math.acos(2 * Math.random() - 1); | |
particle.position.set( | |
radius * Math.sin(phi) * Math.cos(theta), | |
radius * Math.sin(phi) * Math.sin(theta), | |
radius * Math.cos(phi) | |
); | |
particles.add(particle); | |
} | |
scene.add(particles); | |
// Players as glowing spheres | |
const playersGroup = new THREE.Group(); | |
scene.add(playersGroup); | |
camera.position.z = 150; | |
function animate() { | |
requestAnimationFrame(animate); | |
particles.rotation.y += 0.01; | |
renderer.render(scene, camera); | |
} | |
animate(); | |
// WebSocket to update player positions | |
const ws = new WebSocket('ws://localhost:8765'); | |
ws.onmessage = (event) => { | |
const data = JSON.parse(event.data); | |
playersGroup.children.forEach(child => playersGroup.remove(child)); | |
Object.entries(data.players).forEach(([id, pos]) => { | |
const geometry = new THREE.SphereGeometry(3, 32, 32); | |
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); | |
const player = new THREE.Mesh(geometry, material); | |
player.position.set(pos.x, pos.y, pos.z); | |
playersGroup.add(player); | |
}); | |
}; | |
// Send player position | |
document.addEventListener('mousemove', (e) => { | |
const x = (e.clientX / window.innerWidth) * 200 - 100; | |
const y = -(e.clientY / window.innerHeight) * 200 + 100; | |
const z = 0; | |
if (ws.readyState === WebSocket.OPEN) { | |
ws.send(JSON.stringify({ position: { x, y, z } })); | |
} | |
}); | |
</script> | |
""" | |
html(threejs_html, height=450) | |
# --- Story Dialogue --- | |
st.header("Story: Neon Emissary’s Promise") | |
st.write(""" | |
**Scene:** A futuristic envoy, rendered as a stained glass mosaic, stands beneath a vault of radiant pink chrome in the city of Know It All, where giant brain skyscrapers pulse with LED lights. | |
**Dialogue:** | |
*“In these kaleidoscopic shards, every broken piece whispers the promise of rebirth,”* she confesses, eyes sparkling amid refracted light. | |
*“Our love etches its vow into every fragment of this luminous city,”* he replies, as vibrant reflections underscore their fateful meeting. | |
""") | |
# --- World Building Tests --- | |
st.header("World Building Tests") | |
if st.button("Generate Random Event"): | |
event = np.random.choice([ | |
"A neon storm erupts, casting fractured light across the city.", | |
"A brain skyscraper pulses, revealing a hidden memory in the shards.", | |
"A rival envoy appears, challenging the promise of rebirth." | |
]) | |
st.write(event) | |
world_state["elements"].append({ | |
"id": str(np.random.randint(1000000)), | |
"type": "speech", | |
"x": np.random.randint(50, 750), | |
"y": np.random.randint(50, 350), | |
"content": event | |
}) |