File size: 8,007 Bytes
c0f5b84
 
 
 
 
 
bf30843
c0f5b84
 
bb2022a
c0f5b84
 
 
 
 
 
0a0dfe3
 
c0f5b84
 
 
 
 
 
bf30843
c0f5b84
 
0a0dfe3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c0f5b84
bf30843
0a0dfe3
 
c0f5b84
0a0dfe3
bf30843
 
c0f5b84
bf30843
c0f5b84
bf30843
c0f5b84
 
bf30843
 
0a0dfe3
bf30843
0a0dfe3
 
 
bf30843
 
 
0a0dfe3
bf30843
 
 
bb2022a
0a0dfe3
 
bb2022a
 
 
 
 
 
 
 
 
 
0a0dfe3
 
 
 
 
bf30843
 
 
c0f5b84
 
0a0dfe3
c0f5b84
bf30843
0a0dfe3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c0f5b84
0a0dfe3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import streamlit as st  # ๐ŸŒ Streamlit magic
import streamlit.components.v1 as components  # ๐Ÿ–ผ๏ธ Embed custom HTML/JS
import os  # ๐Ÿ“‚ File operations
import json  # ๐Ÿ”„ JSON encoding/decoding
import pandas as pd  # ๐Ÿ“Š DataFrame handling
import uuid  # ๐Ÿ†” Unique IDs
import math  # โž— Math utils
import time  # โณ Time utilities

from gamestate import GameState  # ๐Ÿ’ผ Shared gameโ€state singleton

# ๐Ÿš€ Page setup
st.set_page_config(page_title="Infinite World Builder", layout="wide")

# ๐Ÿ“ Constants for world dimensions & CSV schema
SAVE_DIR = "saved_worlds"
PLOT_WIDTH = 50.0   # โ†”๏ธ Plot width in world units
PLOT_DEPTH = 50.0   # โ†•๏ธ Plot depth in world units
CSV_COLUMNS = [
    'obj_id', 'type',
    'pos_x', 'pos_y', 'pos_z',
    'rot_x', 'rot_y', 'rot_z', 'rot_order'
]

# ๐Ÿ—‚๏ธ Ensure directory for plots exists
os.makedirs(SAVE_DIR, exist_ok=True)

@st.cache_data(ttl=3600)  # ๐Ÿ•’ Cache for 1h
def load_plot_metadata():
    """Scan SAVE_DIR for existing plot CSVs and parse their grid coords."""
    try:
        files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
    except FileNotFoundError:
        return []
    parsed = []
    for fn in files:
        parts = fn[:-4].split('_')
        try:
            gx = int(parts[1][1:])
            gz = int(parts[2][1:])
            name = "_".join(parts[3:]) if len(parts) > 3 else f"Plot({gx},{gz})"
            parsed.append({
                'id': fn[:-4],
                'filename': fn,
                'grid_x': gx,
                'grid_z': gz,
                'name': name,
                'x_offset': gx * PLOT_WIDTH,
                'z_offset': gz * PLOT_DEPTH
            })
        except:
            continue
    parsed.sort(key=lambda p: (p['grid_x'], p['grid_z']))
    return parsed

def load_plot_objects(filename, x_offset, z_offset):
    """Read a plot CSV and shift each object's position by the plot offset."""
    path = os.path.join(SAVE_DIR, filename)
    try:
        df = pd.read_csv(path)
    except (FileNotFoundError, pd.errors.EmptyDataError):
        return []
    # ensure columns
    for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
        if col not in df.columns:
            df[col] = default
    if 'obj_id' not in df.columns:
        df['obj_id'] = [str(uuid.uuid4()) for _ in df.index]
    objs = []
    for row in df.to_dict(orient='records'):
        objs.append({
            'obj_id': row['obj_id'],
            'type':   row['type'],
            'pos_x':  row['pos_x'] + x_offset,
            'pos_y':  row['pos_y'],
            'pos_z':  row['pos_z'] + z_offset,
            'rot_x':  row['rot_x'],
            'rot_y':  row['rot_y'],
            'rot_z':  row['rot_z'],
            'rot_order': row['rot_order']
        })
    return objs

def save_plot_data(filename, objects_list, px, pz):
    """Save newly placed objects back to the corresponding plot CSV."""
    path = os.path.join(SAVE_DIR, filename)
    records = []
    for o in objects_list:
        pos = o.get('position', {})
        rot = o.get('rotation', {})
        if 'x' not in pos or 'y' not in pos or 'z' not in pos:
            continue
        records.append({
            'obj_id':     o.get('obj_id', str(uuid.uuid4())),
            'type':       o.get('type', 'Unknown'),
            'pos_x':      pos['x'] - px,
            'pos_y':      pos['y'],
            'pos_z':      pos['z'] - pz,
            'rot_x':      rot.get('_x', 0.0),
            'rot_y':      rot.get('_y', 0.0),
            'rot_z':      rot.get('_z', 0.0),
            'rot_order':  rot.get('_order', 'XYZ')
        })
    try:
        pd.DataFrame(records, columns=CSV_COLUMNS).to_csv(path, index=False)
        st.success(f"๐ŸŽ‰ Saved {len(records)} objects to {filename}")
        return True
    except Exception as e:
        st.error(f"Save failed: {e}")
        return False

# ๐Ÿ”’ Singleton for global world state
@st.cache_resource
def get_game_state():
    return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")

game_state = get_game_state()

# ๐Ÿง  Session state defaults
st.session_state.setdefault('selected_object', 'None')
st.session_state.setdefault('js_save_processor', None)

# ๐Ÿ”„ Load all saved plots + objects
plots_metadata = load_plot_metadata()
all_initial_objects = []
for p in plots_metadata:
    all_initial_objects += load_plot_objects(p['filename'], p['x_offset'], p['z_offset'])

# ๐Ÿ–ฅ๏ธ Sidebar UI
with st.sidebar:
    st.title("๐Ÿ—๏ธ World Controls")
    st.header("๐Ÿ“ Navigate Plots")
    cols = st.columns(2)
    for idx, p in enumerate(plots_metadata):
        label = f"โžก๏ธ {p['name']} ({p['grid_x']},{p['grid_z']})"
        if cols[idx % 2].button(label, key=f"nav_{p['id']}"):
            from streamlit_js_eval import streamlit_js_eval
            js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});"
            try:
                streamlit_js_eval(js_code=js, key=f"tp_{p['id']}")
            except Exception as e:
                st.error(f"Teleport failed: {e}")

    st.markdown("---")
    st.header("๐ŸŒฒ Place Objects")

    # โ†โ”€โ”€โ”€ four new builder-tool options added here โ”€โ”€โ”€โ”€โ†’
    options = [
        "None",
        "Simple House",
        "Tree",
        "Rock",
        "Fence Post",
        "Cyberpunk City Builder Kit",
        "POLYGON - Fantasy Kingdom Pack",
        "HEROIC FANTASY CREATURES FULL PACKย VOLย 1",
        "POLYGON - Apocalypse Pack"
    ]
    sel = st.selectbox("Select:", options,
        index=options.index(st.session_state.selected_object)
              if st.session_state.selected_object in options else 0,
        key="selected_object_widget"
    )
    if sel != st.session_state.selected_object:
        st.session_state.selected_object = sel

    st.markdown("---")
    st.header("๐Ÿ’พ Save Work")
    if st.button("๐Ÿ’พ Save Current Work"):
        from streamlit_js_eval import streamlit_js_eval
        streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor")
        st.experimental_rerun()

# ๐Ÿ“จ Handle incoming save-data callback
raw = st.session_state.get("js_save_processor")
if raw:
    try:
        payload = json.loads(raw) if isinstance(raw, str) else raw
        pos = payload.get('playerPosition')
        objs = payload.get('objectsToSave', [])
        gx = math.floor(pos['x'] / PLOT_WIDTH)
        gz = math.floor(pos['z'] / PLOT_DEPTH)
        fn = f"plot_X{gx}_Z{gz}.csv"
        if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH):
            load_plot_metadata.clear()
            st.session_state.js_save_processor = None
            st.experimental_rerun()
    except Exception as e:
        st.error(f"Error saving data: {e}")

# ๐Ÿ  Main view & HTML injection
st.header("๐ŸŒ Infinite Shared 3D World")
st.caption("โžก๏ธ Explore, click to build, ๐Ÿ’พ to save!")

state = {
    "ALL_INITIAL_OBJECTS": all_initial_objects,
    "PLOTS_METADATA":      plots_metadata,
    "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
    "PLOT_WIDTH":           PLOT_WIDTH,
    "PLOT_DEPTH":           PLOT_DEPTH,
    "GAME_STATE":           game_state.get_state()
}

try:
    with open('index.html', 'r', encoding='utf-8') as f:
        html = f.read()
    inject = f"""
<script>
  window.ALL_INITIAL_OBJECTS = {json.dumps(state["ALL_INITIAL_OBJECTS"])};
  window.PLOTS_METADATA   = {json.dumps(state["PLOTS_METADATA"])};
  window.SELECTED_OBJECT_TYPE = {json.dumps(state["SELECTED_OBJECT_TYPE"])};
  window.PLOT_WIDTH = {json.dumps(state["PLOT_WIDTH"])};
  window.PLOT_DEPTH = {json.dumps(state["PLOT_DEPTH"])};
  window.GAME_STATE = {json.dumps(state["GAME_STATE"])};
</script>
"""
    html = html.replace("</head>", inject + "\n</head>", 1)
    components.html(html, height=750, scrolling=False)
except FileNotFoundError:
    st.error("โŒ index.html not found!")
except Exception as e:
    st.error(f"HTML injection failed: {e}")