commit b0b3bd9352b8b7dee3354ca4b6d362bc9b4b0330 Author: Sepp Jeremiah Morris Date: Sat Sep 13 21:12:46 2025 +0200 init project release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/Backend/Dockerfile b/Backend/Dockerfile new file mode 100644 index 0000000..edbf2b1 --- /dev/null +++ b/Backend/Dockerfile @@ -0,0 +1,25 @@ +FROM --platform=$BUILDPLATFORM golang:1.25.1-alpine AS build +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download + +COPY . . + +RUN --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ + go build -trimpath -ldflags="-s -w" -o /out/app . + +FROM gcr.io/distroless/static-debian12:nonroot + +COPY --from=build /out/app /app + +USER nonroot:nonroot + +# EXPOSE 8080 + +ENTRYPOINT ["/app"] diff --git a/Backend/go.mod b/Backend/go.mod new file mode 100644 index 0000000..5127917 --- /dev/null +++ b/Backend/go.mod @@ -0,0 +1,10 @@ +module main + +go 1.25.1 + +require ( + github.com/gorilla/websocket v1.5.3 + github.com/vmihailenco/msgpack/v5 v5.4.1 +) + +require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/Backend/go.sum b/Backend/go.sum new file mode 100644 index 0000000..3761b5d --- /dev/null +++ b/Backend/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/Backend/main.go b/Backend/main.go new file mode 100644 index 0000000..8b17e81 --- /dev/null +++ b/Backend/main.go @@ -0,0 +1,416 @@ +package main + +import ( + cryptoRand "crypto/rand" + "encoding/hex" + "flag" + "log" + "math/rand" + "net/http" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/vmihailenco/msgpack/v5" +) + +// -------------------- Config -------------------- +const ( + AddrHTTP = ":8080" + MaxPlayers = 10 + TickRateHz = 15 + NameMaxLen = 25 + ChatMaxLen = 500 + StateBufLimit = 256 + + // Chat Anti-spam + MinChatInterval = 400 * time.Millisecond +) + +// Allowed specified origins for browser WS connections +var allowedOrigins = map[string]bool{ + "https://world.seppjm.com": true, + "https://seppjm.com": true, + "https://cv.seppjm.com": true, + "https://seppdroid.com": true, + "https://sepp.mx": true, + //"http://world.seppjm.com": true, // (dev) + //"http://localhost:5173": true, // (dev) + //"http://localhost:8080": true, // (dev) +} + +// -------------------- Types -------------------- +type Role string + +const ( + RolePlayer Role = "player" + RoleSpectator Role = "spectator" +) + +type Vec3 [3]float64 + +type Player struct { + ID string `msgpack:"id"` + Name string `msgpack:"name"` + Pos Vec3 `msgpack:"pos"` + RotY float64 `msgpack:"rotY"` +} + +type Client struct { + Conn *websocket.Conn + Hub *Hub + ID string + Name string + Role Role + Send chan []byte + Closed chan struct{} + LastChat time.Time + connected time.Time +} + +type Hub struct { + mu sync.RWMutex + clients map[string]*Client // id -> client + players map[string]*Player // id -> player + joinCh chan *Client + leaveCh chan *Client + broadcast chan []byte +} + +func NewHub() *Hub { + return &Hub{ + clients: make(map[string]*Client), + players: make(map[string]*Player), + joinCh: make(chan *Client, 64), + leaveCh: make(chan *Client, 64), + broadcast: make(chan []byte, 1024), + } +} + +func (h *Hub) Run() { + ticker := time.NewTicker(time.Second / TickRateHz) + defer ticker.Stop() + for { + select { + case c := <-h.joinCh: + h.addClient(c) + case c := <-h.leaveCh: + h.removeClient(c) + case msg := <-h.broadcast: + h.mu.RLock() + for _, cl := range h.clients { + select { + case cl.Send <- msg: + default: + } + } + h.mu.RUnlock() + case <-ticker.C: + h.tickState() + } + } +} + +func (h *Hub) addClient(c *Client) { + h.mu.Lock() + h.clients[c.ID] = c + h.mu.Unlock() +} + +func (h *Hub) removeClient(c *Client) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.clients, c.ID) + if _, ok := h.players[c.ID]; ok { + delete(h.players, c.ID) + packet := map[string]any{"type": "player_leave", "id": c.ID} + if b, _ := msgpack.Marshal(packet); b != nil { + for _, cl := range h.clients { + select { + case cl.Send <- b: + default: + } + } + } + } +} + +func (h *Hub) tickState() { + h.mu.RLock() + if len(h.players) == 0 || len(h.clients) == 0 { + h.mu.RUnlock() + return + } + updates := make([]map[string]any, 0, len(h.players)) + for _, p := range h.players { + updates = append(updates, map[string]any{ + "id": p.ID, + "pos": p.Pos, + "rotY": p.RotY, + }) + } + h.mu.RUnlock() + + packet := map[string]any{"type": "state", "updates": updates} + if b, _ := msgpack.Marshal(packet); b != nil { + select { + case h.broadcast <- b: + default: + } + } +} + +// -------------------- WS Handling -------------------- +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + EnableCompression: true, + HandshakeTimeout: 10 * time.Second, + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + return allowedOrigins[origin] + }, +} + +type inbound struct { + Type string `msgpack:"type"` + Name string `msgpack:"name"` + Role Role `msgpack:"role"` + Pos *Vec3 `msgpack:"pos"` + RotY *float64 `msgpack:"rotY"` + Text string `msgpack:"text"` +} + +func wsHandler(h *Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("upgrade:", err) + return + } + id := genID() + c := &Client{ + Conn: conn, + Hub: h, + ID: id, + Send: make(chan []byte, StateBufLimit), + Closed: make(chan struct{}), + connected: time.Now(), + } + h.joinCh <- c + + go writer(c) + reader(c) + } +} + +func reader(c *Client) { + defer func() { + c.Hub.leaveCh <- c + close(c.Closed) + _ = c.Conn.Close() + }() + + c.Conn.SetReadLimit(1 << 20) + _ = c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.Conn.SetPongHandler(func(string) error { + return c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + }) + + // Expect a join first + mt, data, err := c.Conn.ReadMessage() + if err != nil { + return + } + if mt != websocket.BinaryMessage && mt != websocket.TextMessage { + return + } + var first inbound + if msgpack.Unmarshal(data, &first) != nil || first.Type != "join" { + sendPacket(c, map[string]any{"type": "error", "message": "expected join"}) + return + } + name := sanitizeName(first.Name) + if name == "" { + name = randomName() + } + role := first.Role + if role != RoleSpectator { + role = RolePlayer + } + + // Enforce player cap + c.Hub.mu.Lock() + nPlayers := 0 + for _, cl := range c.Hub.clients { + if cl.Role == RolePlayer { + nPlayers++ + } + } + if role == RolePlayer && nPlayers >= MaxPlayers { + c.Hub.mu.Unlock() + sendPacket(c, map[string]any{"type": "full", "maxPlayers": MaxPlayers}) + return + } + c.Name = name + c.Role = role + + // Register player entity + if c.Role == RolePlayer { + spawn := Player{ID: c.ID, Name: c.Name, Pos: Vec3{0, 1.6, 0}, RotY: 0} + c.Hub.players[c.ID] = &spawn + } + + // Welcome snapshot + players := make([]Player, 0, len(c.Hub.players)) + for _, p := range c.Hub.players { + players = append(players, *p) + } + c.Hub.mu.Unlock() + + sendPacket(c, map[string]any{ + "type": "welcome", + "id": c.ID, + "players": players, + "maxPlayers": MaxPlayers, + }) + + if c.Role == RolePlayer { + announce := map[string]any{"type": "player_join", "player": Player{ID: c.ID, Name: c.Name, Pos: Vec3{0, 1.6, 0}, RotY: 0}} + if b, _ := msgpack.Marshal(announce); b != nil { + select { + case c.Hub.broadcast <- b: + default: + } + } + } + + // loop + for { + mt, msg, err := c.Conn.ReadMessage() + if err != nil { + return + } + if mt != websocket.BinaryMessage && mt != websocket.TextMessage { + continue + } + var in inbound + if msgpack.Unmarshal(msg, &in) != nil { + continue + } + switch in.Type { + case "state": + if c.Role != RolePlayer || in.Pos == nil || in.RotY == nil { + continue + } + c.Hub.mu.Lock() + if p, ok := c.Hub.players[c.ID]; ok { + p.Pos = *in.Pos + p.RotY = *in.RotY + } + c.Hub.mu.Unlock() + case "chat": + now := time.Now() + if now.Sub(c.LastChat) < MinChatInterval { + continue + } + c.LastChat = now + text := trimLen(strings.TrimSpace(in.Text), ChatMaxLen) + if text == "" { + continue + } + packet := map[string]any{ + "type": "chat", "id": c.ID, "name": c.Name, "text": text, "ts": now.Unix(), + } + if b, _ := msgpack.Marshal(packet); b != nil { + select { + case c.Hub.broadcast <- b: + default: + } + } + } + } +} + +func writer(c *Client) { + ping := time.NewTicker(20 * time.Second) + defer ping.Stop() + for { + select { + case msg := <-c.Send: + _ = c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.BinaryMessage, msg); err != nil { + return + } + case <-ping.C: + _ = c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + case <-c.Closed: + return + } + } +} + +func sendPacket(c *Client, v any) { + b, _ := msgpack.Marshal(v) + _ = c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + _ = c.Conn.WriteMessage(websocket.BinaryMessage, b) +} + +// -------------------- Utis -------------------- +func genID() string { + var b [8]byte + _, _ = cryptoRand.Read(b[:]) + return "p_" + hex.EncodeToString(b[:]) +} + +func sanitizeName(s string) string { + s = strings.TrimSpace(s) + s = strings.ReplaceAll(s, "\n", " ") + s = strings.ReplaceAll(s, "\r", " ") + if len(s) > NameMaxLen { + s = s[:NameMaxLen] + } + return s +} + +func trimLen(s string, max int) string { + if len(s) > max { + return s[:max] + } + return s +} + +func randomName() string { + adjs := []string{"Blue", "Crimson", "Silent", "Swift", "Lucky", "Rusty", "Cosmic", "Mossy"} + nouns := []string{"Fox", "Raven", "Badger", "Otter", "Falcon", "Lynx", "Marmot", "Golem"} + return adjs[rand.Intn(len(adjs))] + nouns[rand.Intn(len(nouns))] +} + +// -------------------- main -------------------- +func main() { + rand.Seed(time.Now().UnixNano()) + addr := flag.String("addr", AddrHTTP, "http listen addr") + flag.Parse() + + h := NewHub() + go h.Run() + + mux := http.NewServeMux() + mux.HandleFunc("/ws", wsHandler(h)) + + srv := &http.Server{ + Addr: *addr, + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + IdleTimeout: 75 * time.Second, + } + + log.Println("SEPPJM-World WebSocket server listening on", *addr, "path /ws") + if err := srv.ListenAndServe(); err != nil { + log.Fatal(err) + } +} diff --git a/Frontend-WEB/world.seppjm.com/3js/audio/stalker.mp3 b/Frontend-WEB/world.seppjm.com/3js/audio/stalker.mp3 new file mode 100644 index 0000000..ab3434f Binary files /dev/null and b/Frontend-WEB/world.seppjm.com/3js/audio/stalker.mp3 differ diff --git a/Frontend-WEB/world.seppjm.com/3js/background.js b/Frontend-WEB/world.seppjm.com/3js/background.js new file mode 100644 index 0000000..2e3a6c2 --- /dev/null +++ b/Frontend-WEB/world.seppjm.com/3js/background.js @@ -0,0 +1,655 @@ +import * as THREE from "three"; +import { FBXLoader } from "three/addons/loaders/FBXLoader.js"; +import { TGALoader } from "three/addons/loaders/TGALoader.js"; +/* msgpack for server messages */ +import { encode, decode } from "https://esm.sh/@msgpack/msgpack@3.1.2?bundle"; + +(() => { + if (window.__threePS1Campfire) return; + window.__threePS1Campfire = true; + + const MODEL_URL = "https://seppjm.com/3js/models/scene.fbx"; + const NORMALIZE_SCALE = false; + const USE_FOG = true; + + const PS1_SCALE = 3; + const COLOR_LEVELS = 32; + const USE_DITHER = true; + const VERTEX_SNAP_PIXELS = 0.75; + + const FIRE_POS = new THREE.Vector3( + 2885.090654499771, + 5.937671631541306, + -2843.489246932181 + ); + const CAM_POS = new THREE.Vector3( + 3280.98691276581, + 386.84586301208896, + -2012.4527013816644 + ); + const CAM_ROT_DEG = { yaw: 395.51276597880525, pitch: 2.7272791048226543 }; + const CAM_FOV = 90; + + const FIRE_SPRITE_SIZE = 150.0; + const FIRE_HEIGHT_OFFSET = 0.25; + const FIRE_MAIN_INTENSITY = 2.2; + const FIRE_SUB_INTENSITY = 1.0; + const FIRE_MAIN_DISTANCE = 12000; + const FIRE_SUB_DISTANCE = 6000; + const FIRE_DECAY = 1.2; + + /* --- WS spectator (minimal) --- */ + const WS_URL = "wss://ws.world.seppjm.com/ws"; // adjust if you proxy to wss + const REMOTE_SPRITE_HEIGHT = 120; + const REMOTE_SPRITE_ASPECT = 0.55; + const players = new Map(); // id -> {group, target, rotY} + let myID = null; + let ws = null; + + function makeProceduralBillboard() { + const W = 64, H = 128; + const c = document.createElement("canvas"); + c.width = W; c.height = H; + const g = c.getContext("2d"); + g.fillStyle = "rgba(0,0,0,0)"; + g.fillRect(0,0,W,H); + g.fillStyle = "#d6e0ff"; + g.fillRect(18,20,28,78); + g.beginPath(); g.arc(32,30,18,0,Math.PI*2); g.fill(); + g.fillRect(18,98,10,24); g.fillRect(36,98,10,24); + g.strokeStyle = "#131a2f"; g.lineWidth = 3; + g.strokeRect(18,20,28,78); + g.beginPath(); g.arc(32,30,18,0,Math.PI*2); g.stroke(); + const tex = new THREE.CanvasTexture(c); + tex.generateMipmaps = false; + tex.minFilter = THREE.NearestFilter; + tex.magFilter = THREE.NearestFilter; + return tex; + } + function makeBillboardSprite() { + const tex = makeProceduralBillboard(); + const spr = new THREE.Sprite(new THREE.SpriteMaterial({ + map: tex, transparent: true, depthWrite: true, depthTest: true + })); + spr.center.set(0.5, 0.0); // bottom at ground-ish + spr.position.y = 0; + spr.scale.set(REMOTE_SPRITE_HEIGHT * REMOTE_SPRITE_ASPECT, REMOTE_SPRITE_HEIGHT, 1); + return spr; + } + function spawnOrUpdate(p) { + if (myID && p.id === myID) return; + let entry = players.get(p.id); + if (!entry) { + const group = new THREE.Group(); + if (p.pos?.length === 3) group.position.set(p.pos[0], p.pos[1], p.pos[2]); + group.rotation.y = p.rotY || 0; + group.add(makeBillboardSprite()); + scene.add(group); + entry = { group, target: new THREE.Vector3().copy(group.position), rotY: group.rotation.y }; + players.set(p.id, entry); + } else { + if (p.pos?.length === 3) entry.target.set(p.pos[0], p.pos[1], p.pos[2]); + entry.rotY = p.rotY || 0; + } + } + function removePlayer(id) { + const e = players.get(id); + if (!e) return; + scene.remove(e.group); + players.delete(id); + } + function connectSpectator() { + try { + ws = new WebSocket(WS_URL); + ws.binaryType = "arraybuffer"; + ws.addEventListener("open", () => { + const name = `Viewer${Math.floor(Math.random()*9000)+1000}`; + ws.send(encode({ type: "join", name, role: "spectator" })); + }); + ws.addEventListener("message", (ev) => { + const msg = decode(ev.data); + switch (msg.type) { + case "welcome": + myID = msg.id || null; + (msg.players || []).forEach(spawnOrUpdate); + break; + case "player_join": + if (msg.player) spawnOrUpdate(msg.player); + break; + case "player_leave": + if (msg.id) removePlayer(msg.id); + break; + case "state": + if (Array.isArray(msg.updates)) msg.updates.forEach(spawnOrUpdate); + else if (msg.id && msg.pos) spawnOrUpdate(msg); + break; + } + }); + } catch {} + } + /* --- end WS spectator --- */ + + const footer = document.querySelector("footer"); + const canvas = document.createElement("canvas"); + canvas.id = "three-bg"; + Object.assign(canvas.style, { + position: "fixed", + inset: "0", + width: "100vw", + height: "100vh", + display: "block", + zIndex: "0", + pointerEvents: "none", + clipPath: "inset(0 0 var(--bg-clip-bottom, 0px) 0)", + WebkitClipPath: "inset(0 0 var(--bg-clip-bottom, 0px) 0)", + opacity: "0", + transition: "opacity 1400ms ease", + imageRendering: "pixelated", + }); + document.body.prepend(canvas); + + function updateFooterClip() { + const fadeLen = 140; + const rect = footer?.getBoundingClientRect(); + if (!rect) { + canvas.style.setProperty("--bg-clip-bottom", "0px"); + canvas.style.maskImage = "none"; + canvas.style.WebkitMaskImage = "none"; + return; + } + const overlap = Math.max(0, innerHeight - rect.top); + const clip = Math.max(0, overlap - fadeLen); + canvas.style.setProperty("--bg-clip-bottom", `${clip}px`); + if (overlap > 0) { + const grad = `linear-gradient(to bottom, black 0%, black calc(100% - ${fadeLen}px), transparent 100%)`; + canvas.style.maskImage = grad; + canvas.style.WebkitMaskImage = grad; + } else { + canvas.style.maskImage = "none"; + canvas.style.WebkitMaskImage = "none"; + } + } + addEventListener("scroll", updateFooterClip, { passive: true }); + addEventListener("resize", updateFooterClip); + new ResizeObserver(updateFooterClip).observe(document.body); + if (footer) new ResizeObserver(updateFooterClip).observe(footer); + updateFooterClip(); + + const renderer = new THREE.WebGLRenderer({ + canvas, + antialias: false, + alpha: true, + powerPreference: "high-performance", + }); + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.toneMapping = THREE.NoToneMapping; + renderer.shadowMap.enabled = false; + renderer.setPixelRatio(1); + renderer.setClearColor(0x000000, 0); + + let lowW = 0, + lowH = 0, + rt = null; + + const postScene = new THREE.Scene(); + const postCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + const postMat = new THREE.ShaderMaterial({ + uniforms: { + tDiffuse: { value: null }, + uLowRes: { value: new THREE.Vector2(1, 1) }, + uLevels: { value: COLOR_LEVELS }, + uDither: { value: USE_DITHER ? 1 : 0 }, + uTime: { value: 0.0 }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = (position.xy + 1.0) * 0.5; + gl_Position = vec4(position.xy, 0.0, 1.0); + } + `, + fragmentShader: ` + precision mediump float; + uniform sampler2D tDiffuse; + uniform vec2 uLowRes; + uniform int uLevels; + uniform int uDither; + uniform float uTime; + varying vec2 vUv; + + float hash(vec2 p) { + p = fract(p * vec2(123.34, 345.45)); + p += dot(p, p + 34.345); + return fract(p.x * p.y); + } + vec3 quantize(vec3 c, vec2 pix) { + float L = float(uLevels); + float t = (uDither == 1) ? (hash(pix + uTime) - 0.5) : 0.0; + vec3 q = floor(clamp(c + t / L, 0.0, 1.0) * (L - 1.0) + 0.5) / (L - 1.0); + return q; + } + void main() { + vec2 p = vUv * uLowRes; + vec2 uv = (floor(p) + 0.5) / uLowRes; + vec3 col = texture2D(tDiffuse, uv).rgb; + col = quantize(col, p); + gl_FragColor = vec4(col, 1.0); + } + `, + depthTest: false, + depthWrite: false, + }); + postScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), postMat)); + + function resizeRenderer() { + const w = innerWidth, + h = innerHeight; + lowW = Math.max(1, Math.floor(w / PS1_SCALE)); + lowH = Math.max(1, Math.floor(h / PS1_SCALE)); + renderer.setSize(w, h, false); + if (rt) rt.dispose(); + rt = new THREE.WebGLRenderTarget(lowW, lowH, { + minFilter: THREE.NearestFilter, + magFilter: THREE.NearestFilter, + depthBuffer: true, + stencilBuffer: false, + type: THREE.UnsignedByteType, + samples: 0, + }); + postMat.uniforms.uLowRes.value.set(lowW, lowH); + } + resizeRenderer(); + + const scene = new THREE.Scene(); + scene.environment = null; + scene.fog = USE_FOG ? new THREE.FogExp2(0x0b1020, 0.02) : null; + + const camera = new THREE.PerspectiveCamera( + CAM_FOV, + innerWidth / innerHeight, + 0.005, + 5000 + ); + camera.position.copy(CAM_POS); + + const BASE_YAW = THREE.MathUtils.degToRad( + ((CAM_ROT_DEG.yaw % 360) + 360) % 360 + ); + const BASE_PITCH = THREE.MathUtils.degToRad(CAM_ROT_DEG.pitch); + camera.rotation.set(BASE_PITCH, BASE_YAW, 0, "YXZ"); + camera.updateProjectionMatrix(); + + const MAX_YAW_DELTA = THREE.MathUtils.degToRad(6); + const MAX_PITCH_DELTA = THREE.MathUtils.degToRad(4); + let aimYaw = BASE_YAW, + aimPitch = BASE_PITCH; + let curYaw = BASE_YAW, + curPitch = BASE_PITCH; + + addEventListener( + "pointermove", + (e) => { + const nx = (e.clientX / innerWidth) * 2 - 1; + const ny = (e.clientY / innerHeight) * 2 - 1; + aimYaw = BASE_YAW - nx * MAX_YAW_DELTA; + aimPitch = BASE_PITCH + ny * MAX_PITCH_DELTA; + }, + { passive: true } + ); + + function updateCameraHover(dt) { + const smooth = 1.0 - Math.pow(0.2, dt * 60); + curYaw += (aimYaw - curYaw) * smooth; + curPitch += (aimPitch - curPitch) * smooth; + camera.rotation.set(curPitch, curYaw, 0, "YXZ"); + } + + scene.add(new THREE.HemisphereLight(0xb0c8ff, 0x0b1020, 0.28)); + const sun = new THREE.DirectionalLight(0xffffff, 0.6); + sun.position.set(3, 5, 4); + scene.add(sun); + + const basePath = MODEL_URL.slice(0, MODEL_URL.lastIndexOf("/") + 1) || "/"; + const manager = new THREE.LoadingManager(); + manager.addHandler(/\.tga$/i, new TGALoader(manager)); + const loader = new FBXLoader(manager); + loader.setResourcePath(basePath); + + let model, + mixer, + sceneRadius = 100; + const patchedMaterials = new Set(); + const perFrame = []; + + function ps1ifyMaterials(root) { + root.traverse((n) => { + if (!n.isMesh) return; + const src = n.material; + const mats = (Array.isArray(src) ? src : [src]).filter(Boolean); + + const newMats = mats.map((m) => { + const p = { + color: + m.color && m.color.isColor + ? m.color.clone() + : new THREE.Color(0xffffff), + map: m.map || null, + transparent: false, + alphaTest: + (m.alphaTest ?? 0.0) > 0 + ? m.alphaTest + : m.map?.format === THREE.RGBAFormat + ? 0.5 + : 0.0, + side: THREE.DoubleSide, + }; + const lm = new THREE.MeshLambertMaterial(p); + + ["map", "emissiveMap", "aoMap", "specularMap"].forEach((k) => { + const tex = lm[k]; + if (tex) { + tex.generateMipmaps = false; + tex.minFilter = THREE.NearestFilter; + tex.magFilter = THREE.NearestFilter; + tex.anisotropy = 0; + tex.needsUpdate = true; + } + }); + + lm.depthWrite = true; + lm.depthTest = true; + lm.dithering = false; + lm.polygonOffset = true; + lm.polygonOffsetFactor = -0.5; + lm.polygonOffsetUnits = 1.0; + + lm.onBeforeCompile = (shader) => { + shader.uniforms.uResolution = { + value: new THREE.Vector2(innerWidth, innerHeight), + }; + shader.uniforms.uSnapPixels = { value: VERTEX_SNAP_PIXELS }; + shader.vertexShader = shader.vertexShader.replace( + /void\s+main\s*\(\)\s*\{/, + "uniform vec2 uResolution;\nuniform float uSnapPixels;\nvoid main(){" + ); + if (VERTEX_SNAP_PIXELS > 0) { + shader.vertexShader = shader.vertexShader.replace( + "#include ", + ` + #include + vec2 ndc = gl_Position.xy / gl_Position.w; + vec2 pix = (ndc * 0.5 + 0.5) * uResolution; + pix = floor(pix / uSnapPixels) * uSnapPixels; + vec2 ndc2 = (pix / uResolution) * 2.0 - 1.0; + gl_Position.xy = ndc2 * gl_Position.w; + ` + ); + } + lm.userData._ps1Shader = shader; + patchedMaterials.add(lm); + }; + + return lm; + }); + + n.material = Array.isArray(src) ? newMats : newMats[0]; + }); + } + + function autoFogFor(target) { + if (!USE_FOG) return; + const box = new THREE.Box3().setFromObject(target); + const size = box.getSize(new THREE.Vector3()); + sceneRadius = Math.max(size.x, size.y, size.z) * 0.5 || 100; + const k = 0.03; + const density = Math.min(0.06, Math.max(0.000005, k / (sceneRadius || 1))); + scene.fog = new THREE.FogExp2(0x0b1020, density); + } + + loader.load( + MODEL_URL, + (fbx) => { + model = fbx; + if (NORMALIZE_SCALE) { + const box = new THREE.Box3().setFromObject(model); + const size = box.getSize(new THREE.Vector3()); + const maxDim = Math.max(size.x, size.y, size.z) || 1; + const scale = 20.0 / maxDim; + model.scale.setScalar(scale); + const center = box.getCenter(new THREE.Vector3()).multiplyScalar(scale); + model.position.sub(center); + } + ps1ifyMaterials(model); + scene.add(model); + + if (fbx.animations?.length) { + mixer = new THREE.AnimationMixer(model); + mixer.clipAction(fbx.animations[0]).play(); + } + + autoFogFor(model); + addCampfire(FIRE_POS); + }, + undefined, + () => { + addCampfire(FIRE_POS); + } + ); + + let fireGroup = null, + fireLight = null, + fireLight2 = null, + fireSprite = null; + + function makeFlameTexture(size = 128) { + const c = document.createElement("canvas"); + c.width = c.height = size; + const g = c.getContext("2d"); + const grd = g.createRadialGradient( + size * 0.5, + size * 0.6, + size * 0.05, + size * 0.5, + size * 0.6, + size * 0.5 + ); + grd.addColorStop(0.0, "rgba(255,255,255,1)"); + grd.addColorStop(0.25, "rgba(255,200,80,0.95)"); + grd.addColorStop(0.55, "rgba(255,120,30,0.65)"); + grd.addColorStop(0.85, "rgba(200,40,10,0.25)"); + grd.addColorStop(1.0, "rgba(0,0,0,0)"); + g.fillStyle = grd; + g.fillRect(0, 0, size, size); + const tex = new THREE.CanvasTexture(c); + tex.generateMipmaps = false; + tex.minFilter = THREE.NearestFilter; + tex.magFilter = THREE.NearestFilter; + tex.anisotropy = 0; + return tex; + } + + function addCampfire(pos) { + fireGroup = new THREE.Group(); + fireGroup.position.copy(pos); + scene.add(fireGroup); + + fireLight = new THREE.PointLight( + 0xff7a2a, + FIRE_MAIN_INTENSITY, + FIRE_MAIN_DISTANCE, + FIRE_DECAY + ); + fireLight.position.set(0, FIRE_HEIGHT_OFFSET + 0.6, 0); + fireGroup.add(fireLight); + + fireLight2 = new THREE.PointLight( + 0xff3300, + FIRE_SUB_INTENSITY, + FIRE_SUB_DISTANCE, + FIRE_DECAY + ); + fireLight2.position.set(0.35, FIRE_HEIGHT_OFFSET + 0.4, -0.2); + fireGroup.add(fireLight2); + + const flameTex = makeFlameTexture(128); + const mat = new THREE.SpriteMaterial({ + map: flameTex, + color: 0xffffff, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + depthTest: true, + }); + fireSprite = new THREE.Sprite(mat); + fireSprite.position.set(0, FIRE_HEIGHT_OFFSET + 0.25, 0); + fireSprite.scale.set(FIRE_SPRITE_SIZE, FIRE_SPRITE_SIZE * 1.35, 1); + fireGroup.add(fireSprite); + + const base1 = FIRE_MAIN_INTENSITY, + base2 = FIRE_SUB_INTENSITY; + let t = 0; + perFrame.push((dt) => { + t += dt; + const pulse = 0.45 * Math.sin(t * 5.4) + 0.28 * Math.sin(t * 8.7 + 1.1); + fireLight.intensity = base1 + pulse; + fireLight2.intensity = base2 + pulse * 0.75; + fireLight.color.setHSL(0.06 + Math.sin(t * 1.6) * 0.01, 1.0, 0.55); + fireLight2.color.setHSL( + 0.03 + Math.sin(t * 1.9 + 0.8) * 0.012, + 1.0, + 0.47 + ); + fireSprite.material.rotation += dt * 0.5; + const j = 1.0 + Math.sin(t * 6.0) * 0.06; + fireSprite.scale.set( + FIRE_SPRITE_SIZE * j, + FIRE_SPRITE_SIZE * 1.35 * j, + 1 + ); + }); + } + + function animate() { + if (!active) return; + const dt = clock.getDelta(); + if (mixer) mixer.update(dt); + updateCameraHover(dt); + perFrame.forEach((fn) => fn(dt)); + + /* smooth remote players */ + players.forEach(({ group, target, rotY }) => { + const k = 1 - Math.pow(0.0001, dt * 60); + group.position.lerp(target, k); + group.rotation.y += (rotY - group.rotation.y) * k; + }); + + renderer.setRenderTarget(rt); + postMat.uniforms.tDiffuse.value = null; + renderer.clear(); + renderer.render(scene, camera); + + renderer.setRenderTarget(null); + postMat.uniforms.tDiffuse.value = rt.texture; + postMat.uniforms.uTime.value += dt; + renderer.render(postScene, postCam); + + requestAnimationFrame(animate); + } + + const clock = new THREE.Clock(); + let active = true; + addEventListener("visibilitychange", () => { + active = document.visibilityState === "visible"; + if (active) requestAnimationFrame(animate); + }); + const fadeIn = () => requestAnimationFrame(() => (canvas.style.opacity = "1")); + requestAnimationFrame(() => { + animate(); + requestAnimationFrame(fadeIn); + }); + + addEventListener("resize", () => { + camera.aspect = innerWidth / innerHeight; + camera.updateProjectionMatrix(); + resizeRenderer(); + updateFooterClip(); + patchedMaterials.forEach((m) => { + const s = m.userData._ps1Shader; + if (s) s.uniforms.uResolution.value.set(innerWidth, innerHeight); + }); + }); + + const audio = new Audio("https://seppjm.com/3js/audio/stalker.mp3"); + audio.loop = true; + audio.preload = "auto"; + audio.crossOrigin = "anonymous"; + audio.volume = 0.5; + + const btn = document.createElement("button"); + btn.type = "button"; + btn.textContent = "Play music"; + btn.setAttribute("aria-pressed", "false"); + btn.ariaLabel = "Toggle background music"; + Object.assign(btn.style, { + position: "fixed", + right: "16px", + bottom: "16px", + zIndex: "3", + padding: "10px 14px", + borderRadius: "9999px", + border: "1px solid rgba(255,255,255,0.25)", + background: "rgba(0,0,0,0.45)", + color: "white", + font: + "500 14px/1.1 system-ui, -apple-system, Segoe UI, Roboto, Inter, sans-serif", + backdropFilter: "blur(6px)", + WebkitBackdropFilter: "blur(6px)", + cursor: "pointer", + userSelect: "none", + }); + document.body.appendChild(btn); + + let isPlaying = false, + fadeRAF = null; + function fadeTo(target, ms = 700) { + cancelAnimationFrame(fadeRAF); + const start = audio.volume, + delta = target - start, + t0 = performance.now(); + const tick = (now) => { + const p = Math.min(1, (now - t0) / ms); + audio.volume = Math.max(0, Math.min(1, start + delta * p)); + if (p < 1) fadeRAF = requestAnimationFrame(tick); + }; + fadeRAF = requestAnimationFrame(tick); + } + async function playAudio() { + try { + await audio.play(); + isPlaying = true; + btn.textContent = "Pause music"; + btn.setAttribute("aria-pressed", "true"); + fadeTo(0.5, 600); + } catch {} + } + function pauseAudio() { + fadeTo(0.0, 400); + setTimeout(() => audio.pause(), 420); + isPlaying = false; + btn.textContent = "Play music"; + btn.setAttribute("aria-pressed", "false"); + } + btn.addEventListener("click", () => + isPlaying ? pauseAudio() : playAudio() + ); + const startOnGesture = () => { + if (!isPlaying) playAudio(); + removeEventListener("pointerdown", startOnGesture); + removeEventListener("keydown", startOnGesture); + }; + addEventListener("pointerdown", startOnGesture, { once: true }); + addEventListener("keydown", startOnGesture, { once: true }); + + /* start spectator connection */ + connectSpectator(); +})(); diff --git a/Frontend-WEB/world.seppjm.com/3js/models/scene.fbx b/Frontend-WEB/world.seppjm.com/3js/models/scene.fbx new file mode 100644 index 0000000..99ae89c Binary files /dev/null and b/Frontend-WEB/world.seppjm.com/3js/models/scene.fbx differ diff --git a/Frontend-WEB/world.seppjm.com/3js/world.js b/Frontend-WEB/world.seppjm.com/3js/world.js new file mode 100644 index 0000000..92ace8a --- /dev/null +++ b/Frontend-WEB/world.seppjm.com/3js/world.js @@ -0,0 +1,682 @@ +import * as THREE from "three"; +import { FBXLoader } from "three/addons/loaders/FBXLoader.js"; +import { TGALoader } from "three/addons/loaders/TGALoader.js"; +import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; +import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js"; +import { MeshBVH, acceleratedRaycast } from "https://esm.sh/three-mesh-bvh@0.7.6?deps=three@0.160.0"; +import { encode, decode } from "https://esm.sh/@msgpack/msgpack@3.1.2?bundle"; + +/* status overlay */ +const statusEl = document.createElement("div"); +Object.assign(statusEl.style, { position:"fixed", left:"12px", top:"12px", zIndex:"99999", padding:"6px 10px", borderRadius:"8px", background:"rgba(0,0,0,.55)", color:"#fff", font:"12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Inter,sans-serif", whiteSpace:"pre", pointerEvents:"none", maxWidth:"45vw" }); +statusEl.textContent = "booting…"; +document.body.appendChild(statusEl); +const setStatus = (t)=> (statusEl.textContent = t); + +/* config */ +const MODEL_URL = "https://world.seppjm.com/3js/models/scene.fbx"; +const USE_FOG = true; +let RENDER_SCALE = 3; +const COLOR_LEVELS = 32, USE_DITHER = true, VERTEX_SNAP_PIXELS = 0.75; +const FIRE_POS = new THREE.Vector3(2885.090654499771, 5.937671631541306, -2843.489246932181); +const CAM_POS = new THREE.Vector3(3280.98691276581, 386.84586301208896, -2012.4527013816644); +const CAM_ROT_DEG = { yaw: 395.51276597880525, pitch: 2.7272791048226543 }; +const CAM_FOV = 90; + +/* fire */ +const FIRE_SPRITE_SIZE = 150.0, FIRE_HEIGHT_OFFSET = 0.25; +const FIRE_MAIN_INTENSITY = 2.2, FIRE_SUB_INTENSITY = 1.0, FIRE_MAIN_DISTANCE = 12000, FIRE_SUB_DISTANCE = 6000, FIRE_DECAY = 1.2; + +/* networking */ +const WS_URL = "wss://ws.world.seppjm.com/ws"; + +/* scale / movement */ +const SCALE = 50; +const WALK_SPEED = 4.6 * SCALE, ACCEL = 30.0, AIR_ACCEL = 7.0, FRICTION = 9.0, GRAVITY = 27.0 * SCALE; +const JUMP_STRENGTH = 11.0 * SCALE, COYOTE_TIME = 0.12; + +/* spectator freecam */ +const FREECAM_SPEED = 6.0 * SCALE; + +/* slopes / capsule */ +const WALL_NORMAL_Y = 0.58; +const CAPSULE_RADIUS = 0.5 * SCALE, CAPSULE_HEIGHT = 1.2 * SCALE, PLAYER_EYE_HEIGHT = CAPSULE_HEIGHT * 0.9 + CAPSULE_RADIUS; + +/* spawn */ +const SPAWN_OFFSET = new THREE.Vector3(2.8 * SCALE, 0, 2.2 * SCALE), SPAWN_EXTRA_Y = 50 * SCALE; + +/* collision tuning */ +const CONTACT_OFFSET = 0.08 * SCALE, MAX_SUBSTEP_DIST = 0.25 * CAPSULE_RADIUS; + +/* ground + wall */ +const SNAP_CAST_DIST = 3.0 * CAPSULE_RADIUS, SNAP_EPS = 0.15 * SCALE; +const WALL_MARGIN = 0.16 * SCALE, WALL_ITER = 3, RAY_FAN_COUNT = 16; + +/* net cadence */ +const SEND_RATE_MS = 1000 / 12; + +/* remote visuals */ +const SPRITE_HEIGHT = Math.max(1.6 * SCALE, PLAYER_EYE_HEIGHT * 1.05); +const SPRITE_ASPECT = 0.55; +const NAME_PAD = 0.24 * SCALE; +const PLAYER_SPRITE_URL = null; + +/* renderer + post */ +const canvas = document.createElement("canvas"); +canvas.id = "three-bg"; +Object.assign(canvas.style, { + position:"fixed", + inset:"0", + width:"100vw", + height:"100vh", + display:"block", + zIndex:"-1", + pointerEvents:"none", + opacity:"0", + transition:"opacity 600ms ease", + imageRendering:"pixelated", + background:"transparent" +}); +document.body.prepend(canvas); + +const renderer = new THREE.WebGLRenderer({ canvas, antialias:false, alpha:true, powerPreference:"high-performance" }); +renderer.outputColorSpace = THREE.SRGBColorSpace; +renderer.toneMapping = THREE.NoToneMapping; +renderer.shadowMap.enabled = false; +renderer.setPixelRatio(1); +renderer.setClearColor(0x000000, 0); + +/* 2D labels layer */ +const labelsLayer = document.createElement("div"); +Object.assign(labelsLayer.style, { + position:"fixed", + inset:"0", + pointerEvents:"none", + zIndex:"-1", + fontFamily:"Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" +}); +document.body.appendChild(labelsLayer); + +let lowW=0, lowH=0, rt=null; +const postScene = new THREE.Scene(); +const postCam = new THREE.OrthographicCamera(-1,1,1,-1,0,1); +const postMat = new THREE.ShaderMaterial({ + uniforms:{ tDiffuse:{value:null}, uLowRes:{value:new THREE.Vector2(1,1)}, uLevels:{value:COLOR_LEVELS}, uDither:{value:USE_DITHER?1:0}, uTime:{value:0} }, + vertexShader:`varying vec2 vUv; void main(){ vUv=(position.xy+1.)*.5; gl_Position=vec4(position.xy,0.,1.); }`, + fragmentShader:` + precision mediump float; uniform sampler2D tDiffuse; uniform vec2 uLowRes; uniform int uLevels,uDither; uniform float uTime; varying vec2 vUv; + float hash(vec2 p){ p=fract(p*vec2(123.34,345.45)); p+=dot(p,p+34.345); return fract(p.x*p.y); } + vec3 quantize(vec3 c, vec2 pix){ float L=float(uLevels); float t=(uDither==1)?(hash(pix+uTime)-.5):0.; return floor(clamp(c+t/L,0.,1.)*(L-1.)+.5)/(L-1.); } + void main(){ vec2 p=vUv*uLowRes; vec2 uv=(floor(p)+.5)/uLowRes; vec3 col=texture2D(tDiffuse,uv).rgb; col=quantize(col,p); gl_FragColor=vec4(col,1.); } + `, + depthTest:false, depthWrite:false +}); +postScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2,2), postMat)); +function allocRT(){ const w=innerWidth,h=innerHeight; lowW=Math.max(1,Math.floor(w/RENDER_SCALE)); lowH=Math.max(1,Math.floor(h/RENDER_SCALE)); renderer.setSize(w,h,false); if(rt) rt.dispose(); rt=new THREE.WebGLRenderTarget(lowW,lowH,{minFilter:THREE.NearestFilter,magFilter:THREE.NearestFilter,depthBuffer:true,stencilBuffer:false,type:THREE.UnsignedByteType,samples:0}); postMat.uniforms.uLowRes.value.set(lowW,lowH); } +allocRT(); + +/* scene + idle cam */ +const scene = new THREE.Scene(); +scene.environment = null; +scene.fog = USE_FOG ? new THREE.FogExp2(0x0b1020, 0.02) : null; +const camera = new THREE.PerspectiveCamera(CAM_FOV, innerWidth/innerHeight, 0.005, 5000); +const BASE_YAW = THREE.MathUtils.degToRad(((CAM_ROT_DEG.yaw%360)+360)%360); +const BASE_PITCH = THREE.MathUtils.degToRad(CAM_ROT_DEG.pitch); +camera.position.copy(CAM_POS); +camera.rotation.set(BASE_PITCH, BASE_YAW, 0, "YXZ"); +camera.updateProjectionMatrix(); +const MAX_YAW_DELTA = THREE.MathUtils.degToRad(6), MAX_PITCH_DELTA = THREE.MathUtils.degToRad(4); +let aimYaw=BASE_YAW, aimPitch=BASE_PITCH, curYaw=BASE_YAW, curPitch=BASE_PITCH, playMode=false; +addEventListener("pointermove",(e)=>{ if(playMode || spectateMode) return; const nx=(e.clientX/innerWidth)*2-1, ny=(e.clientY/innerHeight)*2-1; aimYaw=BASE_YAW - nx*MAX_YAW_DELTA; aimPitch=BASE_PITCH + ny*MAX_PITCH_DELTA; },{passive:true}); +function updateCameraHover(dt){ const s=1.0 - Math.pow(0.2, dt*60); curYaw += (aimYaw-curYaw)*s; curPitch += (aimPitch-curPitch)*s; camera.rotation.set(curPitch,curYaw,0,"YXZ"); } +scene.add(new THREE.HemisphereLight(0xb0c8ff,0x0b1020,0.28)); const sun=new THREE.DirectionalLight(0xffffff,0.6); sun.position.set(3,5,4); scene.add(sun); + +/* loader + bvh */ +THREE.Mesh.prototype.raycast = acceleratedRaycast; +const basePath = MODEL_URL.slice(0, MODEL_URL.lastIndexOf("/") + 1) || "/"; +const manager = new THREE.LoadingManager(); manager.addHandler(/\.tga$/i, new TGALoader(manager)); +const loader = new FBXLoader(manager); loader.setResourcePath(basePath); +let model, mixer, levelReady=false, colliderGeom=null, colliderBVH=null, colliderMesh=null, colliderMinY=-Infinity, colliderMaxY=Infinity; +const patchedMaterials = new Set(); const perFrame = []; + +/* shader uniform cache for resize */ +const ps1Uniforms = new WeakMap(); + +/* patch material for PS1 look */ +function ps1ifyLambert(mat){ + ["map","emissiveMap","aoMap","specularMap"].forEach((k)=>{ const t=mat[k]; if(!t) return; t.generateMipmaps=false; t.minFilter=THREE.NearestFilter; t.magFilter=THREE.NearestFilter; t.anisotropy=0; t.needsUpdate=true; }); + mat.depthWrite=true; mat.depthTest=true; mat.dithering=false; mat.polygonOffset=true; mat.polygonOffsetFactor=-0.5; mat.polygonOffsetUnits=1.0; + mat.onBeforeCompile=(shader)=>{ + shader.uniforms.uResolution = { value:new THREE.Vector2(innerWidth,innerHeight) }; + shader.uniforms.uSnapPixels = { value:VERTEX_SNAP_PIXELS }; + if(!/uniform\s+vec2\s+uResolution/.test(shader.vertexShader)){ + shader.vertexShader = `uniform vec2 uResolution;\nuniform float uSnapPixels;\n` + shader.vertexShader; + } + shader.vertexShader = shader.vertexShader.replace( + '#include ', + ` + #include + vec2 ndc = gl_Position.xy / gl_Position.w; + vec2 pix = (ndc*0.5 + 0.5) * uResolution; + pix = floor(pix / uSnapPixels) * uSnapPixels; + vec2 ndc2 = (pix / uResolution) * 2.0 - 1.0; + gl_Position.xy = ndc2 * gl_Position.w; + ` + ); + ps1Uniforms.set(mat, shader.uniforms); + patchedMaterials.add(mat); + }; + return mat; +} + +/* convert imported model to PS1 style and build collider */ +function ps1ifyMaterialsAndBuildCollider(root){ + const geos=[]; root.updateWorldMatrix(true,true); + root.traverse((n)=>{ + if(!n.isMesh||!n.geometry) return; + const src=n.material; const mats=(Array.isArray(src)?src:[src]).filter(Boolean); + const newMats=mats.map((m)=>ps1ifyLambert(new THREE.MeshLambertMaterial({ + color:(m.color&&m.color.isColor)?m.color.clone():new THREE.Color(0xffffff), + map:m.map||null, transparent:false, alphaTest: (m.alphaTest??0)>0?m.alphaTest: (m.map?.format===THREE.RGBAFormat?0.5:0.0), side:THREE.DoubleSide + }))); + n.material = Array.isArray(src)?newMats:newMats[0]; + const srcGeo=n.geometry.clone(); srcGeo.applyMatrix4(n.matrixWorld); const pos=srcGeo.getAttribute("position"); if(!pos) return; + const clean=new THREE.BufferGeometry(); clean.setAttribute("position",pos.clone()); clean.setIndex(null); geos.push(clean); + }); + let merged=null; + try{ merged=BufferGeometryUtils.mergeGeometries(geos,false); } + catch{ const chunks=[]; for(let i=0;i{ t+=dt; const pulse=0.45*Math.sin(t*5.4)+0.28*Math.sin(t*8.7+1.1); + fireLight.intensity=FIRE_MAIN_INTENSITY + pulse; fireLight2.intensity=FIRE_SUB_INTENSITY + pulse*0.75; + fireLight.color.setHSL(0.06+Math.sin(t*1.6)*0.01,1.0,0.55); fireLight2.color.setHSL(0.03+Math.sin(t*1.9+0.8)*0.012,1.0,0.47); + fireSprite.material.rotation += dt*0.5; const j=1.0+Math.sin(t*6.0)*0.06; fireSprite.scale.set(FIRE_SPRITE_SIZE*j, FIRE_SPRITE_SIZE*1.35*j, 1); + }); +} + +/* load world */ +loader.load(MODEL_URL, (fbx)=>{ + model=fbx; scene.add(model); ps1ifyMaterialsAndBuildCollider(model); + if(fbx.animations?.length){ mixer=new THREE.AnimationMixer(model); mixer.clipAction(fbx.animations[0]).play(); } + autoFogFor(model); addCampfire(FIRE_POS); + levelReady=true; setStatus("world loaded"); + if(playMode) initialPlace(true); +}, undefined, ()=> setStatus("ERR loading model")); + +/* Music: autoplay + 'M' toggle */ +const audio=new Audio("https://world.seppjm.com/3js/audio/stalker.mp3"); +audio.loop=true; audio.preload="auto"; audio.crossOrigin="anonymous"; audio.volume=0.5; +let isPlaying=false, fadeRAF=null; +const fadeTo=(target,ms=700)=>{ cancelAnimationFrame(fadeRAF); const start=audio.volume,d=target-start,t0=performance.now(); const tick=(now)=>{ const p=Math.min(1,(now-t0)/ms); audio.volume=Math.max(0,Math.min(1,start+d*p)); if(p<1) fadeRAF=requestAnimationFrame(tick); }; fadeRAF=requestAnimationFrame(tick); }; +async function playMusic(auto=false){ try{ if(auto) audio.volume=0.0; await audio.play(); isPlaying=true; if(auto) fadeTo(0.5,800); }catch{} } +function pauseMusic(){ fadeTo(0.0,300); setTimeout(()=>audio.pause(),320); isPlaying=false; } +playMusic(true); +["pointerdown","keydown","visibilitychange"].forEach((ev)=> addEventListener(ev, ()=>{ if(!isPlaying) playMusic(true); }, { once:true })); + +/* fps controller + collisions (player) */ +let controls=null; const keys=new Set(); const velocity=new THREE.Vector3(); let onGround=false, physicsEnabled=false; +const lastSafePos=new THREE.Vector3(); let timeSinceGrounded=0, lastGroundedAt=0; +let playerLight=null; function ensurePlayerLight(obj){ if(playerLight) return; playerLight=new THREE.PointLight(0xffa25a,0.9,1500*SCALE,1.4); playerLight.position.set(0,-PLAYER_EYE_HEIGHT*0.2,0); obj.add(playerLight); } + +/* spectator freecam */ +let spectateMode=false, specControls=null; +function onSpecKeyDown(e){ + keys.add(e.code); + if(e.code==="KeyM"){ if(isPlaying) pauseMusic(); else playMusic(); } +} +function onSpecKeyUp(e){ keys.delete(e.code); } +function makeSpectateHint(){ + const hint=document.createElement("div"); + Object.assign(hint.style,{ position:"fixed", left:"16px", bottom:"16px", zIndex:20, color:"#fff", font:"600 14px Inter, system-ui", opacity:"0.9", pointerEvents:"none", textAlign:"left", lineHeight:"1.35", background:"rgba(0,0,0,0.45)", padding:"8px 10px", borderRadius:"10px", maxWidth:"44ch" }); + hint.textContent="Spectator freecam — WASD move • Mouse look • Q/E down/up • Shift boost • M music • ESC cursor"; + document.body.appendChild(hint); +} +function startSpectate(){ + if(spectateMode || playMode) return; + spectateMode=true; + + canvas.style.zIndex = "10"; + canvas.style.pointerEvents = "auto"; + canvas.style.opacity = "1"; + labelsLayer.style.zIndex = "15"; + + specControls = new PointerLockControls(camera, canvas); + scene.add(specControls.getObject()); + specControls.lock(); + canvas.addEventListener("click", ()=> specControls.lock()); + specControls.addEventListener("lock", ()=> setStatus("spectate: locked (WASD, mouse, Q/E, Shift, M)")); + specControls.addEventListener("unlock", ()=> setStatus("spectate: unlocked (click to lock)")); + + addEventListener("keydown", onSpecKeyDown); + addEventListener("keyup", onSpecKeyUp); + makeSpectateHint(); +} +function stopSpectate(){ + if(!spectateMode) return; + removeEventListener("keydown", onSpecKeyDown); + removeEventListener("keyup", onSpecKeyUp); + if(specControls){ + try{ specControls.unlock(); }catch{} + scene.remove(specControls.getObject()); + } + specControls=null; + spectateMode=false; +} +function updateFreecam(dt){ + if(!spectateMode) return; + const boost = (keys.has("ShiftLeft") || keys.has("ShiftRight")) ? 3.0 : 1.0; + const speed = FREECAM_SPEED * boost; + const fwd=new THREE.Vector3(); camera.getWorldDirection(fwd); fwd.normalize(); + const right=new THREE.Vector3().crossVectors(fwd,new THREE.Vector3(0,1,0)).normalize(); + const up=new THREE.Vector3(0,1,0); + const move=new THREE.Vector3(); + if(keys.has("KeyW")) move.add(fwd); + if(keys.has("KeyS")) move.addScaledVector(fwd,-1); + if(keys.has("KeyA")) move.addScaledVector(right,-1); + if(keys.has("KeyD")) move.add(right); + if(keys.has("KeyE")) move.add(up); + if(keys.has("KeyQ")) move.addScaledVector(up,-1); + if(move.lengthSq()>0){ + move.normalize().multiplyScalar(speed*dt); + camera.position.add(move); + } +} + +/* rays */ +const dirFan=Array.from({length:RAY_FAN_COUNT},(_,i)=>{ const a=(i/RAY_FAN_COUNT)*Math.PI*2; return new THREE.Vector3(Math.sin(a),0,Math.cos(a)).normalize(); }); +const raycaster=new THREE.Raycaster(); raycaster.firstHitOnly=true; + +/* helpers */ +const feetYFromEye=(eyeY)=> eyeY - (PLAYER_EYE_HEIGHT - CAPSULE_RADIUS); +function groundSnap(eyePos, maxUp=SNAP_EPS){ + if(!colliderMesh) return false; + const feetY=feetYFromEye(eyePos.y), origin=new THREE.Vector3(eyePos.x, feetY+CAPSULE_RADIUS*0.75, eyePos.z), dir=new THREE.Vector3(0,-1,0); + raycaster.set(origin,dir); raycaster.near=0; raycaster.far=SNAP_CAST_DIST; + const hit=raycaster.intersectObject(colliderMesh,false)[0]; if(!hit) return false; + const n=hit.face?.normal ?? new THREE.Vector3(0,1,0); if(n.y < WALL_NORMAL_Y) return false; + const desiredFeet=hit.point.y + CAPSULE_RADIUS + CONTACT_OFFSET, delta=desiredFeet - feetY; + if(delta>-maxUp && delta=WALL_NORMAL_Y) continue; + const push=radius - hit.distance + CONTACT_OFFSET; if(push>0) eyePos.addScaledVector(d,-push); + } + } + } +} +function sweepClampMove(eyePos, moveXZ){ + if(!colliderMesh) return moveXZ; const len=moveXZ.length(); if(len<1e-6) return moveXZ; + const dir=moveXZ.clone().normalize(), feet=feetYFromEye(eyePos.y); + const heights=[ feet+CAPSULE_RADIUS*0.2, feet+CAPSULE_HEIGHT*0.5, feet+CAPSULE_HEIGHT-CAPSULE_RADIUS*0.2 ]; + const radius=CAPSULE_RADIUS + WALL_MARGIN; let maxLen=len; + for(const h of heights){ + const origin=new THREE.Vector3(eyePos.x,h,eyePos.z); raycaster.set(origin,dir); raycaster.near=0; raycaster.far=len+radius; + const hit=raycaster.intersectObject(colliderMesh,false)[0]; if(!hit) continue; + const n=hit.face?.normal ?? new THREE.Vector3(0,1,0); if(n.y>=WALL_NORMAL_Y) continue; + const allowed=Math.max(0, hit.distance - (radius+CONTACT_OFFSET)); if(allowedcolliderMaxY+200*SCALE){ eye.y=colliderMaxY+200*SCALE; velocity.y=Math.min(0,velocity.y); } +} +function updateFPS(dt){ + if(!playMode || !levelReady || !physicsEnabled) return; + const obj=controls.getObject(); + const fwd=new THREE.Vector3(); camera.getWorldDirection(fwd); fwd.y=0; fwd.normalize(); + const right=new THREE.Vector3().crossVectors(fwd,new THREE.Vector3(0,1,0)).normalize(); + const wish=new THREE.Vector3(); if(keys.has("KeyW")) wish.add(fwd); if(keys.has("KeyS")) wish.addScaledVector(fwd,-1); if(keys.has("KeyA")) wish.addScaledVector(right,-1); if(keys.has("KeyD")) wish.add(right); if(wish.lengthSq()>0) wish.normalize(); + const accel=onGround?ACCEL:AIR_ACCEL, targetVel=wish.multiplyScalar(WALK_SPEED), horizVel=new THREE.Vector3(velocity.x,0,velocity.z), add=new THREE.Vector3().subVectors(targetVel,horizVel).multiplyScalar(accel*dt); + horizVel.add(add); velocity.x=horizVel.x; velocity.z=horizVel.z; + velocity.y -= GRAVITY*dt; + + const moveLen=velocity.clone().multiplyScalar(dt).length(), steps=Math.max(1, Math.ceil(moveLen / MAX_SUBSTEP_DIST)); onGround=false; const subDt=dt/steps; + for(let i=0;i4.0){ respawnAtFire(); } + ensurePlayerLight(obj); +} + +/* remote players + 2D name labels */ +const players=new Map(); +function makeLabelEl(text){ + const el=document.createElement("div"); + el.textContent=text; + Object.assign(el.style,{ + position:"absolute", + transform:"translate(-50%,-100%)", + color:"#fff", + fontWeight:"800", + fontSize:"22px", + lineHeight:"1", + padding:"4px 8px", + borderRadius:"8px", + background:"rgba(0,0,0,0.55)", + textShadow:"0 1px 2px rgba(0,0,0,0.65)", + whiteSpace:"nowrap", + pointerEvents:"none", + willChange:"transform", + }); + labelsLayer.appendChild(el); + return el; +} +function updateLabelPosition(label, worldPos){ + const v = worldPos.clone().project(camera); + if (v.z < 0 || v.z > 1) { label.style.display="none"; return; } + const x = (v.x * 0.5 + 0.5) * innerWidth; + const y = ( -v.y * 0.5 + 0.5) * innerHeight; + label.style.display=""; + label.style.left = `${x}px`; + label.style.top = `${y}px`; +} + +function makeBillboardTexture(cb){ + if(PLAYER_SPRITE_URL){ + const img=new Image(); img.crossOrigin="anonymous"; + img.onload=()=>{ const tex=new THREE.Texture(img); tex.needsUpdate=true; tex.generateMipmaps=false; tex.minFilter=THREE.NearestFilter; tex.magFilter=THREE.NearestFilter; cb(tex); }; + img.onerror=()=>cb(makeProceduralBillboard()); img.src=PLAYER_SPRITE_URL; + } else cb(makeProceduralBillboard()); +} +function makeProceduralBillboard(){ + const W=64,H=128,c=document.createElement("canvas"); c.width=W; c.height=H; const g=c.getContext("2d"); g.fillStyle="rgba(0,0,0,0)"; g.fillRect(0,0,W,H); + g.fillStyle="#d6e0ff"; g.fillRect(18,20,28,78); g.beginPath(); g.arc(32,30,18,0,Math.PI*2); g.fill(); g.fillRect(18,98,10,24); g.fillRect(36,98,10,24); + g.strokeStyle="#131a2f"; g.lineWidth=3; g.strokeRect(18,20,28,78); g.beginPath(); g.arc(32,30,18,0,Math.PI*2); g.stroke(); + const tex=new THREE.CanvasTexture(c); tex.generateMipmaps=false; tex.minFilter=THREE.NearestFilter; tex.magFilter=THREE.NearestFilter; return tex; +} +function makeBillboardSprite(onReady){ + makeBillboardTexture((tex)=>{ + const spr=new THREE.Sprite(new THREE.SpriteMaterial({ map:tex, transparent:true, depthWrite:true, depthTest:true })); + spr.center.set(0.5, 0.0); // bottom at ground + spr.position.y = 0; + spr.scale.set(SPRITE_HEIGHT*SPRITE_ASPECT, SPRITE_HEIGHT, 1); + onReady(spr); + }); +} + +/* spawn/update remote, anchor to ground (pos is eye from server) */ +function spawnOrUpdate(p){ + if (myID && p?.id === myID) return; // ignore self + + const baseY = (p.pos?.[1] ?? 0) - PLAYER_EYE_HEIGHT; + let entry = players.get(p.id); + if(!entry){ + const group=new THREE.Group(); group.position.set(p.pos?.[0]??0, baseY, p.pos?.[2]??0); group.rotation.y=p.rotY||0; + makeBillboardSprite((body)=>{ group.add(body); }); + scene.add(group); + const labelEl = makeLabelEl(p.name || "Player"); + entry = { group, labelEl, target:new THREE.Vector3(p.pos?.[0]??0, baseY, p.pos?.[2]??0), rotY:p.rotY||0, name:p.name||"Player" }; + players.set(p.id, entry); + } else { + entry.target.set(p.pos?.[0]??0, baseY, p.pos?.[2]??0); + entry.rotY = p.rotY || 0; + if (p.name && p.name !== entry.name) { entry.name = p.name; entry.labelEl.textContent = p.name; } + } +} +function removePlayer(id){ + const e=players.get(id); if(!e) return; + scene.remove(e.group); + if(e.labelEl?.parentNode) e.labelEl.parentNode.removeChild(e.labelEl); + players.delete(id); +} + +/* chat UI (only when playing) */ +function makeChatUI(){ + const hint=document.createElement("div"); + Object.assign(hint.style,{ position:"fixed", left:"16px", bottom:"16px", zIndex:20, color:"#fff", font:"600 14px Inter, system-ui", opacity:"0.9", pointerEvents:"none", textAlign:"left", lineHeight:"1.35", background:"rgba(0,0,0,0.45)", padding:"8px 10px", borderRadius:"10px", maxWidth:"44ch" }); + hint.innerHTML=`Pointer-lock FPS
WASD move • SPACE jump • T chat • M music • ESC cursor`; + document.body.appendChild(hint); + + const log=document.createElement("div"); Object.assign(log.style,{ position:"fixed", left:"16px", bottom:"96px", width:"420px", maxHeight:"40vh", overflow:"hidden", display:"flex", flexDirection:"column-reverse", gap:"6px", zIndex:20, font:"15px/1.35 Inter, system-ui", color:"#fff" }); log.id="chat-log"; document.body.appendChild(log); + + const form=document.createElement("form"); form.id="chat-form"; Object.assign(form.style,{ position:"fixed", left:"16px", bottom:"16px", zIndex:21, display:"none" }); + const inp=document.createElement("input"); inp.type="text"; inp.placeholder="type to chat…"; Object.assign(inp.style,{ width:"400px", padding:"10px 12px", borderRadius:"10px", border:"1px solid rgba(255,255,255,.2)", background:"rgba(0,0,0,.5)", color:"#fff" }); form.appendChild(inp); document.body.appendChild(form); + + form.addEventListener("submit",(e)=>{ e.preventDefault(); const t=inp.value.trim(); if(!t) return; send({type:"chat", text:t}); inp.value=""; hideChat(); }); + function showChat(){ form.style.display="block"; inp.focus(); controls.unlock(); } + function hideChat(){ form.style.display="none"; controls.lock(); } + addEventListener("keydown",(e)=>{ if(e.key==="t"||e.key==="T"){ e.preventDefault(); showChat(); }}); + + addEventListener("keydown",(e)=>{ + if(e.code==="KeyM"){ + if(isPlaying) pauseMusic(); else playMusic(); + setStatus(isPlaying ? "music: playing" : "music: paused"); + } + }); +} +function addChat(logEl, name, text){ if(!logEl) return; const row=document.createElement("div"); row.textContent=`${name}: ${text}`; row.style.background="rgba(0,0,0,.45)"; row.style.padding="6px 10px"; row.style.borderRadius="8px"; logEl.prepend(row); const kids=[...logEl.children]; while(kids.length>24){ logEl.removeChild(kids.pop()); } } + +/* networking */ +let ws=null, myID=null, lastSend=0, currentRole="spectator", suppressToast=false; +function connect(role="spectator", name="Viewer"){ + try{ + if(ws && ws.readyState<=1){ suppressToast=true; try{ ws.close(); }catch{} } + currentRole=role; + ws=new WebSocket(WS_URL); ws.binaryType="arraybuffer"; + ws.addEventListener("open", ()=>{ + setStatus(`ws: connected (${role})`); + send({ type:"join", name, role }); + if(role==="player"){ const p=controls?.getObject?.().position; if(p) send({type:"state", pos:[p.x,p.y,p.z], rotY:camera.rotation.y}); } + }); + ws.addEventListener("message",(ev)=>{ try{ onMessage(ev); }catch(e){ setStatus("ws msg err: "+e.message); } }); + ws.addEventListener("close", ()=>{ + if(!suppressToast){ const toast=document.createElement("div"); Object.assign(toast.style,{ position:"fixed", left:"50%", top:"14px", transform:"translateX(-50%)", background:"rgba(200,0,0,0.75)", color:"#fff", padding:"8px 12px", borderRadius:"10px", zIndex:50, font:"600 14px Inter, system-ui" }); toast.textContent="Disconnected from server"; document.body.appendChild(toast); setTimeout(()=>toast.remove(),3500); } + suppressToast=false; setStatus("ws: disconnected"); + }); + ws.addEventListener("error", ()=> setStatus("ws: error")); + }catch(e){ setStatus("ws init err: "+e.message); } +} +function send(obj){ if(ws && ws.readyState===1) ws.send(encode(obj)); } +function onMessage(ev){ + const msg=decode(ev.data); + switch(msg.type){ + case "welcome": + myID = msg.id; + removePlayer(myID); + (msg.players||[]).forEach((p)=>{ if(p.id!==myID) spawnOrUpdate(p); }); + break; + case "player_join": + if(msg.player && msg.player.id !== myID) spawnOrUpdate(msg.player); + break; + case "player_leave": + if(msg.id) removePlayer(msg.id); + break; + case "state": + if(Array.isArray(msg.updates)) msg.updates.forEach((u)=>{ if(u.id!==myID) spawnOrUpdate(u); }); + else if(msg.id && msg.pos){ if(msg.id!==myID) spawnOrUpdate(msg); } + break; + case "player_state": + if(msg.id && msg.pos && msg.id!==myID) spawnOrUpdate(msg); + break; + case "chat": + addChat(document.getElementById("chat-log"), msg.name||"Player", msg.text||""); + break; + case "full": + alert(`World is full (max ${msg.maxPlayers ?? 10})`); + break; + } +} + +/* enter play */ +function startPlay(){ + if(playMode) return; + // if we were spectating, clean it up + if(spectateMode) stopSpectate(); + + playMode=true; + canvas.style.zIndex = "10"; + canvas.style.pointerEvents = "auto"; + canvas.style.opacity = "1"; + labelsLayer.style.zIndex = "15"; + + const controlsLocal=new PointerLockControls(camera, canvas); controls=controlsLocal; scene.add(controls.getObject()); + controls.lock(); canvas.addEventListener("click", ()=>controls.lock()); + controls.addEventListener("lock", ()=> setStatus("locked (WASD, SPACE jump, T chat, M music)")); + controls.addEventListener("unlock", ()=> setStatus("unlocked (click to lock)")); + makeChatUI(); + if(levelReady) initialPlace(true); + const name = (prompt("Authentication not implemented; for now enter a nickname:", "Guest") || "Guest").slice(0,25); + connect("player", name); + addEventListener("keydown",(e)=>{ keys.add(e.code); + if(e.code==="Space" && playMode){ + const now=performance.now(); + if(onGround || (now - lastGroundedAt) < COYOTE_TIME*1000){ + velocity.y = JUMP_STRENGTH; + onGround=false; + } + } + }); + addEventListener("keyup",(e)=> keys.delete(e.code)); +} + +/* join hooks — match your exact buttons and href */ +(function hookJoin(){ + const candidates=[ + '#join-btn', '#spectate-btn', + 'a[href="#Join"]','a[href="#join"]', + 'a.button[href="#About"]','#join','[data-join]','.join','button.join' + ]; + for(const sel of candidates){ + const el=document.querySelector(sel); + if(el){ + el.addEventListener("click",(e)=>{ + e.preventDefault(); + if(el.id === "spectate-btn"){ + startSpectate(); + setStatus("Spectating…"); + }else{ + startPlay(); + } + }); + } + } + addEventListener("keydown",(e)=>{ if(e.code==="Enter" && !playMode) startPlay(); }); +})(); + +/* spectator connect on boot */ +const defaultSpectatorName = `Viewer${Math.floor((Math.random()*9000)+1000)}`; +connect("spectator", defaultSpectatorName); + +/* loop */ +const clock=new THREE.Clock(); let active=true; +addEventListener("visibilitychange",()=>{ active=document.visibilityState==="visible"; if(active) requestAnimationFrame(animate); }); +requestAnimationFrame(()=>{ animate(); canvas.style.opacity="1"; }); + +let fpsAccum=0,fpsCount=0,scaleCooldown=0; +function animate(){ + if(!active) return; + const dt=Math.min(0.05, clock.getDelta()); const now=performance.now(); + if(mixer) mixer.update(dt); + if(playMode) updateFPS(dt); + if(spectateMode) updateFreecam(dt); + + // smooth remote players and update 2D labels + players.forEach(({group,target,rotY,labelEl})=>{ + const k=1 - Math.pow(0.0001, dt*60); + group.position.lerp(target,k); + group.rotation.y += (rotY - group.rotation.y)*k; + + const headWorld = new THREE.Vector3( + group.position.x, + group.position.y + SPRITE_HEIGHT + NAME_PAD, + group.position.z + ); + updateLabelPosition(labelEl, headWorld); + }); + + perFrame.forEach((fn)=>fn(dt)); + + renderer.setRenderTarget(rt); postMat.uniforms.tDiffuse.value=null; renderer.clear(); renderer.render(scene,camera); renderer.setRenderTarget(null); + postMat.uniforms.tDiffuse.value=rt.texture; postMat.uniforms.uTime.value += dt; renderer.render(postScene,postCam); + + fpsAccum += dt; fpsCount++; + if(now - scaleCooldown > 1000){ + const avg=fpsAccum/Math.max(1,fpsCount); + if(avg>0.03 && RENDER_SCALE<6){ RENDER_SCALE+=0.5; allocRT(); } + else if(avg<0.017 && RENDER_SCALE>2){ RENDER_SCALE-=0.5; allocRT(); } + fpsAccum=0; fpsCount=0; scaleCooldown=now; + } + + if(currentRole==="player" && ws && ws.readyState===1 && now - lastSend >= SEND_RATE_MS){ + lastSend=now; const p=controls.getObject().position; send({type:"state", pos:[p.x,p.y,p.z], rotY:camera.rotation.y}); + } + + if(!playMode && !spectateMode) updateCameraHover(dt); + requestAnimationFrame(animate); +} + +/* resize */ +addEventListener("resize",()=>{ + camera.aspect=innerWidth/innerHeight; + camera.updateProjectionMatrix(); + allocRT(); + patchedMaterials.forEach((m)=>{ + const u = ps1Uniforms.get(m); + if(u?.uResolution?.value){ u.uResolution.value.set(innerWidth, innerHeight); } + }); +}); diff --git a/Frontend-WEB/world.seppjm.com/index.html b/Frontend-WEB/world.seppjm.com/index.html new file mode 100644 index 0000000..82eddca --- /dev/null +++ b/Frontend-WEB/world.seppjm.com/index.html @@ -0,0 +1,68 @@ + + + + + + SEPPJM.COM | Join World + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Join World

+

Let's start

+
+
+ Join + Spectate +
+
+ Source Code + Download Client + Start in DiodeMatrix DEVBOX +
+
+ Back to SEPPJM.COM +

+

---


+

Special thanks & credits to McDuck for the PSX Scene and SENKO for the song.

+
+
+
+ + + + + + + + + + + diff --git a/World-SEPPJM-Client/.gitignore b/World-SEPPJM-Client/.gitignore new file mode 100644 index 0000000..129d522 --- /dev/null +++ b/World-SEPPJM-Client/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/World-SEPPJM-Client/README.md b/World-SEPPJM-Client/README.md new file mode 100644 index 0000000..eefcd5c --- /dev/null +++ b/World-SEPPJM-Client/README.md @@ -0,0 +1,16 @@ +# README + +## About + +This is the official Wails Svelte template. + +## Live Development + +To run in live development mode, run `wails dev` in the project directory. This will run a Vite development +server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser +and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect +to this in your browser, and you can call your Go code from devtools. + +## Building + +To build a redistributable, production mode package, use `wails build`. diff --git a/World-SEPPJM-Client/app.go b/World-SEPPJM-Client/app.go new file mode 100644 index 0000000..af53038 --- /dev/null +++ b/World-SEPPJM-Client/app.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called when the app starts. The context is saved +// so we can call the runtime methods +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// Greet returns a greeting for the given name +func (a *App) Greet(name string) string { + return fmt.Sprintf("Hello %s, It's show time!", name) +} diff --git a/World-SEPPJM-Client/build/README.md b/World-SEPPJM-Client/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/World-SEPPJM-Client/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/World-SEPPJM-Client/build/appicon.png b/World-SEPPJM-Client/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/World-SEPPJM-Client/build/appicon.png differ diff --git a/World-SEPPJM-Client/build/darwin/Info.dev.plist b/World-SEPPJM-Client/build/darwin/Info.dev.plist new file mode 100644 index 0000000..14121ef --- /dev/null +++ b/World-SEPPJM-Client/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/World-SEPPJM-Client/build/darwin/Info.plist b/World-SEPPJM-Client/build/darwin/Info.plist new file mode 100644 index 0000000..d17a747 --- /dev/null +++ b/World-SEPPJM-Client/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/World-SEPPJM-Client/build/windows/icon.ico b/World-SEPPJM-Client/build/windows/icon.ico new file mode 100644 index 0000000..f334798 Binary files /dev/null and b/World-SEPPJM-Client/build/windows/icon.ico differ diff --git a/World-SEPPJM-Client/build/windows/info.json b/World-SEPPJM-Client/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/World-SEPPJM-Client/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/World-SEPPJM-Client/build/windows/installer/project.nsi b/World-SEPPJM-Client/build/windows/installer/project.nsi new file mode 100644 index 0000000..654ae2e --- /dev/null +++ b/World-SEPPJM-Client/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/World-SEPPJM-Client/build/windows/installer/wails_tools.nsh b/World-SEPPJM-Client/build/windows/installer/wails_tools.nsh new file mode 100644 index 0000000..2f6d321 --- /dev/null +++ b/World-SEPPJM-Client/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/World-SEPPJM-Client/build/windows/wails.exe.manifest b/World-SEPPJM-Client/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/World-SEPPJM-Client/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/World-SEPPJM-Client/frontend/.vscode/extensions.json b/World-SEPPJM-Client/frontend/.vscode/extensions.json new file mode 100644 index 0000000..b869ef8 --- /dev/null +++ b/World-SEPPJM-Client/frontend/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "svelte.svelte-vscode" + ] +} diff --git a/World-SEPPJM-Client/frontend/README.md b/World-SEPPJM-Client/frontend/README.md new file mode 100644 index 0000000..a346289 --- /dev/null +++ b/World-SEPPJM-Client/frontend/README.md @@ -0,0 +1,63 @@ +# Svelte + Vite + +This template should help get you started developing with Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + ++ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its +serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, +and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer +experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` +templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been +structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash +references keeps the default TypeScript setting of accepting type information from the entire workspace, while also +adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to +install the recommended extension upon opening the project. + +**Why enable `checkJs` in the JS template?** + +It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. +This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of +JavaScript, it is trivial to change the configuration. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` +and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the +details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be +replaced by HMR. + +```js +// store.js +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/World-SEPPJM-Client/frontend/index.html b/World-SEPPJM-Client/frontend/index.html new file mode 100644 index 0000000..63a81ec --- /dev/null +++ b/World-SEPPJM-Client/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + World-SEPPJM-Client + + +
+ + + diff --git a/World-SEPPJM-Client/frontend/jsconfig.json b/World-SEPPJM-Client/frontend/jsconfig.json new file mode 100644 index 0000000..3918b4f --- /dev/null +++ b/World-SEPPJM-Client/frontend/jsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": [ + "src/**/*.d.ts", + "src/**/*.js", + "src/**/*.svelte" + ] +} diff --git a/World-SEPPJM-Client/frontend/package-lock.json b/World-SEPPJM-Client/frontend/package-lock.json new file mode 100644 index 0000000..e4b486e --- /dev/null +++ b/World-SEPPJM-Client/frontend/package-lock.json @@ -0,0 +1,781 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz", + "integrity": "sha512-6QupI/jemMfK+yI2pMtJcu5iO2gtgTfcBdGwMZZt+lgbFELhszbDl6Qjh000HgAV8+XUA+8EY8DusOFk8WhOIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "deepmerge": "^4.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.26.7", + "svelte-hmr": "^0.15.1", + "vitefu": "^0.2.2" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.44.0", + "vite": "^3.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "3.59.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz", + "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + } + } +} diff --git a/World-SEPPJM-Client/frontend/package.json b/World-SEPPJM-Client/frontend/package.json new file mode 100644 index 0000000..8c9ae62 --- /dev/null +++ b/World-SEPPJM-Client/frontend/package.json @@ -0,0 +1,16 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/World-SEPPJM-Client/frontend/package.json.md5 b/World-SEPPJM-Client/frontend/package.json.md5 new file mode 100644 index 0000000..0a2df21 --- /dev/null +++ b/World-SEPPJM-Client/frontend/package.json.md5 @@ -0,0 +1 @@ +d9dc84f0d17ed164f36dd584057aae68 \ No newline at end of file diff --git a/World-SEPPJM-Client/frontend/src/App.svelte b/World-SEPPJM-Client/frontend/src/App.svelte new file mode 100644 index 0000000..bf6b153 --- /dev/null +++ b/World-SEPPJM-Client/frontend/src/App.svelte @@ -0,0 +1,109 @@ + + + + + + + + + +{#if view === 'menu'} +
+
+
+

World.SEPPJM Client

+

Join the online world or play offline

+ + +
+
+
+ +{:else if view === 'offline'} +
+
+
+

Offline Mode not available

+

Offline mode not implemented yet. Press Esc or use the button in the top-right to return.

+ + + +
+
+
+{/if} + +{#if view !== 'menu'} + +{/if} + + diff --git a/World-SEPPJM-Client/frontend/src/assets/fonts/OFL.txt b/World-SEPPJM-Client/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/World-SEPPJM-Client/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/World-SEPPJM-Client/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/World-SEPPJM-Client/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/World-SEPPJM-Client/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/World-SEPPJM-Client/frontend/src/assets/js/world.js b/World-SEPPJM-Client/frontend/src/assets/js/world.js new file mode 100644 index 0000000..92ace8a --- /dev/null +++ b/World-SEPPJM-Client/frontend/src/assets/js/world.js @@ -0,0 +1,682 @@ +import * as THREE from "three"; +import { FBXLoader } from "three/addons/loaders/FBXLoader.js"; +import { TGALoader } from "three/addons/loaders/TGALoader.js"; +import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; +import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js"; +import { MeshBVH, acceleratedRaycast } from "https://esm.sh/three-mesh-bvh@0.7.6?deps=three@0.160.0"; +import { encode, decode } from "https://esm.sh/@msgpack/msgpack@3.1.2?bundle"; + +/* status overlay */ +const statusEl = document.createElement("div"); +Object.assign(statusEl.style, { position:"fixed", left:"12px", top:"12px", zIndex:"99999", padding:"6px 10px", borderRadius:"8px", background:"rgba(0,0,0,.55)", color:"#fff", font:"12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Inter,sans-serif", whiteSpace:"pre", pointerEvents:"none", maxWidth:"45vw" }); +statusEl.textContent = "booting…"; +document.body.appendChild(statusEl); +const setStatus = (t)=> (statusEl.textContent = t); + +/* config */ +const MODEL_URL = "https://world.seppjm.com/3js/models/scene.fbx"; +const USE_FOG = true; +let RENDER_SCALE = 3; +const COLOR_LEVELS = 32, USE_DITHER = true, VERTEX_SNAP_PIXELS = 0.75; +const FIRE_POS = new THREE.Vector3(2885.090654499771, 5.937671631541306, -2843.489246932181); +const CAM_POS = new THREE.Vector3(3280.98691276581, 386.84586301208896, -2012.4527013816644); +const CAM_ROT_DEG = { yaw: 395.51276597880525, pitch: 2.7272791048226543 }; +const CAM_FOV = 90; + +/* fire */ +const FIRE_SPRITE_SIZE = 150.0, FIRE_HEIGHT_OFFSET = 0.25; +const FIRE_MAIN_INTENSITY = 2.2, FIRE_SUB_INTENSITY = 1.0, FIRE_MAIN_DISTANCE = 12000, FIRE_SUB_DISTANCE = 6000, FIRE_DECAY = 1.2; + +/* networking */ +const WS_URL = "wss://ws.world.seppjm.com/ws"; + +/* scale / movement */ +const SCALE = 50; +const WALK_SPEED = 4.6 * SCALE, ACCEL = 30.0, AIR_ACCEL = 7.0, FRICTION = 9.0, GRAVITY = 27.0 * SCALE; +const JUMP_STRENGTH = 11.0 * SCALE, COYOTE_TIME = 0.12; + +/* spectator freecam */ +const FREECAM_SPEED = 6.0 * SCALE; + +/* slopes / capsule */ +const WALL_NORMAL_Y = 0.58; +const CAPSULE_RADIUS = 0.5 * SCALE, CAPSULE_HEIGHT = 1.2 * SCALE, PLAYER_EYE_HEIGHT = CAPSULE_HEIGHT * 0.9 + CAPSULE_RADIUS; + +/* spawn */ +const SPAWN_OFFSET = new THREE.Vector3(2.8 * SCALE, 0, 2.2 * SCALE), SPAWN_EXTRA_Y = 50 * SCALE; + +/* collision tuning */ +const CONTACT_OFFSET = 0.08 * SCALE, MAX_SUBSTEP_DIST = 0.25 * CAPSULE_RADIUS; + +/* ground + wall */ +const SNAP_CAST_DIST = 3.0 * CAPSULE_RADIUS, SNAP_EPS = 0.15 * SCALE; +const WALL_MARGIN = 0.16 * SCALE, WALL_ITER = 3, RAY_FAN_COUNT = 16; + +/* net cadence */ +const SEND_RATE_MS = 1000 / 12; + +/* remote visuals */ +const SPRITE_HEIGHT = Math.max(1.6 * SCALE, PLAYER_EYE_HEIGHT * 1.05); +const SPRITE_ASPECT = 0.55; +const NAME_PAD = 0.24 * SCALE; +const PLAYER_SPRITE_URL = null; + +/* renderer + post */ +const canvas = document.createElement("canvas"); +canvas.id = "three-bg"; +Object.assign(canvas.style, { + position:"fixed", + inset:"0", + width:"100vw", + height:"100vh", + display:"block", + zIndex:"-1", + pointerEvents:"none", + opacity:"0", + transition:"opacity 600ms ease", + imageRendering:"pixelated", + background:"transparent" +}); +document.body.prepend(canvas); + +const renderer = new THREE.WebGLRenderer({ canvas, antialias:false, alpha:true, powerPreference:"high-performance" }); +renderer.outputColorSpace = THREE.SRGBColorSpace; +renderer.toneMapping = THREE.NoToneMapping; +renderer.shadowMap.enabled = false; +renderer.setPixelRatio(1); +renderer.setClearColor(0x000000, 0); + +/* 2D labels layer */ +const labelsLayer = document.createElement("div"); +Object.assign(labelsLayer.style, { + position:"fixed", + inset:"0", + pointerEvents:"none", + zIndex:"-1", + fontFamily:"Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" +}); +document.body.appendChild(labelsLayer); + +let lowW=0, lowH=0, rt=null; +const postScene = new THREE.Scene(); +const postCam = new THREE.OrthographicCamera(-1,1,1,-1,0,1); +const postMat = new THREE.ShaderMaterial({ + uniforms:{ tDiffuse:{value:null}, uLowRes:{value:new THREE.Vector2(1,1)}, uLevels:{value:COLOR_LEVELS}, uDither:{value:USE_DITHER?1:0}, uTime:{value:0} }, + vertexShader:`varying vec2 vUv; void main(){ vUv=(position.xy+1.)*.5; gl_Position=vec4(position.xy,0.,1.); }`, + fragmentShader:` + precision mediump float; uniform sampler2D tDiffuse; uniform vec2 uLowRes; uniform int uLevels,uDither; uniform float uTime; varying vec2 vUv; + float hash(vec2 p){ p=fract(p*vec2(123.34,345.45)); p+=dot(p,p+34.345); return fract(p.x*p.y); } + vec3 quantize(vec3 c, vec2 pix){ float L=float(uLevels); float t=(uDither==1)?(hash(pix+uTime)-.5):0.; return floor(clamp(c+t/L,0.,1.)*(L-1.)+.5)/(L-1.); } + void main(){ vec2 p=vUv*uLowRes; vec2 uv=(floor(p)+.5)/uLowRes; vec3 col=texture2D(tDiffuse,uv).rgb; col=quantize(col,p); gl_FragColor=vec4(col,1.); } + `, + depthTest:false, depthWrite:false +}); +postScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2,2), postMat)); +function allocRT(){ const w=innerWidth,h=innerHeight; lowW=Math.max(1,Math.floor(w/RENDER_SCALE)); lowH=Math.max(1,Math.floor(h/RENDER_SCALE)); renderer.setSize(w,h,false); if(rt) rt.dispose(); rt=new THREE.WebGLRenderTarget(lowW,lowH,{minFilter:THREE.NearestFilter,magFilter:THREE.NearestFilter,depthBuffer:true,stencilBuffer:false,type:THREE.UnsignedByteType,samples:0}); postMat.uniforms.uLowRes.value.set(lowW,lowH); } +allocRT(); + +/* scene + idle cam */ +const scene = new THREE.Scene(); +scene.environment = null; +scene.fog = USE_FOG ? new THREE.FogExp2(0x0b1020, 0.02) : null; +const camera = new THREE.PerspectiveCamera(CAM_FOV, innerWidth/innerHeight, 0.005, 5000); +const BASE_YAW = THREE.MathUtils.degToRad(((CAM_ROT_DEG.yaw%360)+360)%360); +const BASE_PITCH = THREE.MathUtils.degToRad(CAM_ROT_DEG.pitch); +camera.position.copy(CAM_POS); +camera.rotation.set(BASE_PITCH, BASE_YAW, 0, "YXZ"); +camera.updateProjectionMatrix(); +const MAX_YAW_DELTA = THREE.MathUtils.degToRad(6), MAX_PITCH_DELTA = THREE.MathUtils.degToRad(4); +let aimYaw=BASE_YAW, aimPitch=BASE_PITCH, curYaw=BASE_YAW, curPitch=BASE_PITCH, playMode=false; +addEventListener("pointermove",(e)=>{ if(playMode || spectateMode) return; const nx=(e.clientX/innerWidth)*2-1, ny=(e.clientY/innerHeight)*2-1; aimYaw=BASE_YAW - nx*MAX_YAW_DELTA; aimPitch=BASE_PITCH + ny*MAX_PITCH_DELTA; },{passive:true}); +function updateCameraHover(dt){ const s=1.0 - Math.pow(0.2, dt*60); curYaw += (aimYaw-curYaw)*s; curPitch += (aimPitch-curPitch)*s; camera.rotation.set(curPitch,curYaw,0,"YXZ"); } +scene.add(new THREE.HemisphereLight(0xb0c8ff,0x0b1020,0.28)); const sun=new THREE.DirectionalLight(0xffffff,0.6); sun.position.set(3,5,4); scene.add(sun); + +/* loader + bvh */ +THREE.Mesh.prototype.raycast = acceleratedRaycast; +const basePath = MODEL_URL.slice(0, MODEL_URL.lastIndexOf("/") + 1) || "/"; +const manager = new THREE.LoadingManager(); manager.addHandler(/\.tga$/i, new TGALoader(manager)); +const loader = new FBXLoader(manager); loader.setResourcePath(basePath); +let model, mixer, levelReady=false, colliderGeom=null, colliderBVH=null, colliderMesh=null, colliderMinY=-Infinity, colliderMaxY=Infinity; +const patchedMaterials = new Set(); const perFrame = []; + +/* shader uniform cache for resize */ +const ps1Uniforms = new WeakMap(); + +/* patch material for PS1 look */ +function ps1ifyLambert(mat){ + ["map","emissiveMap","aoMap","specularMap"].forEach((k)=>{ const t=mat[k]; if(!t) return; t.generateMipmaps=false; t.minFilter=THREE.NearestFilter; t.magFilter=THREE.NearestFilter; t.anisotropy=0; t.needsUpdate=true; }); + mat.depthWrite=true; mat.depthTest=true; mat.dithering=false; mat.polygonOffset=true; mat.polygonOffsetFactor=-0.5; mat.polygonOffsetUnits=1.0; + mat.onBeforeCompile=(shader)=>{ + shader.uniforms.uResolution = { value:new THREE.Vector2(innerWidth,innerHeight) }; + shader.uniforms.uSnapPixels = { value:VERTEX_SNAP_PIXELS }; + if(!/uniform\s+vec2\s+uResolution/.test(shader.vertexShader)){ + shader.vertexShader = `uniform vec2 uResolution;\nuniform float uSnapPixels;\n` + shader.vertexShader; + } + shader.vertexShader = shader.vertexShader.replace( + '#include ', + ` + #include + vec2 ndc = gl_Position.xy / gl_Position.w; + vec2 pix = (ndc*0.5 + 0.5) * uResolution; + pix = floor(pix / uSnapPixels) * uSnapPixels; + vec2 ndc2 = (pix / uResolution) * 2.0 - 1.0; + gl_Position.xy = ndc2 * gl_Position.w; + ` + ); + ps1Uniforms.set(mat, shader.uniforms); + patchedMaterials.add(mat); + }; + return mat; +} + +/* convert imported model to PS1 style and build collider */ +function ps1ifyMaterialsAndBuildCollider(root){ + const geos=[]; root.updateWorldMatrix(true,true); + root.traverse((n)=>{ + if(!n.isMesh||!n.geometry) return; + const src=n.material; const mats=(Array.isArray(src)?src:[src]).filter(Boolean); + const newMats=mats.map((m)=>ps1ifyLambert(new THREE.MeshLambertMaterial({ + color:(m.color&&m.color.isColor)?m.color.clone():new THREE.Color(0xffffff), + map:m.map||null, transparent:false, alphaTest: (m.alphaTest??0)>0?m.alphaTest: (m.map?.format===THREE.RGBAFormat?0.5:0.0), side:THREE.DoubleSide + }))); + n.material = Array.isArray(src)?newMats:newMats[0]; + const srcGeo=n.geometry.clone(); srcGeo.applyMatrix4(n.matrixWorld); const pos=srcGeo.getAttribute("position"); if(!pos) return; + const clean=new THREE.BufferGeometry(); clean.setAttribute("position",pos.clone()); clean.setIndex(null); geos.push(clean); + }); + let merged=null; + try{ merged=BufferGeometryUtils.mergeGeometries(geos,false); } + catch{ const chunks=[]; for(let i=0;i{ t+=dt; const pulse=0.45*Math.sin(t*5.4)+0.28*Math.sin(t*8.7+1.1); + fireLight.intensity=FIRE_MAIN_INTENSITY + pulse; fireLight2.intensity=FIRE_SUB_INTENSITY + pulse*0.75; + fireLight.color.setHSL(0.06+Math.sin(t*1.6)*0.01,1.0,0.55); fireLight2.color.setHSL(0.03+Math.sin(t*1.9+0.8)*0.012,1.0,0.47); + fireSprite.material.rotation += dt*0.5; const j=1.0+Math.sin(t*6.0)*0.06; fireSprite.scale.set(FIRE_SPRITE_SIZE*j, FIRE_SPRITE_SIZE*1.35*j, 1); + }); +} + +/* load world */ +loader.load(MODEL_URL, (fbx)=>{ + model=fbx; scene.add(model); ps1ifyMaterialsAndBuildCollider(model); + if(fbx.animations?.length){ mixer=new THREE.AnimationMixer(model); mixer.clipAction(fbx.animations[0]).play(); } + autoFogFor(model); addCampfire(FIRE_POS); + levelReady=true; setStatus("world loaded"); + if(playMode) initialPlace(true); +}, undefined, ()=> setStatus("ERR loading model")); + +/* Music: autoplay + 'M' toggle */ +const audio=new Audio("https://world.seppjm.com/3js/audio/stalker.mp3"); +audio.loop=true; audio.preload="auto"; audio.crossOrigin="anonymous"; audio.volume=0.5; +let isPlaying=false, fadeRAF=null; +const fadeTo=(target,ms=700)=>{ cancelAnimationFrame(fadeRAF); const start=audio.volume,d=target-start,t0=performance.now(); const tick=(now)=>{ const p=Math.min(1,(now-t0)/ms); audio.volume=Math.max(0,Math.min(1,start+d*p)); if(p<1) fadeRAF=requestAnimationFrame(tick); }; fadeRAF=requestAnimationFrame(tick); }; +async function playMusic(auto=false){ try{ if(auto) audio.volume=0.0; await audio.play(); isPlaying=true; if(auto) fadeTo(0.5,800); }catch{} } +function pauseMusic(){ fadeTo(0.0,300); setTimeout(()=>audio.pause(),320); isPlaying=false; } +playMusic(true); +["pointerdown","keydown","visibilitychange"].forEach((ev)=> addEventListener(ev, ()=>{ if(!isPlaying) playMusic(true); }, { once:true })); + +/* fps controller + collisions (player) */ +let controls=null; const keys=new Set(); const velocity=new THREE.Vector3(); let onGround=false, physicsEnabled=false; +const lastSafePos=new THREE.Vector3(); let timeSinceGrounded=0, lastGroundedAt=0; +let playerLight=null; function ensurePlayerLight(obj){ if(playerLight) return; playerLight=new THREE.PointLight(0xffa25a,0.9,1500*SCALE,1.4); playerLight.position.set(0,-PLAYER_EYE_HEIGHT*0.2,0); obj.add(playerLight); } + +/* spectator freecam */ +let spectateMode=false, specControls=null; +function onSpecKeyDown(e){ + keys.add(e.code); + if(e.code==="KeyM"){ if(isPlaying) pauseMusic(); else playMusic(); } +} +function onSpecKeyUp(e){ keys.delete(e.code); } +function makeSpectateHint(){ + const hint=document.createElement("div"); + Object.assign(hint.style,{ position:"fixed", left:"16px", bottom:"16px", zIndex:20, color:"#fff", font:"600 14px Inter, system-ui", opacity:"0.9", pointerEvents:"none", textAlign:"left", lineHeight:"1.35", background:"rgba(0,0,0,0.45)", padding:"8px 10px", borderRadius:"10px", maxWidth:"44ch" }); + hint.textContent="Spectator freecam — WASD move • Mouse look • Q/E down/up • Shift boost • M music • ESC cursor"; + document.body.appendChild(hint); +} +function startSpectate(){ + if(spectateMode || playMode) return; + spectateMode=true; + + canvas.style.zIndex = "10"; + canvas.style.pointerEvents = "auto"; + canvas.style.opacity = "1"; + labelsLayer.style.zIndex = "15"; + + specControls = new PointerLockControls(camera, canvas); + scene.add(specControls.getObject()); + specControls.lock(); + canvas.addEventListener("click", ()=> specControls.lock()); + specControls.addEventListener("lock", ()=> setStatus("spectate: locked (WASD, mouse, Q/E, Shift, M)")); + specControls.addEventListener("unlock", ()=> setStatus("spectate: unlocked (click to lock)")); + + addEventListener("keydown", onSpecKeyDown); + addEventListener("keyup", onSpecKeyUp); + makeSpectateHint(); +} +function stopSpectate(){ + if(!spectateMode) return; + removeEventListener("keydown", onSpecKeyDown); + removeEventListener("keyup", onSpecKeyUp); + if(specControls){ + try{ specControls.unlock(); }catch{} + scene.remove(specControls.getObject()); + } + specControls=null; + spectateMode=false; +} +function updateFreecam(dt){ + if(!spectateMode) return; + const boost = (keys.has("ShiftLeft") || keys.has("ShiftRight")) ? 3.0 : 1.0; + const speed = FREECAM_SPEED * boost; + const fwd=new THREE.Vector3(); camera.getWorldDirection(fwd); fwd.normalize(); + const right=new THREE.Vector3().crossVectors(fwd,new THREE.Vector3(0,1,0)).normalize(); + const up=new THREE.Vector3(0,1,0); + const move=new THREE.Vector3(); + if(keys.has("KeyW")) move.add(fwd); + if(keys.has("KeyS")) move.addScaledVector(fwd,-1); + if(keys.has("KeyA")) move.addScaledVector(right,-1); + if(keys.has("KeyD")) move.add(right); + if(keys.has("KeyE")) move.add(up); + if(keys.has("KeyQ")) move.addScaledVector(up,-1); + if(move.lengthSq()>0){ + move.normalize().multiplyScalar(speed*dt); + camera.position.add(move); + } +} + +/* rays */ +const dirFan=Array.from({length:RAY_FAN_COUNT},(_,i)=>{ const a=(i/RAY_FAN_COUNT)*Math.PI*2; return new THREE.Vector3(Math.sin(a),0,Math.cos(a)).normalize(); }); +const raycaster=new THREE.Raycaster(); raycaster.firstHitOnly=true; + +/* helpers */ +const feetYFromEye=(eyeY)=> eyeY - (PLAYER_EYE_HEIGHT - CAPSULE_RADIUS); +function groundSnap(eyePos, maxUp=SNAP_EPS){ + if(!colliderMesh) return false; + const feetY=feetYFromEye(eyePos.y), origin=new THREE.Vector3(eyePos.x, feetY+CAPSULE_RADIUS*0.75, eyePos.z), dir=new THREE.Vector3(0,-1,0); + raycaster.set(origin,dir); raycaster.near=0; raycaster.far=SNAP_CAST_DIST; + const hit=raycaster.intersectObject(colliderMesh,false)[0]; if(!hit) return false; + const n=hit.face?.normal ?? new THREE.Vector3(0,1,0); if(n.y < WALL_NORMAL_Y) return false; + const desiredFeet=hit.point.y + CAPSULE_RADIUS + CONTACT_OFFSET, delta=desiredFeet - feetY; + if(delta>-maxUp && delta=WALL_NORMAL_Y) continue; + const push=radius - hit.distance + CONTACT_OFFSET; if(push>0) eyePos.addScaledVector(d,-push); + } + } + } +} +function sweepClampMove(eyePos, moveXZ){ + if(!colliderMesh) return moveXZ; const len=moveXZ.length(); if(len<1e-6) return moveXZ; + const dir=moveXZ.clone().normalize(), feet=feetYFromEye(eyePos.y); + const heights=[ feet+CAPSULE_RADIUS*0.2, feet+CAPSULE_HEIGHT*0.5, feet+CAPSULE_HEIGHT-CAPSULE_RADIUS*0.2 ]; + const radius=CAPSULE_RADIUS + WALL_MARGIN; let maxLen=len; + for(const h of heights){ + const origin=new THREE.Vector3(eyePos.x,h,eyePos.z); raycaster.set(origin,dir); raycaster.near=0; raycaster.far=len+radius; + const hit=raycaster.intersectObject(colliderMesh,false)[0]; if(!hit) continue; + const n=hit.face?.normal ?? new THREE.Vector3(0,1,0); if(n.y>=WALL_NORMAL_Y) continue; + const allowed=Math.max(0, hit.distance - (radius+CONTACT_OFFSET)); if(allowedcolliderMaxY+200*SCALE){ eye.y=colliderMaxY+200*SCALE; velocity.y=Math.min(0,velocity.y); } +} +function updateFPS(dt){ + if(!playMode || !levelReady || !physicsEnabled) return; + const obj=controls.getObject(); + const fwd=new THREE.Vector3(); camera.getWorldDirection(fwd); fwd.y=0; fwd.normalize(); + const right=new THREE.Vector3().crossVectors(fwd,new THREE.Vector3(0,1,0)).normalize(); + const wish=new THREE.Vector3(); if(keys.has("KeyW")) wish.add(fwd); if(keys.has("KeyS")) wish.addScaledVector(fwd,-1); if(keys.has("KeyA")) wish.addScaledVector(right,-1); if(keys.has("KeyD")) wish.add(right); if(wish.lengthSq()>0) wish.normalize(); + const accel=onGround?ACCEL:AIR_ACCEL, targetVel=wish.multiplyScalar(WALK_SPEED), horizVel=new THREE.Vector3(velocity.x,0,velocity.z), add=new THREE.Vector3().subVectors(targetVel,horizVel).multiplyScalar(accel*dt); + horizVel.add(add); velocity.x=horizVel.x; velocity.z=horizVel.z; + velocity.y -= GRAVITY*dt; + + const moveLen=velocity.clone().multiplyScalar(dt).length(), steps=Math.max(1, Math.ceil(moveLen / MAX_SUBSTEP_DIST)); onGround=false; const subDt=dt/steps; + for(let i=0;i4.0){ respawnAtFire(); } + ensurePlayerLight(obj); +} + +/* remote players + 2D name labels */ +const players=new Map(); +function makeLabelEl(text){ + const el=document.createElement("div"); + el.textContent=text; + Object.assign(el.style,{ + position:"absolute", + transform:"translate(-50%,-100%)", + color:"#fff", + fontWeight:"800", + fontSize:"22px", + lineHeight:"1", + padding:"4px 8px", + borderRadius:"8px", + background:"rgba(0,0,0,0.55)", + textShadow:"0 1px 2px rgba(0,0,0,0.65)", + whiteSpace:"nowrap", + pointerEvents:"none", + willChange:"transform", + }); + labelsLayer.appendChild(el); + return el; +} +function updateLabelPosition(label, worldPos){ + const v = worldPos.clone().project(camera); + if (v.z < 0 || v.z > 1) { label.style.display="none"; return; } + const x = (v.x * 0.5 + 0.5) * innerWidth; + const y = ( -v.y * 0.5 + 0.5) * innerHeight; + label.style.display=""; + label.style.left = `${x}px`; + label.style.top = `${y}px`; +} + +function makeBillboardTexture(cb){ + if(PLAYER_SPRITE_URL){ + const img=new Image(); img.crossOrigin="anonymous"; + img.onload=()=>{ const tex=new THREE.Texture(img); tex.needsUpdate=true; tex.generateMipmaps=false; tex.minFilter=THREE.NearestFilter; tex.magFilter=THREE.NearestFilter; cb(tex); }; + img.onerror=()=>cb(makeProceduralBillboard()); img.src=PLAYER_SPRITE_URL; + } else cb(makeProceduralBillboard()); +} +function makeProceduralBillboard(){ + const W=64,H=128,c=document.createElement("canvas"); c.width=W; c.height=H; const g=c.getContext("2d"); g.fillStyle="rgba(0,0,0,0)"; g.fillRect(0,0,W,H); + g.fillStyle="#d6e0ff"; g.fillRect(18,20,28,78); g.beginPath(); g.arc(32,30,18,0,Math.PI*2); g.fill(); g.fillRect(18,98,10,24); g.fillRect(36,98,10,24); + g.strokeStyle="#131a2f"; g.lineWidth=3; g.strokeRect(18,20,28,78); g.beginPath(); g.arc(32,30,18,0,Math.PI*2); g.stroke(); + const tex=new THREE.CanvasTexture(c); tex.generateMipmaps=false; tex.minFilter=THREE.NearestFilter; tex.magFilter=THREE.NearestFilter; return tex; +} +function makeBillboardSprite(onReady){ + makeBillboardTexture((tex)=>{ + const spr=new THREE.Sprite(new THREE.SpriteMaterial({ map:tex, transparent:true, depthWrite:true, depthTest:true })); + spr.center.set(0.5, 0.0); // bottom at ground + spr.position.y = 0; + spr.scale.set(SPRITE_HEIGHT*SPRITE_ASPECT, SPRITE_HEIGHT, 1); + onReady(spr); + }); +} + +/* spawn/update remote, anchor to ground (pos is eye from server) */ +function spawnOrUpdate(p){ + if (myID && p?.id === myID) return; // ignore self + + const baseY = (p.pos?.[1] ?? 0) - PLAYER_EYE_HEIGHT; + let entry = players.get(p.id); + if(!entry){ + const group=new THREE.Group(); group.position.set(p.pos?.[0]??0, baseY, p.pos?.[2]??0); group.rotation.y=p.rotY||0; + makeBillboardSprite((body)=>{ group.add(body); }); + scene.add(group); + const labelEl = makeLabelEl(p.name || "Player"); + entry = { group, labelEl, target:new THREE.Vector3(p.pos?.[0]??0, baseY, p.pos?.[2]??0), rotY:p.rotY||0, name:p.name||"Player" }; + players.set(p.id, entry); + } else { + entry.target.set(p.pos?.[0]??0, baseY, p.pos?.[2]??0); + entry.rotY = p.rotY || 0; + if (p.name && p.name !== entry.name) { entry.name = p.name; entry.labelEl.textContent = p.name; } + } +} +function removePlayer(id){ + const e=players.get(id); if(!e) return; + scene.remove(e.group); + if(e.labelEl?.parentNode) e.labelEl.parentNode.removeChild(e.labelEl); + players.delete(id); +} + +/* chat UI (only when playing) */ +function makeChatUI(){ + const hint=document.createElement("div"); + Object.assign(hint.style,{ position:"fixed", left:"16px", bottom:"16px", zIndex:20, color:"#fff", font:"600 14px Inter, system-ui", opacity:"0.9", pointerEvents:"none", textAlign:"left", lineHeight:"1.35", background:"rgba(0,0,0,0.45)", padding:"8px 10px", borderRadius:"10px", maxWidth:"44ch" }); + hint.innerHTML=`Pointer-lock FPS
WASD move • SPACE jump • T chat • M music • ESC cursor`; + document.body.appendChild(hint); + + const log=document.createElement("div"); Object.assign(log.style,{ position:"fixed", left:"16px", bottom:"96px", width:"420px", maxHeight:"40vh", overflow:"hidden", display:"flex", flexDirection:"column-reverse", gap:"6px", zIndex:20, font:"15px/1.35 Inter, system-ui", color:"#fff" }); log.id="chat-log"; document.body.appendChild(log); + + const form=document.createElement("form"); form.id="chat-form"; Object.assign(form.style,{ position:"fixed", left:"16px", bottom:"16px", zIndex:21, display:"none" }); + const inp=document.createElement("input"); inp.type="text"; inp.placeholder="type to chat…"; Object.assign(inp.style,{ width:"400px", padding:"10px 12px", borderRadius:"10px", border:"1px solid rgba(255,255,255,.2)", background:"rgba(0,0,0,.5)", color:"#fff" }); form.appendChild(inp); document.body.appendChild(form); + + form.addEventListener("submit",(e)=>{ e.preventDefault(); const t=inp.value.trim(); if(!t) return; send({type:"chat", text:t}); inp.value=""; hideChat(); }); + function showChat(){ form.style.display="block"; inp.focus(); controls.unlock(); } + function hideChat(){ form.style.display="none"; controls.lock(); } + addEventListener("keydown",(e)=>{ if(e.key==="t"||e.key==="T"){ e.preventDefault(); showChat(); }}); + + addEventListener("keydown",(e)=>{ + if(e.code==="KeyM"){ + if(isPlaying) pauseMusic(); else playMusic(); + setStatus(isPlaying ? "music: playing" : "music: paused"); + } + }); +} +function addChat(logEl, name, text){ if(!logEl) return; const row=document.createElement("div"); row.textContent=`${name}: ${text}`; row.style.background="rgba(0,0,0,.45)"; row.style.padding="6px 10px"; row.style.borderRadius="8px"; logEl.prepend(row); const kids=[...logEl.children]; while(kids.length>24){ logEl.removeChild(kids.pop()); } } + +/* networking */ +let ws=null, myID=null, lastSend=0, currentRole="spectator", suppressToast=false; +function connect(role="spectator", name="Viewer"){ + try{ + if(ws && ws.readyState<=1){ suppressToast=true; try{ ws.close(); }catch{} } + currentRole=role; + ws=new WebSocket(WS_URL); ws.binaryType="arraybuffer"; + ws.addEventListener("open", ()=>{ + setStatus(`ws: connected (${role})`); + send({ type:"join", name, role }); + if(role==="player"){ const p=controls?.getObject?.().position; if(p) send({type:"state", pos:[p.x,p.y,p.z], rotY:camera.rotation.y}); } + }); + ws.addEventListener("message",(ev)=>{ try{ onMessage(ev); }catch(e){ setStatus("ws msg err: "+e.message); } }); + ws.addEventListener("close", ()=>{ + if(!suppressToast){ const toast=document.createElement("div"); Object.assign(toast.style,{ position:"fixed", left:"50%", top:"14px", transform:"translateX(-50%)", background:"rgba(200,0,0,0.75)", color:"#fff", padding:"8px 12px", borderRadius:"10px", zIndex:50, font:"600 14px Inter, system-ui" }); toast.textContent="Disconnected from server"; document.body.appendChild(toast); setTimeout(()=>toast.remove(),3500); } + suppressToast=false; setStatus("ws: disconnected"); + }); + ws.addEventListener("error", ()=> setStatus("ws: error")); + }catch(e){ setStatus("ws init err: "+e.message); } +} +function send(obj){ if(ws && ws.readyState===1) ws.send(encode(obj)); } +function onMessage(ev){ + const msg=decode(ev.data); + switch(msg.type){ + case "welcome": + myID = msg.id; + removePlayer(myID); + (msg.players||[]).forEach((p)=>{ if(p.id!==myID) spawnOrUpdate(p); }); + break; + case "player_join": + if(msg.player && msg.player.id !== myID) spawnOrUpdate(msg.player); + break; + case "player_leave": + if(msg.id) removePlayer(msg.id); + break; + case "state": + if(Array.isArray(msg.updates)) msg.updates.forEach((u)=>{ if(u.id!==myID) spawnOrUpdate(u); }); + else if(msg.id && msg.pos){ if(msg.id!==myID) spawnOrUpdate(msg); } + break; + case "player_state": + if(msg.id && msg.pos && msg.id!==myID) spawnOrUpdate(msg); + break; + case "chat": + addChat(document.getElementById("chat-log"), msg.name||"Player", msg.text||""); + break; + case "full": + alert(`World is full (max ${msg.maxPlayers ?? 10})`); + break; + } +} + +/* enter play */ +function startPlay(){ + if(playMode) return; + // if we were spectating, clean it up + if(spectateMode) stopSpectate(); + + playMode=true; + canvas.style.zIndex = "10"; + canvas.style.pointerEvents = "auto"; + canvas.style.opacity = "1"; + labelsLayer.style.zIndex = "15"; + + const controlsLocal=new PointerLockControls(camera, canvas); controls=controlsLocal; scene.add(controls.getObject()); + controls.lock(); canvas.addEventListener("click", ()=>controls.lock()); + controls.addEventListener("lock", ()=> setStatus("locked (WASD, SPACE jump, T chat, M music)")); + controls.addEventListener("unlock", ()=> setStatus("unlocked (click to lock)")); + makeChatUI(); + if(levelReady) initialPlace(true); + const name = (prompt("Authentication not implemented; for now enter a nickname:", "Guest") || "Guest").slice(0,25); + connect("player", name); + addEventListener("keydown",(e)=>{ keys.add(e.code); + if(e.code==="Space" && playMode){ + const now=performance.now(); + if(onGround || (now - lastGroundedAt) < COYOTE_TIME*1000){ + velocity.y = JUMP_STRENGTH; + onGround=false; + } + } + }); + addEventListener("keyup",(e)=> keys.delete(e.code)); +} + +/* join hooks — match your exact buttons and href */ +(function hookJoin(){ + const candidates=[ + '#join-btn', '#spectate-btn', + 'a[href="#Join"]','a[href="#join"]', + 'a.button[href="#About"]','#join','[data-join]','.join','button.join' + ]; + for(const sel of candidates){ + const el=document.querySelector(sel); + if(el){ + el.addEventListener("click",(e)=>{ + e.preventDefault(); + if(el.id === "spectate-btn"){ + startSpectate(); + setStatus("Spectating…"); + }else{ + startPlay(); + } + }); + } + } + addEventListener("keydown",(e)=>{ if(e.code==="Enter" && !playMode) startPlay(); }); +})(); + +/* spectator connect on boot */ +const defaultSpectatorName = `Viewer${Math.floor((Math.random()*9000)+1000)}`; +connect("spectator", defaultSpectatorName); + +/* loop */ +const clock=new THREE.Clock(); let active=true; +addEventListener("visibilitychange",()=>{ active=document.visibilityState==="visible"; if(active) requestAnimationFrame(animate); }); +requestAnimationFrame(()=>{ animate(); canvas.style.opacity="1"; }); + +let fpsAccum=0,fpsCount=0,scaleCooldown=0; +function animate(){ + if(!active) return; + const dt=Math.min(0.05, clock.getDelta()); const now=performance.now(); + if(mixer) mixer.update(dt); + if(playMode) updateFPS(dt); + if(spectateMode) updateFreecam(dt); + + // smooth remote players and update 2D labels + players.forEach(({group,target,rotY,labelEl})=>{ + const k=1 - Math.pow(0.0001, dt*60); + group.position.lerp(target,k); + group.rotation.y += (rotY - group.rotation.y)*k; + + const headWorld = new THREE.Vector3( + group.position.x, + group.position.y + SPRITE_HEIGHT + NAME_PAD, + group.position.z + ); + updateLabelPosition(labelEl, headWorld); + }); + + perFrame.forEach((fn)=>fn(dt)); + + renderer.setRenderTarget(rt); postMat.uniforms.tDiffuse.value=null; renderer.clear(); renderer.render(scene,camera); renderer.setRenderTarget(null); + postMat.uniforms.tDiffuse.value=rt.texture; postMat.uniforms.uTime.value += dt; renderer.render(postScene,postCam); + + fpsAccum += dt; fpsCount++; + if(now - scaleCooldown > 1000){ + const avg=fpsAccum/Math.max(1,fpsCount); + if(avg>0.03 && RENDER_SCALE<6){ RENDER_SCALE+=0.5; allocRT(); } + else if(avg<0.017 && RENDER_SCALE>2){ RENDER_SCALE-=0.5; allocRT(); } + fpsAccum=0; fpsCount=0; scaleCooldown=now; + } + + if(currentRole==="player" && ws && ws.readyState===1 && now - lastSend >= SEND_RATE_MS){ + lastSend=now; const p=controls.getObject().position; send({type:"state", pos:[p.x,p.y,p.z], rotY:camera.rotation.y}); + } + + if(!playMode && !spectateMode) updateCameraHover(dt); + requestAnimationFrame(animate); +} + +/* resize */ +addEventListener("resize",()=>{ + camera.aspect=innerWidth/innerHeight; + camera.updateProjectionMatrix(); + allocRT(); + patchedMaterials.forEach((m)=>{ + const u = ps1Uniforms.get(m); + if(u?.uResolution?.value){ u.uResolution.value.set(innerWidth, innerHeight); } + }); +}); diff --git a/World-SEPPJM-Client/frontend/src/main.js b/World-SEPPJM-Client/frontend/src/main.js new file mode 100644 index 0000000..95c41a5 --- /dev/null +++ b/World-SEPPJM-Client/frontend/src/main.js @@ -0,0 +1,8 @@ +import './style.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/World-SEPPJM-Client/frontend/src/style.css b/World-SEPPJM-Client/frontend/src/style.css new file mode 100644 index 0000000..a1e7f8e --- /dev/null +++ b/World-SEPPJM-Client/frontend/src/style.css @@ -0,0 +1,3 @@ +html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} +.container{position:relative;width:100%;max-width:960px;margin:0 auto;padding:0 20px;box-sizing:border-box}.column,.columns{width:100%;float:left;box-sizing:border-box}@media (min-width:400px){.container{width:85%;padding:0}}@media (min-width:550px){.container{width:80%}.column,.columns{margin-left:4%}.column:first-child,.columns:first-child{margin-left:0}.one.column,.one.columns{width:4.66666666667%}.two.columns{width:13.3333333333%}.three.columns{width:22%}.four.columns{width:30.6666666667%}.five.columns{width:39.3333333333%}.six.columns{width:48%}.seven.columns{width:56.6666666667%}.eight.columns{width:65.3333333333%}.nine.columns{width:74.0%}.ten.columns{width:82.6666666667%}.eleven.columns{width:91.3333333333%}.twelve.columns{width:100%;margin-left:0}.one-third.column{width:30.6666666667%}.two-thirds.column{width:65.3333333333%}.one-half.column{width:48%}.offset-by-one.column,.offset-by-one.columns{margin-left:8.66666666667%}.offset-by-two.column,.offset-by-two.columns{margin-left:17.3333333333%}.offset-by-three.column,.offset-by-three.columns{margin-left:26%}.offset-by-four.column,.offset-by-four.columns{margin-left:34.6666666667%}.offset-by-five.column,.offset-by-five.columns{margin-left:43.3333333333%}.offset-by-six.column,.offset-by-six.columns{margin-left:52%}.offset-by-seven.column,.offset-by-seven.columns{margin-left:60.6666666667%}.offset-by-eight.column,.offset-by-eight.columns{margin-left:69.3333333333%}.offset-by-nine.column,.offset-by-nine.columns{margin-left:78.0%}.offset-by-ten.column,.offset-by-ten.columns{margin-left:86.6666666667%}.offset-by-eleven.column,.offset-by-eleven.columns{margin-left:95.3333333333%}.offset-by-one-third.column,.offset-by-one-third.columns{margin-left:34.6666666667%}.offset-by-two-thirds.column,.offset-by-two-thirds.columns{margin-left:69.3333333333%}.offset-by-one-half.column,.offset-by-one-half.columns{margin-left:52%}}html{font-size:62.5%}body{background-color:#111010;font-size:1.5em;line-height:1.6;font-weight:400;font-family:"Courier New","HelveticaNeue","Helvetica Neue",Helvetica,Arial,sans-serif;color:#fff}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:2rem;font-weight:300}h1{font-size:4.0rem;line-height:1.2;letter-spacing:-.1rem}h2{font-size:3.6rem;line-height:1.25;letter-spacing:-.1rem}h3{font-size:3.0rem;line-height:1.3;letter-spacing:-.1rem}h4{font-size:2.4rem;line-height:1.35;letter-spacing:-.08rem}h5{font-size:1.8rem;line-height:1.5;letter-spacing:-.05rem}h6{font-size:1.5rem;line-height:1.6;letter-spacing:0}@media (min-width:550px){h1{font-size:5.0rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3.0rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}p{margin-top:0}a{color:#1eaedb}a:hover{color:#0fa0ce}.button,button,input[type="submit"],input[type="reset"],input[type="button"]{display:inline-block;height:38px;padding:0 30px;color:#fff;text-align:center;font-size:11px;font-weight:600;line-height:38px;letter-spacing:.1rem;text-transform:uppercase;text-decoration:none;white-space:nowrap;background-color:transparent;border-radius:4px;border:1px solid #bbb;cursor:pointer;box-sizing:border-box}.button:hover,button:hover,input[type="submit"]:hover,input[type="reset"]:hover,input[type="button"]:hover,.button:focus,button:focus,input[type="submit"]:focus,input[type="reset"]:focus,input[type="button"]:focus{color:#333;border-color:#888;outline:0}.button.button-primary,button.button-primary,input[type="submit"].button-primary,input[type="reset"].button-primary,input[type="button"].button-primary{color:#fff;background-color:#33c3f0;border-color:#33c3f0}.button.button-primary:hover,button.button-primary:hover,input[type="submit"].button-primary:hover,input[type="reset"].button-primary:hover,input[type="button"].button-primary:hover,.button.button-primary:focus,button.button-primary:focus,input[type="submit"].button-primary:focus,input[type="reset"].button-primary:focus,input[type="button"].button-primary:focus{color:#fff;background-color:#1eaedb;border-color:#1eaedb}input[type="email"],input[type="number"],input[type="search"],input[type="text"],input[type="tel"],input[type="url"],input[type="password"],textarea,select{height:38px;padding:6px 10px;background-color:#fff;border:1px solid #d1d1d1;border-radius:4px;box-shadow:none;box-sizing:border-box}input[type="email"],input[type="number"],input[type="search"],input[type="text"],input[type="tel"],input[type="url"],input[type="password"],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none}textarea{min-height:65px;padding-top:6px;padding-bottom:6px}input[type="email"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="text"]:focus,input[type="tel"]:focus,input[type="url"]:focus,input[type="password"]:focus,textarea:focus,select:focus{border:1px solid #33c3f0;outline:0}label,legend{display:block;margin-bottom:.5rem;font-weight:600}fieldset{padding:0;border-width:0}input[type="checkbox"],input[type="radio"]{display:inline}label>.label-body{display:inline-block;margin-left:.5rem;font-weight:normal}ul{list-style:circle inside}ol{list-style:decimal inside}ol,ul{padding-left:0;margin-top:0}ul ul,ul ol,ol ol,ol ul{margin:1.5rem 0 1.5rem 3rem;font-size:90%}li{margin-bottom:1rem}code{padding:.2rem .5rem;margin:0 .2rem;font-size:90%;white-space:nowrap;border:1px solid #e1e1e1;border-radius:4px;overflow-x:auto}pre>code{display:block;padding:1rem 1.5rem;white-space:pre}th,td{padding:12px 15px;text-align:left;border-bottom:1px solid #e1e1e1}th:first-child,td:first-child{padding-left:0}th:last-child,td:last-child{padding-right:0}button,.button{margin-bottom:1rem}input,textarea,select,fieldset{margin-bottom:1.5rem}pre,blockquote,dl,figure,table,p,ul,ol,form{margin-bottom:2.5rem}.u-full-width{width:100%;box-sizing:border-box}.u-max-full-width{max-width:100%;box-sizing:border-box}.u-pull-right{float:right}.u-pull-left{float:left}hr{margin-top:3rem;margin-bottom:3.5rem;border-width:0;border-top:1px solid #e1e1e1}.container:after,.row:after,.u-cf{content:"";display:table;clear:both}@media (max-width:550px){#navbar{overflow:hidden;background-color:#000;position:fixed;width:100%;text-align:center;top:0;z-index:100;transition:top .3s}#navbar a{display:block;color:#f2f2f2;text-align:center;padding:14px;text-decoration:none}#navbar a:hover{background-color: #ffefef21}.nav #navlinks{display:none}.dropdown{display:block;color:#f2f2f2;text-align:center;text-decoration:none}.dropdown .dropbtn{border:none;outline:none;padding:14px 16px;background-color:inherit;font-family:inherit;margin:0}.navbar a:hover,.dropdown:hover .dropbtn{background-color: #ffefef21}.dropdown-content{display:none;position:fixed;background-color:#000;min-width:160px;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);width:100%;z-index:1}.dropdown-content a{float:none;color:#000;padding:12px 16px;text-decoration:none;display:block;text-align:left}.dropdown-content a:hover{background-color:#ddd}.show{display:block}}@media (min-width:550px){#navbar{overflow:hidden;background-color:#000;position:fixed;width:100%;text-align:center;top:0;z-index:100;transition:top .3s}#navbar a{display:inline-block;color:#f2f2f2;text-align:center;padding:14px;text-decoration:none}#navbar a:hover{background-color: #ffefef21}.nav #burgermenu{display:none}.dropdown{display:inline-block;color:#f2f2f2;text-align:center;text-decoration:none}.dropdown .dropbtn{border:none;outline:none;padding:14px 16px;background-color:inherit;font-family:inherit;margin:0}.navbar a:hover,.dropdown:hover .dropbtn{background-color: #ffefef21}.dropdown-content{display:none;position:fixed;background-color:#000;min-width:160px;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);z-index:1}.dropdown-content a{float:none;color:#000;padding:12px 16px;text-decoration:none;display:block;text-align:left}.dropdown-content a:hover{background-color:#ddd}.show{display:block}}footer{text-align:center;padding:22px;background-color:#000;color:#fff;margin-top:25px} +@font-face{font-family:'FontAwesome';src:url(../fonts/fontawesome-webfont.eot?v=4.7.0);src:url(../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0) format('embedded-opentype') , url(../fonts/fontawesome-webfont.woff2?v=4.7.0) format('woff2') , url(../fonts/fontawesome-webfont.woff?v=4.7.0) format('woff') , url(../fonts/fontawesome-webfont.ttf?v=4.7.0) format('truetype') , url(../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular) format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} \ No newline at end of file diff --git a/World-SEPPJM-Client/frontend/src/vite-env.d.ts b/World-SEPPJM-Client/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/World-SEPPJM-Client/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/World-SEPPJM-Client/frontend/vite.config.js b/World-SEPPJM-Client/frontend/vite.config.js new file mode 100644 index 0000000..d37616f --- /dev/null +++ b/World-SEPPJM-Client/frontend/vite.config.js @@ -0,0 +1,7 @@ +import {defineConfig} from 'vite' +import {svelte} from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()] +}) diff --git a/World-SEPPJM-Client/frontend/wailsjs/go/main/App.d.ts b/World-SEPPJM-Client/frontend/wailsjs/go/main/App.d.ts new file mode 100644 index 0000000..02a3bb9 --- /dev/null +++ b/World-SEPPJM-Client/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,4 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1:string):Promise; diff --git a/World-SEPPJM-Client/frontend/wailsjs/go/main/App.js b/World-SEPPJM-Client/frontend/wailsjs/go/main/App.js new file mode 100644 index 0000000..c71ae77 --- /dev/null +++ b/World-SEPPJM-Client/frontend/wailsjs/go/main/App.js @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Greet(arg1) { + return window['go']['main']['App']['Greet'](arg1); +} diff --git a/World-SEPPJM-Client/frontend/wailsjs/runtime/package.json b/World-SEPPJM-Client/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/World-SEPPJM-Client/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.d.ts b/World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..4445dac --- /dev/null +++ b/World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,249 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.js b/World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..623397b --- /dev/null +++ b/World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,238 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} \ No newline at end of file diff --git a/World-SEPPJM-Client/go.mod b/World-SEPPJM-Client/go.mod new file mode 100644 index 0000000..442d7b5 --- /dev/null +++ b/World-SEPPJM-Client/go.mod @@ -0,0 +1,37 @@ +module World-SEPPJM-Client + +go 1.23 + +require github.com/wailsapp/wails/v2 v2.10.2 + +require ( + github.com/bep/debounce v1.2.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.19 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.10.2 => C:\Users\Sepp\go\pkg\mod diff --git a/World-SEPPJM-Client/go.sum b/World-SEPPJM-Client/go.sum new file mode 100644 index 0000000..b1e0229 --- /dev/null +++ b/World-SEPPJM-Client/go.sum @@ -0,0 +1,81 @@ +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU= +github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk= +github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/World-SEPPJM-Client/main.go b/World-SEPPJM-Client/main.go new file mode 100644 index 0000000..7ca33e9 --- /dev/null +++ b/World-SEPPJM-Client/main.go @@ -0,0 +1,34 @@ +// main.go +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +func main() { + app := NewApp() + + err := wails.Run(&options.App{ + Title: "World-SEPPJM-Client", + Width: 1024, + Height: 768, + + // Start fullscreen? + WindowStartState: options.Fullscreen, + + AssetServer: &assetserver.Options{Assets: assets}, + BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 255}, + OnStartup: app.startup, + Bind: []interface{}{app}, + }) + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/World-SEPPJM-Client/wails.json b/World-SEPPJM-Client/wails.json new file mode 100644 index 0000000..7c12328 --- /dev/null +++ b/World-SEPPJM-Client/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "World-SEPPJM-Client", + "outputfilename": "World-SEPPJM-Client", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Sepp Jeremiah Morris", + "email": "git@seppdroid.com" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29