init project release
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
25
Backend/Dockerfile
Normal file
25
Backend/Dockerfile
Normal file
@@ -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"]
|
10
Backend/go.mod
Normal file
10
Backend/go.mod
Normal file
@@ -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
|
14
Backend/go.sum
Normal file
14
Backend/go.sum
Normal file
@@ -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=
|
416
Backend/main.go
Normal file
416
Backend/main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
BIN
Frontend-WEB/world.seppjm.com/3js/audio/stalker.mp3
Normal file
BIN
Frontend-WEB/world.seppjm.com/3js/audio/stalker.mp3
Normal file
Binary file not shown.
655
Frontend-WEB/world.seppjm.com/3js/background.js
Normal file
655
Frontend-WEB/world.seppjm.com/3js/background.js
Normal file
@@ -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 <project_vertex>",
|
||||||
|
`
|
||||||
|
#include <project_vertex>
|
||||||
|
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();
|
||||||
|
})();
|
BIN
Frontend-WEB/world.seppjm.com/3js/models/scene.fbx
Normal file
BIN
Frontend-WEB/world.seppjm.com/3js/models/scene.fbx
Normal file
Binary file not shown.
682
Frontend-WEB/world.seppjm.com/3js/world.js
Normal file
682
Frontend-WEB/world.seppjm.com/3js/world.js
Normal file
@@ -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 <project_vertex>',
|
||||||
|
`
|
||||||
|
#include <project_vertex>
|
||||||
|
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<geos.length;i+=64){ chunks.push(BufferGeometryUtils.mergeGeometries(geos.slice(i,i+64),false)); } merged=BufferGeometryUtils.mergeGeometries(chunks,false); }
|
||||||
|
merged = BufferGeometryUtils.mergeVertices(merged,1e-3); merged.computeBoundingBox(); merged.computeBoundingSphere();
|
||||||
|
colliderGeom=merged; colliderBVH=new MeshBVH(colliderGeom,{maxLeafTris:32}); colliderGeom.boundsTree=colliderBVH;
|
||||||
|
colliderMesh=new THREE.Mesh(colliderGeom,new THREE.MeshBasicMaterial({visible:false}));
|
||||||
|
colliderMinY = colliderGeom.boundingBox ? colliderGeom.boundingBox.min.y : -1e6;
|
||||||
|
colliderMaxY = colliderGeom.boundingBox ? colliderGeom.boundingBox.max.y : +1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fog tuning vs scene size */
|
||||||
|
function autoFogFor(target){
|
||||||
|
const box=new THREE.Box3().setFromObject(target), size=box.getSize(new THREE.Vector3());
|
||||||
|
const k=0.03, density=Math.min(0.06, Math.max(0.000005, k/(Math.max(size.x,size.y,size.z)*0.5 || 1)));
|
||||||
|
scene.fog = USE_FOG ? new THREE.FogExp2(0x0b1020, density) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* campfire */
|
||||||
|
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*.5,size*.6,size*.05,size*.5,size*.6,size*.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);
|
||||||
|
fireSprite=new THREE.Sprite(new THREE.SpriteMaterial({ map:flameTex, color:0xffffff, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending, depthTest:true }));
|
||||||
|
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);
|
||||||
|
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=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<maxUp){ eyePos.y += delta; return true; } return false;
|
||||||
|
}
|
||||||
|
function pushOutWalls(eyePos){
|
||||||
|
if(!colliderMesh) return;
|
||||||
|
const feet=feetYFromEye(eyePos.y), heights=[ feet+CAPSULE_RADIUS*0.2, feet+CAPSULE_HEIGHT*0.5, feet+CAPSULE_HEIGHT-CAPSULE_RADIUS*0.2 ];
|
||||||
|
const radius=CAPSULE_RADIUS + WALL_MARGIN;
|
||||||
|
for(let iter=0; iter<WALL_ITER; iter++){
|
||||||
|
for(const h of heights){
|
||||||
|
for(const d of dirFan){
|
||||||
|
const o=new THREE.Vector3(eyePos.x,h,eyePos.z); raycaster.set(o,d); raycaster.near=0; raycaster.far=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 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(allowed<maxLen) maxLen=allowed;
|
||||||
|
}
|
||||||
|
if(maxLen<len) moveXZ.setLength(maxLen); return moveXZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* spawn at fire */
|
||||||
|
function initialPlace(forceHigh=false){
|
||||||
|
if(!levelReady) return;
|
||||||
|
const obj=controls.getObject(); const spawn=FIRE_POS.clone().add(SPAWN_OFFSET); const y=(forceHigh?SPAWN_EXTRA_Y:5*SCALE);
|
||||||
|
obj.position.copy(spawn).add(new THREE.Vector3(0,y,0));
|
||||||
|
const toFire=new THREE.Vector3().subVectors(FIRE_POS,spawn), yaw=Math.atan2(toFire.x,toFire.z);
|
||||||
|
obj.rotation.set(0,yaw,0,"YXZ"); camera.rotation.set(0,yaw,0,"YXZ");
|
||||||
|
physicsEnabled=true; velocity.set(0,0,0); onGround=false; timeSinceGrounded=0; lastGroundedAt=0; lastSafePos.copy(obj.position); setStatus("ready");
|
||||||
|
}
|
||||||
|
function respawnAtFire(){ const obj=controls.getObject(), spawn=FIRE_POS.clone().add(SPAWN_OFFSET); obj.position.copy(spawn).add(new THREE.Vector3(0,SPAWN_EXTRA_Y,0)); velocity.set(0,0,0); onGround=false; timeSinceGrounded=0; setStatus("respawn"); }
|
||||||
|
|
||||||
|
/* physics step */
|
||||||
|
function integrateSubstep(subDt){
|
||||||
|
const obj=controls.getObject(), eye=obj.position, move=velocity.clone().multiplyScalar(subDt), moveXZ=new THREE.Vector3(move.x,0,move.z);
|
||||||
|
sweepClampMove(eye,moveXZ); eye.add(moveXZ); pushOutWalls(eye);
|
||||||
|
eye.y += move.y; pushOutWalls(eye);
|
||||||
|
|
||||||
|
if (velocity.y <= 0) {
|
||||||
|
const snapped=groundSnap(eye, SNAP_EPS + Math.max(0,-velocity.y*subDt + CONTACT_OFFSET));
|
||||||
|
if(snapped){ onGround=true; if(velocity.y<0) velocity.y=0; lastGroundedAt=performance.now(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Number.isFinite(colliderMinY)){ const minEye=colliderMinY + (PLAYER_EYE_HEIGHT - CAPSULE_RADIUS) + 0.25*SCALE; if(eye.y<minEye){ eye.y=minEye; velocity.y=Math.max(0,velocity.y); } }
|
||||||
|
if(Number.isFinite(colliderMaxY) && eye.y>colliderMaxY+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;i<steps;i++) integrateSubstep(subDt);
|
||||||
|
|
||||||
|
if(velocity.y <= 0 && groundSnap(obj.position)) { onGround=true; lastGroundedAt=performance.now(); }
|
||||||
|
|
||||||
|
if(onGround){ const f=Math.max(0,1-FRICTION*dt); velocity.x*=f; velocity.z*=f; if(velocity.y<0) velocity.y=0; timeSinceGrounded=0; lastSafePos.copy(obj.position); setStatus("grounded"); }
|
||||||
|
else { timeSinceGrounded+=dt; setStatus("air"); }
|
||||||
|
|
||||||
|
const minEye=colliderMinY + (PLAYER_EYE_HEIGHT - CAPSULE_RADIUS) - 10*SCALE; if(obj.position.y<minEye || timeSinceGrounded>4.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<br>WASD move • <b>SPACE</b> jump • <b>T</b> chat • <b>M</b> music • <b>ESC</b> 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); }
|
||||||
|
});
|
||||||
|
});
|
68
Frontend-WEB/world.seppjm.com/index.html
Normal file
68
Frontend-WEB/world.seppjm.com/index.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- Basic Page Needs -->
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>SEPPJM.COM | Join World</title>
|
||||||
|
<meta name="description" content="World | SeppJM.com" />
|
||||||
|
<meta name="author" content="Sepp -seppdroid- Jeremiah Morris" />
|
||||||
|
|
||||||
|
<!-- Mobile Specific Metas -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<!-- FONT -->
|
||||||
|
<link href="//sjm.cdn.prutzel.com/fonts/crn/font.css" rel="stylesheet" type="text/css" />
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="//sjm.cdn.prutzel.com/css/normalize.css" />
|
||||||
|
<link rel="stylesheet" href="//sjm.cdn.prutzel.com/css/skeleton.css" />
|
||||||
|
<link rel="stylesheet" href="//sjm.cdn.prutzel.com/fonts/font-awesome-4.7.0/css/font-awesome.min.css" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="//sjm.cdn.prutzel.com/images/favicon.png" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Primary Page Layout
|
||||||
|
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="column" style="margin-top: 17%">
|
||||||
|
<center><h3>Join World</h3></center>
|
||||||
|
<center><p>Let's start</p></center>
|
||||||
|
<br>
|
||||||
|
<center>
|
||||||
|
<a class="button" href="#Join" id="join-btn">Join</a>
|
||||||
|
<a class="button" href="#Spectate" id="spectate-btn">Spectate</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a class="button" href="https://git.seppjm.com/seppdroid/world-seppjm" id="src-code-btn">Source Code</a>
|
||||||
|
<a class="button" href="https://git.seppjm.com/seppdroid/world-seppjm/releases" id="client-btn">Download Client</a>
|
||||||
|
<a class="button" href="steam://rungameid/3379200" id="devbox-btn">Start in DiodeMatrix DEVBOX</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a class="button" href="https://seppjm.com" id="back-btn">Back to SEPPJM.COM</a>
|
||||||
|
</center><br>
|
||||||
|
<center><p>---</p></center><br>
|
||||||
|
<center><p>Special thanks & credits to <a href="https://www.patreon.com/McPatoo">McDuck</a> for the PSX Scene and <a href="https://senko.dev/">SENKO</a> for the song.</p></center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JS -->
|
||||||
|
<script async src="https://unpkg.com/es-module-shims@1.10.0/dist/es-module-shims.js"></script>
|
||||||
|
|
||||||
|
<!-- Import map for three.js -->
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
||||||
|
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Player mode overlay -->
|
||||||
|
<script type="module" src="/3js/world.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
World-SEPPJM-Client/.gitignore
vendored
Normal file
3
World-SEPPJM-Client/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build/bin
|
||||||
|
node_modules
|
||||||
|
frontend/dist
|
16
World-SEPPJM-Client/README.md
Normal file
16
World-SEPPJM-Client/README.md
Normal file
@@ -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`.
|
27
World-SEPPJM-Client/app.go
Normal file
27
World-SEPPJM-Client/app.go
Normal file
@@ -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)
|
||||||
|
}
|
35
World-SEPPJM-Client/build/README.md
Normal file
35
World-SEPPJM-Client/build/README.md
Normal file
@@ -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.
|
BIN
World-SEPPJM-Client/build/appicon.png
Normal file
BIN
World-SEPPJM-Client/build/appicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
68
World-SEPPJM-Client/build/darwin/Info.dev.plist
Normal file
68
World-SEPPJM-Client/build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
63
World-SEPPJM-Client/build/darwin/Info.plist
Normal file
63
World-SEPPJM-Client/build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
</dict>
|
||||||
|
</plist>
|
BIN
World-SEPPJM-Client/build/windows/icon.ico
Normal file
BIN
World-SEPPJM-Client/build/windows/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
15
World-SEPPJM-Client/build/windows/info.json
Normal file
15
World-SEPPJM-Client/build/windows/info.json
Normal file
@@ -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}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
World-SEPPJM-Client/build/windows/installer/project.nsi
Normal file
114
World-SEPPJM-Client/build/windows/installer/project.nsi
Normal file
@@ -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
|
249
World-SEPPJM-Client/build/windows/installer/wails_tools.nsh
Normal file
249
World-SEPPJM-Client/build/windows/installer/wails_tools.nsh
Normal file
@@ -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
|
15
World-SEPPJM-Client/build/windows/wails.exe.manifest
Normal file
15
World-SEPPJM-Client/build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
5
World-SEPPJM-Client/frontend/.vscode/extensions.json
vendored
Normal file
5
World-SEPPJM-Client/frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"svelte.svelte-vscode"
|
||||||
|
]
|
||||||
|
}
|
63
World-SEPPJM-Client/frontend/README.md
Normal file
63
World-SEPPJM-Client/frontend/README.md
Normal file
@@ -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)
|
||||||
|
```
|
12
World-SEPPJM-Client/frontend/index.html
Normal file
12
World-SEPPJM-Client/frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>World-SEPPJM-Client</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="./src/main.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
38
World-SEPPJM-Client/frontend/jsconfig.json
Normal file
38
World-SEPPJM-Client/frontend/jsconfig.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
781
World-SEPPJM-Client/frontend/package-lock.json
generated
Normal file
781
World-SEPPJM-Client/frontend/package-lock.json
generated
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
World-SEPPJM-Client/frontend/package.json
Normal file
16
World-SEPPJM-Client/frontend/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
1
World-SEPPJM-Client/frontend/package.json.md5
Normal file
1
World-SEPPJM-Client/frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
|||||||
|
d9dc84f0d17ed164f36dd584057aae68
|
109
World-SEPPJM-Client/frontend/src/App.svelte
Normal file
109
World-SEPPJM-Client/frontend/src/App.svelte
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<!-- frontend/src/App.svelte -->
|
||||||
|
<script>
|
||||||
|
import { Quit } from '../wailsjs/runtime/runtime';
|
||||||
|
|
||||||
|
let view = 'menu'; // 'menu' | 'offline'
|
||||||
|
|
||||||
|
function goOffline(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
view = 'offline';
|
||||||
|
}
|
||||||
|
function backToMenu(e) {
|
||||||
|
e?.preventDefault();
|
||||||
|
view = 'menu';
|
||||||
|
}
|
||||||
|
function exitApp(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (e.key === 'Escape' && view !== 'menu') view = 'menu';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link href="//sjm.cdn.prutzel.com/fonts/crn/font.css" rel="stylesheet" type="text/css" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeydown} />
|
||||||
|
|
||||||
|
{#if view === 'menu'}
|
||||||
|
<main
|
||||||
|
class="container"
|
||||||
|
style="min-height: 100vh; display: flex; align-items: center; justify-content: center;"
|
||||||
|
>
|
||||||
|
<section class="row" style="width: 100%;">
|
||||||
|
<div class="column" style="text-align: center;">
|
||||||
|
<h3>World.SEPPJM Client</h3>
|
||||||
|
<p>Join the online world or play offline</p>
|
||||||
|
|
||||||
|
<div style="margin: 1rem 0;">
|
||||||
|
<a class="button" href="https://world.seppjm.com">Online</a>
|
||||||
|
<a class="button" href="#" on:click={goOffline}>Offline</a>
|
||||||
|
<a class="button" href="https://seppjm.com">My Website</a>
|
||||||
|
<a class="button" href="#" on:click={exitApp}>Exit (Alt+F4)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{:else if view === 'offline'}
|
||||||
|
<main
|
||||||
|
class="container"
|
||||||
|
style="min-height: 100vh; display: flex; align-items: center; justify-content: center;"
|
||||||
|
>
|
||||||
|
<section class="row" style="width: 100%;">
|
||||||
|
<div class="column" style="text-align: center;">
|
||||||
|
<h3>Offline Mode not available</h3>
|
||||||
|
<p>Offline mode not implemented yet. Press <kbd>Esc</kbd> or use the button in the top-right to return.</p>
|
||||||
|
|
||||||
|
<!-- TODO: offline 3js scene here -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if view !== 'menu'}
|
||||||
|
<button class="overlay-back" on:click={backToMenu} aria-label="Return to menu" title="Return">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M15 18l-6-6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span class="label">Return</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay-back {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.65;
|
||||||
|
transition: opacity 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.25);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
.overlay-back:hover,
|
||||||
|
.overlay-back:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 18px rgba(0,0,0,0.3);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.overlay-back .label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
93
World-SEPPJM-Client/frontend/src/assets/fonts/OFL.txt
Normal file
93
World-SEPPJM-Client/frontend/src/assets/fonts/OFL.txt
Normal file
@@ -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.
|
Binary file not shown.
682
World-SEPPJM-Client/frontend/src/assets/js/world.js
Normal file
682
World-SEPPJM-Client/frontend/src/assets/js/world.js
Normal file
@@ -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 <project_vertex>',
|
||||||
|
`
|
||||||
|
#include <project_vertex>
|
||||||
|
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<geos.length;i+=64){ chunks.push(BufferGeometryUtils.mergeGeometries(geos.slice(i,i+64),false)); } merged=BufferGeometryUtils.mergeGeometries(chunks,false); }
|
||||||
|
merged = BufferGeometryUtils.mergeVertices(merged,1e-3); merged.computeBoundingBox(); merged.computeBoundingSphere();
|
||||||
|
colliderGeom=merged; colliderBVH=new MeshBVH(colliderGeom,{maxLeafTris:32}); colliderGeom.boundsTree=colliderBVH;
|
||||||
|
colliderMesh=new THREE.Mesh(colliderGeom,new THREE.MeshBasicMaterial({visible:false}));
|
||||||
|
colliderMinY = colliderGeom.boundingBox ? colliderGeom.boundingBox.min.y : -1e6;
|
||||||
|
colliderMaxY = colliderGeom.boundingBox ? colliderGeom.boundingBox.max.y : +1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fog tuning vs scene size */
|
||||||
|
function autoFogFor(target){
|
||||||
|
const box=new THREE.Box3().setFromObject(target), size=box.getSize(new THREE.Vector3());
|
||||||
|
const k=0.03, density=Math.min(0.06, Math.max(0.000005, k/(Math.max(size.x,size.y,size.z)*0.5 || 1)));
|
||||||
|
scene.fog = USE_FOG ? new THREE.FogExp2(0x0b1020, density) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* campfire */
|
||||||
|
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*.5,size*.6,size*.05,size*.5,size*.6,size*.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);
|
||||||
|
fireSprite=new THREE.Sprite(new THREE.SpriteMaterial({ map:flameTex, color:0xffffff, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending, depthTest:true }));
|
||||||
|
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);
|
||||||
|
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=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<maxUp){ eyePos.y += delta; return true; } return false;
|
||||||
|
}
|
||||||
|
function pushOutWalls(eyePos){
|
||||||
|
if(!colliderMesh) return;
|
||||||
|
const feet=feetYFromEye(eyePos.y), heights=[ feet+CAPSULE_RADIUS*0.2, feet+CAPSULE_HEIGHT*0.5, feet+CAPSULE_HEIGHT-CAPSULE_RADIUS*0.2 ];
|
||||||
|
const radius=CAPSULE_RADIUS + WALL_MARGIN;
|
||||||
|
for(let iter=0; iter<WALL_ITER; iter++){
|
||||||
|
for(const h of heights){
|
||||||
|
for(const d of dirFan){
|
||||||
|
const o=new THREE.Vector3(eyePos.x,h,eyePos.z); raycaster.set(o,d); raycaster.near=0; raycaster.far=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 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(allowed<maxLen) maxLen=allowed;
|
||||||
|
}
|
||||||
|
if(maxLen<len) moveXZ.setLength(maxLen); return moveXZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* spawn at fire */
|
||||||
|
function initialPlace(forceHigh=false){
|
||||||
|
if(!levelReady) return;
|
||||||
|
const obj=controls.getObject(); const spawn=FIRE_POS.clone().add(SPAWN_OFFSET); const y=(forceHigh?SPAWN_EXTRA_Y:5*SCALE);
|
||||||
|
obj.position.copy(spawn).add(new THREE.Vector3(0,y,0));
|
||||||
|
const toFire=new THREE.Vector3().subVectors(FIRE_POS,spawn), yaw=Math.atan2(toFire.x,toFire.z);
|
||||||
|
obj.rotation.set(0,yaw,0,"YXZ"); camera.rotation.set(0,yaw,0,"YXZ");
|
||||||
|
physicsEnabled=true; velocity.set(0,0,0); onGround=false; timeSinceGrounded=0; lastGroundedAt=0; lastSafePos.copy(obj.position); setStatus("ready");
|
||||||
|
}
|
||||||
|
function respawnAtFire(){ const obj=controls.getObject(), spawn=FIRE_POS.clone().add(SPAWN_OFFSET); obj.position.copy(spawn).add(new THREE.Vector3(0,SPAWN_EXTRA_Y,0)); velocity.set(0,0,0); onGround=false; timeSinceGrounded=0; setStatus("respawn"); }
|
||||||
|
|
||||||
|
/* physics step */
|
||||||
|
function integrateSubstep(subDt){
|
||||||
|
const obj=controls.getObject(), eye=obj.position, move=velocity.clone().multiplyScalar(subDt), moveXZ=new THREE.Vector3(move.x,0,move.z);
|
||||||
|
sweepClampMove(eye,moveXZ); eye.add(moveXZ); pushOutWalls(eye);
|
||||||
|
eye.y += move.y; pushOutWalls(eye);
|
||||||
|
|
||||||
|
if (velocity.y <= 0) {
|
||||||
|
const snapped=groundSnap(eye, SNAP_EPS + Math.max(0,-velocity.y*subDt + CONTACT_OFFSET));
|
||||||
|
if(snapped){ onGround=true; if(velocity.y<0) velocity.y=0; lastGroundedAt=performance.now(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Number.isFinite(colliderMinY)){ const minEye=colliderMinY + (PLAYER_EYE_HEIGHT - CAPSULE_RADIUS) + 0.25*SCALE; if(eye.y<minEye){ eye.y=minEye; velocity.y=Math.max(0,velocity.y); } }
|
||||||
|
if(Number.isFinite(colliderMaxY) && eye.y>colliderMaxY+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;i<steps;i++) integrateSubstep(subDt);
|
||||||
|
|
||||||
|
if(velocity.y <= 0 && groundSnap(obj.position)) { onGround=true; lastGroundedAt=performance.now(); }
|
||||||
|
|
||||||
|
if(onGround){ const f=Math.max(0,1-FRICTION*dt); velocity.x*=f; velocity.z*=f; if(velocity.y<0) velocity.y=0; timeSinceGrounded=0; lastSafePos.copy(obj.position); setStatus("grounded"); }
|
||||||
|
else { timeSinceGrounded+=dt; setStatus("air"); }
|
||||||
|
|
||||||
|
const minEye=colliderMinY + (PLAYER_EYE_HEIGHT - CAPSULE_RADIUS) - 10*SCALE; if(obj.position.y<minEye || timeSinceGrounded>4.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<br>WASD move • <b>SPACE</b> jump • <b>T</b> chat • <b>M</b> music • <b>ESC</b> 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); }
|
||||||
|
});
|
||||||
|
});
|
8
World-SEPPJM-Client/frontend/src/main.js
Normal file
8
World-SEPPJM-Client/frontend/src/main.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import './style.css'
|
||||||
|
import App from './App.svelte'
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.getElementById('app')
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
3
World-SEPPJM-Client/frontend/src/style.css
Normal file
3
World-SEPPJM-Client/frontend/src/style.css
Normal file
File diff suppressed because one or more lines are too long
2
World-SEPPJM-Client/frontend/src/vite-env.d.ts
vendored
Normal file
2
World-SEPPJM-Client/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
7
World-SEPPJM-Client/frontend/vite.config.js
Normal file
7
World-SEPPJM-Client/frontend/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import {svelte} from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()]
|
||||||
|
})
|
4
World-SEPPJM-Client/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
4
World-SEPPJM-Client/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -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<string>;
|
7
World-SEPPJM-Client/frontend/wailsjs/go/main/App.js
Normal file
7
World-SEPPJM-Client/frontend/wailsjs/go/main/App.js
Normal file
@@ -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);
|
||||||
|
}
|
24
World-SEPPJM-Client/frontend/wailsjs/runtime/package.json
Normal file
24
World-SEPPJM-Client/frontend/wailsjs/runtime/package.json
Normal file
@@ -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 <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
249
World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -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<boolean>;
|
||||||
|
|
||||||
|
// [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<Size>;
|
||||||
|
|
||||||
|
// [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<Position>;
|
||||||
|
|
||||||
|
// [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<boolean>;
|
||||||
|
|
||||||
|
// [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<boolean>;
|
||||||
|
|
||||||
|
// [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<boolean>;
|
||||||
|
|
||||||
|
// [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<Screen[]>;
|
||||||
|
|
||||||
|
// [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<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [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<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [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
|
238
World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.js
Normal file
238
World-SEPPJM-Client/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -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);
|
||||||
|
}
|
37
World-SEPPJM-Client/go.mod
Normal file
37
World-SEPPJM-Client/go.mod
Normal file
@@ -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
|
81
World-SEPPJM-Client/go.sum
Normal file
81
World-SEPPJM-Client/go.sum
Normal file
@@ -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=
|
34
World-SEPPJM-Client/main.go
Normal file
34
World-SEPPJM-Client/main.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
13
World-SEPPJM-Client/wails.json
Normal file
13
World-SEPPJM-Client/wails.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user