ui calculator
This commit is contained in:
@@ -1,34 +1,229 @@
|
|||||||
# Galaxy
|
# Services Architecture
|
||||||
|
|
||||||
Galaxy is a turn-based strategy game which took place in space.
|
Galaxy Plus: Turn-based Strategy Game
|
||||||
|
|
||||||
At the highest level Game has a Backend service and an UI Client.
|
## Purpose
|
||||||
|
|
||||||
## Backend service
|
This document fixes the high-level service architecture of the system.
|
||||||
|
It is the starting point for implementing the external edge layer, authentication/session management, business services, and push delivery.
|
||||||
|
|
||||||
Backend service is presented by several "microservices" with a different set of responsibilities.
|
## Main Principles
|
||||||
|
|
||||||
- AuthN Proxy Service: handles all incoming requests,
|
- The system exposes a single external entry point: **Edge Gateway**.
|
||||||
immediately rejects not-authenticated requests and passes authenticated request to the next service.
|
- Internal business services are **not reachable directly from outside**.
|
||||||
|
- Any external command, except public auth commands, must be authenticated before it is routed further.
|
||||||
|
- Gateway handles only edge concerns. Business validation and domain rules remain inside business services.
|
||||||
|
- Push / long-polling delivery is also handled by the gateway.
|
||||||
|
|
||||||
- AuthZ Service: checks permissions avaliable to the authenticated user and passes request to the next service
|
## Main Components
|
||||||
if user is authorized to use specific game api.
|
|
||||||
|
|
||||||
- Games Orchestrator Service:
|
### 1. Edge Gateway
|
||||||
- finds appropriate Game Server according to Client's request data,
|
|
||||||
- passes user's commands to the selected Game Server,
|
|
||||||
- reads response and bounces it back up to request chain to the Client,
|
|
||||||
- manages Game Server state for health monitoring and making next turn.
|
|
||||||
|
|
||||||
- Game Servers: several instances of ongoing games with avaliable unprotected API ready to receive Player's and Administrator's commands.
|
The gateway is the only public entry point for client traffic.
|
||||||
|
|
||||||
## UI Client
|
Responsibilities:
|
||||||
|
|
||||||
UI Client is capable of:
|
- transport parsing
|
||||||
|
- authentication of external requests
|
||||||
|
- transport integrity checks
|
||||||
|
- session cache lookup
|
||||||
|
- request signature verification
|
||||||
|
- timestamp window verification
|
||||||
|
- anti-replay checks
|
||||||
|
- rate limiting and abuse protection
|
||||||
|
- command routing
|
||||||
|
- basic policy enforcement
|
||||||
|
- long-polling / push connection handling
|
||||||
|
- delivery of client-facing events from pub/sub
|
||||||
|
|
||||||
- Register a new player and login for an existing player using only e-mail and one-time codes,
|
The gateway must not implement domain-specific business logic.
|
||||||
- Enlist to a new Game from available onboard Games list,
|
|
||||||
- Request list of Games in which Player participating,
|
### 2. Auth / Session Service
|
||||||
- Request, store and display particular Game data,
|
|
||||||
- Use push-like mechanism for receiving asynchronous updates from Server,
|
This service owns authentication and device session lifecycle.
|
||||||
- Offline mode when no internet connection is available or user desired to work offline.
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- `send_email_code`
|
||||||
|
- `confirm_email_code`
|
||||||
|
- device session creation
|
||||||
|
- public key registration for device sessions
|
||||||
|
- session revoke / logout
|
||||||
|
- persistence of session state
|
||||||
|
- publishing session state changes for cache invalidation/update
|
||||||
|
|
||||||
|
This service is the source of truth for `device_session` state.
|
||||||
|
|
||||||
|
### 3. Session Store
|
||||||
|
|
||||||
|
Persistent storage for device sessions.
|
||||||
|
|
||||||
|
Typical fields:
|
||||||
|
|
||||||
|
- `device_session_id`
|
||||||
|
- `user_id`
|
||||||
|
- client public key
|
||||||
|
- session status
|
||||||
|
- creation / revoke timestamps
|
||||||
|
- optional client metadata
|
||||||
|
|
||||||
|
### 4. Session Cache
|
||||||
|
|
||||||
|
Fast lookup cache used by the gateway.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- resolve `device_session_id -> user_id + public_key + status`
|
||||||
|
- avoid synchronous calls from gateway to auth/session service on every request
|
||||||
|
|
||||||
|
Cache updates should be driven by session lifecycle events and may also use TTL.
|
||||||
|
|
||||||
|
### 5. Anti-Replay Store
|
||||||
|
|
||||||
|
Edge-level storage for recently seen transport `request_id` values.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- reject replayed authenticated transport messages within the allowed time window
|
||||||
|
|
||||||
|
This is transport-level replay protection, not business idempotency.
|
||||||
|
|
||||||
|
### 6. Rate Limit Store
|
||||||
|
|
||||||
|
Shared state for edge-level throttling and abuse control.
|
||||||
|
|
||||||
|
Typical dimensions:
|
||||||
|
|
||||||
|
- IP / network
|
||||||
|
- `device_session_id`
|
||||||
|
- `user_id`
|
||||||
|
- command class
|
||||||
|
|
||||||
|
### 7. Business Services
|
||||||
|
|
||||||
|
Internal services that process authenticated commands.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- business validation
|
||||||
|
- authorization by `user_id`
|
||||||
|
- ownership checks
|
||||||
|
- domain invariants
|
||||||
|
- state transitions
|
||||||
|
- business idempotency where required
|
||||||
|
- publishing domain events and/or client-facing events
|
||||||
|
|
||||||
|
Business services do not verify external signatures and do not access external clients directly.
|
||||||
|
|
||||||
|
### 8. Event Bus / Pub-Sub
|
||||||
|
|
||||||
|
Used for internal event distribution.
|
||||||
|
|
||||||
|
Purposes:
|
||||||
|
|
||||||
|
- session cache invalidation/update
|
||||||
|
- client-facing event delivery through the gateway
|
||||||
|
- optional internal domain event propagation between services
|
||||||
|
|
||||||
|
## External Flows
|
||||||
|
|
||||||
|
### Public Auth Flow
|
||||||
|
|
||||||
|
These commands are public and do not require an existing device session:
|
||||||
|
|
||||||
|
- `send_email_code`
|
||||||
|
- `confirm_email_code`
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. client sends public auth command to gateway
|
||||||
|
2. gateway applies public-edge checks (format, rate limits, abuse policy)
|
||||||
|
3. gateway routes command to auth/session service
|
||||||
|
4. auth/session service performs auth logic
|
||||||
|
5. if login is confirmed, auth/session service creates `device_session`
|
||||||
|
6. auth/session service publishes cache update/invalidation event
|
||||||
|
|
||||||
|
### Authenticated Command Flow
|
||||||
|
|
||||||
|
All other external commands require authentication.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. client sends authenticated request to gateway
|
||||||
|
2. gateway validates transport envelope presence and protocol version
|
||||||
|
3. gateway resolves `device_session_id` through session cache
|
||||||
|
4. gateway rejects unknown or revoked sessions
|
||||||
|
5. gateway verifies request signature
|
||||||
|
6. gateway verifies timestamp window
|
||||||
|
7. gateway verifies anti-replay constraints
|
||||||
|
8. gateway applies rate limits and basic policy checks
|
||||||
|
9. gateway extracts authenticated context, including `user_id`
|
||||||
|
10. gateway routes the request to the target business service based on `command_type`
|
||||||
|
|
||||||
|
No business service should receive an unauthenticated external request.
|
||||||
|
|
||||||
|
### Push / Long-Polling Flow
|
||||||
|
|
||||||
|
The gateway owns external push / long-polling connections.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. client opens authenticated push / long-polling connection through gateway
|
||||||
|
2. gateway binds connection to `user_id` and `device_session_id`
|
||||||
|
3. gateway may send current server time for clock offset calculation
|
||||||
|
4. internal services publish client-facing events to pub/sub
|
||||||
|
5. gateway consumes those events and delivers them to the proper client connections
|
||||||
|
|
||||||
|
Gateway is a delivery layer, not the source of business events.
|
||||||
|
|
||||||
|
## Internal Contract Between Gateway and Business Services
|
||||||
|
|
||||||
|
Business services should receive an internal authenticated command, not raw external transport data.
|
||||||
|
|
||||||
|
Typical internal authenticated context:
|
||||||
|
|
||||||
|
- `user_id`
|
||||||
|
- `device_session_id`
|
||||||
|
- `command_type`
|
||||||
|
- verified payload bytes
|
||||||
|
- transport `request_id`
|
||||||
|
- optional command id / trace id
|
||||||
|
- optional client metadata relevant for logging
|
||||||
|
|
||||||
|
Business services must trust only the gateway as their external ingress.
|
||||||
|
|
||||||
|
## Separation of Responsibilities
|
||||||
|
|
||||||
|
### Gateway is responsible for
|
||||||
|
|
||||||
|
- who sent the request
|
||||||
|
- whether transport integrity is valid
|
||||||
|
- whether the request is fresh
|
||||||
|
- whether replay is detected
|
||||||
|
- whether request volume is acceptable
|
||||||
|
- where to route the request
|
||||||
|
|
||||||
|
### Business services are responsible for
|
||||||
|
|
||||||
|
- whether the user is allowed to perform the business action
|
||||||
|
- whether the target object belongs to the user
|
||||||
|
- whether the domain state transition is valid
|
||||||
|
- whether business idempotency rules are satisfied
|
||||||
|
|
||||||
|
## Revocation Behavior
|
||||||
|
|
||||||
|
When a device session is revoked:
|
||||||
|
|
||||||
|
- auth/session service updates the source of truth
|
||||||
|
- auth/session service publishes revoke/invalidation event
|
||||||
|
- gateway updates or invalidates session cache
|
||||||
|
- gateway rejects further requests for that session
|
||||||
|
- gateway closes active push / long-polling connections bound to that session, if applicable
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
The gateway is not a place for full domain authorization logic.
|
||||||
|
It must not become a business “god service”.
|
||||||
|
|
||||||
|
The auth/session service is not the hot path for every authenticated request.
|
||||||
|
The gateway should authenticate most requests from cache, not by synchronous round-trips.
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Secure Exchange Architecture
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document fixes the transport-level secure exchange model between client and server.
|
||||||
|
It is the starting point for implementing authenticated device sessions, signed requests/responses, and anti-replay protection.
|
||||||
|
|
||||||
|
## Main Principles
|
||||||
|
|
||||||
|
- No browser cookies are used.
|
||||||
|
- Authentication is device-session based.
|
||||||
|
- Each device/session is unique and independently revocable.
|
||||||
|
- There are no short-lived access tokens or refresh-token flows in the main design.
|
||||||
|
- Requests are authenticated by client-side signatures.
|
||||||
|
- Responses are authenticated by server-side signatures.
|
||||||
|
- Transport integrity and freshness are verified before payload is processed.
|
||||||
|
|
||||||
|
## Device Session Model
|
||||||
|
|
||||||
|
After successful login through e-mail code:
|
||||||
|
|
||||||
|
1. client generates an asymmetric key pair
|
||||||
|
2. private key remains on the client device
|
||||||
|
3. public key is registered on the server
|
||||||
|
4. server creates a persistent `device_session`
|
||||||
|
5. client stores:
|
||||||
|
- `device_session_id`
|
||||||
|
- private key
|
||||||
|
|
||||||
|
The server stores at least:
|
||||||
|
|
||||||
|
- `device_session_id`
|
||||||
|
- `user_id`
|
||||||
|
- client public key
|
||||||
|
- session status
|
||||||
|
- revoke metadata
|
||||||
|
|
||||||
|
## Key Storage
|
||||||
|
|
||||||
|
### Native Clients
|
||||||
|
|
||||||
|
Private key should be stored in platform secure storage.
|
||||||
|
|
||||||
|
### Browser / WASM Clients
|
||||||
|
|
||||||
|
Private key should be created and used through WebCrypto.
|
||||||
|
Non-exportable key storage is preferred.
|
||||||
|
Loss of browser storage is acceptable and means re-login is required.
|
||||||
|
|
||||||
|
## Request Structure
|
||||||
|
|
||||||
|
Each authenticated request logically contains:
|
||||||
|
|
||||||
|
- `payload_bytes`
|
||||||
|
- `request_envelope`
|
||||||
|
- `signature`
|
||||||
|
|
||||||
|
### Request Envelope
|
||||||
|
|
||||||
|
Minimal required fields:
|
||||||
|
|
||||||
|
- `protocol_version`
|
||||||
|
- `device_session_id`
|
||||||
|
- `message_type`
|
||||||
|
- `timestamp_ms`
|
||||||
|
- `request_id`
|
||||||
|
- `payload_hash`
|
||||||
|
|
||||||
|
### Request Signing Input
|
||||||
|
|
||||||
|
The client signs canonical bytes built from:
|
||||||
|
|
||||||
|
- request domain marker, for example `myapp-request-v1`
|
||||||
|
- `protocol_version`
|
||||||
|
- `device_session_id`
|
||||||
|
- `message_type`
|
||||||
|
- `timestamp_ms`
|
||||||
|
- `request_id`
|
||||||
|
- `payload_hash`
|
||||||
|
|
||||||
|
`payload_hash` should be computed from raw `payload_bytes`.
|
||||||
|
|
||||||
|
The goal is to bind the signature to:
|
||||||
|
|
||||||
|
- the concrete device session
|
||||||
|
- the concrete message type
|
||||||
|
- the concrete payload
|
||||||
|
- a fresh request instance
|
||||||
|
|
||||||
|
## Response Structure
|
||||||
|
|
||||||
|
Each server response logically contains:
|
||||||
|
|
||||||
|
- `payload_bytes`
|
||||||
|
- `response_envelope`
|
||||||
|
- `signature`
|
||||||
|
|
||||||
|
### Response Envelope
|
||||||
|
|
||||||
|
Minimal required fields:
|
||||||
|
|
||||||
|
- `protocol_version`
|
||||||
|
- `request_id`
|
||||||
|
- `timestamp_ms`
|
||||||
|
- `result_code`
|
||||||
|
- `payload_hash`
|
||||||
|
|
||||||
|
### Response Signing Input
|
||||||
|
|
||||||
|
The server signs canonical bytes built from:
|
||||||
|
|
||||||
|
- response domain marker, for example `myapp-response-v1`
|
||||||
|
- `protocol_version`
|
||||||
|
- `request_id`
|
||||||
|
- `timestamp_ms`
|
||||||
|
- `result_code`
|
||||||
|
- `payload_hash`
|
||||||
|
|
||||||
|
The client verifies the signature using a trusted server public key.
|
||||||
|
|
||||||
|
## Verification Order on Server
|
||||||
|
|
||||||
|
Before processing payload, the server/gateway must:
|
||||||
|
|
||||||
|
1. verify that the transport envelope is present and supported
|
||||||
|
2. resolve `device_session_id`
|
||||||
|
3. reject unknown or revoked sessions
|
||||||
|
4. verify client signature using stored public key
|
||||||
|
5. verify timestamp freshness window
|
||||||
|
6. verify anti-replay constraints using `request_id`
|
||||||
|
7. only then pass payload to business processing
|
||||||
|
|
||||||
|
## Verification Order on Client
|
||||||
|
|
||||||
|
Before accepting response payload, the client must:
|
||||||
|
|
||||||
|
1. verify server signature
|
||||||
|
2. verify `request_id` matches the corresponding request
|
||||||
|
3. verify `payload_hash`
|
||||||
|
4. verify timestamp freshness if applicable
|
||||||
|
5. only then accept the response payload
|
||||||
|
|
||||||
|
## Anti-Replay Model
|
||||||
|
|
||||||
|
Transport anti-replay uses:
|
||||||
|
|
||||||
|
- `timestamp_ms`
|
||||||
|
- `request_id`
|
||||||
|
|
||||||
|
The server accepts requests only inside an allowed time window.
|
||||||
|
Recently seen `request_id` values must be tracked for the corresponding session and rejected on reuse.
|
||||||
|
|
||||||
|
This protects transport freshness.
|
||||||
|
It does not replace business idempotency.
|
||||||
|
|
||||||
|
## Server Time Offset
|
||||||
|
|
||||||
|
Clients use server time offset instead of trusting local clock directly.
|
||||||
|
|
||||||
|
Expected approach:
|
||||||
|
|
||||||
|
- client establishes authenticated long-polling / push connection
|
||||||
|
- server provides current server time
|
||||||
|
- client computes local offset
|
||||||
|
- subsequent signed requests use adjusted time
|
||||||
|
|
||||||
|
No extra sync request is required if push / long-polling already exists.
|
||||||
|
|
||||||
|
## TLS and MITM Considerations
|
||||||
|
|
||||||
|
### Native Clients notes
|
||||||
|
|
||||||
|
Native clients should use TLS pinning in addition to signed request/response exchange.
|
||||||
|
Pinning should be based on public key / SPKI rather than leaf certificate whenever possible.
|
||||||
|
|
||||||
|
### Browser / WASM Clients notes
|
||||||
|
|
||||||
|
Real TLS pinning is not available in the browser in the same way as in native clients.
|
||||||
|
Browser clients still use the signed request/response model, but browser-managed TLS remains the platform limitation.
|
||||||
|
|
||||||
|
## Threat Model Boundaries
|
||||||
|
|
||||||
|
This design protects against:
|
||||||
|
|
||||||
|
- request/response tampering in transit
|
||||||
|
- replay of previously seen transport messages inside the protected window
|
||||||
|
- use of unknown or revoked device sessions
|
||||||
|
- forged server responses without server signing key
|
||||||
|
- forged client requests without client signing key
|
||||||
|
|
||||||
|
This design does not guarantee that a legitimate user cannot generate their own valid requests from their own client environment.
|
||||||
|
That is handled by server-side business validation and authorization.
|
||||||
|
|
||||||
|
## Architectural Notes
|
||||||
|
|
||||||
|
- Transport authentication and business authorization are separate concerns.
|
||||||
|
- Signed transport proves message origin and integrity.
|
||||||
|
- Business services must still validate command correctness, ownership, permissions, and state transitions.
|
||||||
|
- Transport `request_id` is not the same as business idempotency key.
|
||||||
|
|
||||||
|
## Recommended Outcome
|
||||||
|
|
||||||
|
The system should treat the secure exchange layer as the mandatory outer contract for all authenticated traffic.
|
||||||
|
Only after successful transport validation may payload be routed to business logic.
|
||||||
+7
-19
@@ -1,22 +1,10 @@
|
|||||||
# Client for Galaxy Plus
|
# Client for Galaxy Plus
|
||||||
|
|
||||||
## Ship Calculator
|
UI Client is capable of:
|
||||||
|
|
||||||
```text
|
- Register a new player and login for an existing player using only e-mail and one-time codes,
|
||||||
Class: [ ] { Create }
|
- Enlist to a new Game from available onboard Games list,
|
||||||
Drives: [20.000] x 1.013 [O--]
|
- Request list of Games in which Player participating,
|
||||||
Weapons: [ 0.000] x [1.000] [==0]
|
- Request, store and display particular Game data,
|
||||||
Armament: [ 0 ]
|
- Use push-like mechanism for receiving asynchronous updates from Server,
|
||||||
Schields: [ 5.500] @ [1.123]
|
- Offline mode when no internet connection is available or user desired to work offline.
|
||||||
Cargo: [30.125] @ [1.320]
|
|
||||||
|
|
||||||
Mass: [ 123,45 ] [==0]
|
|
||||||
Speed: ( 12,456 ) [O--]
|
|
||||||
Attack: 0
|
|
||||||
Defense: 100,0
|
|
||||||
|
|
||||||
Planet { Name } Production:
|
|
||||||
[ 100.0 ] MAT per turn produced [O--] supplied
|
|
||||||
{ N.000 } ship(s) per turn
|
|
||||||
{ M.000 } turn(s) per ship
|
|
||||||
```
|
|
||||||
|
|||||||
+50
-3
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gerr "galaxy/error"
|
gerr "galaxy/error"
|
||||||
@@ -9,10 +10,11 @@ import (
|
|||||||
var (
|
var (
|
||||||
checkConnectionInterval = 5 * time.Second
|
checkConnectionInterval = 5 * time.Second
|
||||||
checkVersionInterval = time.Hour
|
checkVersionInterval = time.Hour
|
||||||
|
statePersistInterval = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *client) startBackground() {
|
func (e *client) startBackground() {
|
||||||
if e.fullConnector == nil && e.updater == nil {
|
if e.conn == nil || e.updater == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,9 +30,11 @@ func (e *client) stopBackground() {
|
|||||||
func (e *client) backgroundLoop() {
|
func (e *client) backgroundLoop() {
|
||||||
checkConnTimer := time.NewTimer(checkConnectionInterval)
|
checkConnTimer := time.NewTimer(checkConnectionInterval)
|
||||||
checkVersionTimer := time.NewTimer(checkVersionInterval)
|
checkVersionTimer := time.NewTimer(checkVersionInterval)
|
||||||
|
persistStateTimer := time.NewTimer(statePersistInterval)
|
||||||
defer func() {
|
defer func() {
|
||||||
checkConnTimer.Stop()
|
checkConnTimer.Stop()
|
||||||
checkVersionTimer.Stop()
|
checkVersionTimer.Stop()
|
||||||
|
persistStateTimer.Stop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -38,8 +42,8 @@ func (e *client) backgroundLoop() {
|
|||||||
case <-e.backgroundStop:
|
case <-e.backgroundStop:
|
||||||
return
|
return
|
||||||
case <-checkConnTimer.C:
|
case <-checkConnTimer.C:
|
||||||
if e.fullConnector != nil {
|
if e.conn != nil {
|
||||||
e.OnConnection(e.fullConnector.CheckConnection())
|
e.OnConnection(e.conn.CheckConnection())
|
||||||
}
|
}
|
||||||
checkConnTimer.Reset(checkConnectionInterval)
|
checkConnTimer.Reset(checkConnectionInterval)
|
||||||
case <-checkVersionTimer.C:
|
case <-checkVersionTimer.C:
|
||||||
@@ -49,15 +53,58 @@ func (e *client) backgroundLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkVersionTimer.Reset(checkVersionInterval)
|
checkVersionTimer.Reset(checkVersionInterval)
|
||||||
|
case <-persistStateTimer.C:
|
||||||
|
e.ensureStatePersist()
|
||||||
|
persistStateTimer.Reset(statePersistInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *client) ensureStatePersist() {
|
||||||
|
param := e.GetParams()
|
||||||
|
needSaving := false
|
||||||
|
e.stateMu.Lock()
|
||||||
|
if e.world != nil {
|
||||||
|
if param.CameraZoom > 0 && param.CameraZoom != e.state.CameraZoom {
|
||||||
|
e.state.CameraZoom = param.CameraZoom
|
||||||
|
needSaving = true
|
||||||
|
}
|
||||||
|
if param.CameraXWorldFp != e.state.CameraXFp {
|
||||||
|
e.state.CameraXFp = param.CameraXWorldFp
|
||||||
|
needSaving = true
|
||||||
|
}
|
||||||
|
if param.CameraYWorldFp != e.state.CameraYFp {
|
||||||
|
e.state.CameraYFp = param.CameraYWorldFp
|
||||||
|
needSaving = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.mapSplitter != nil && e.mapSplitter.Offset != e.state.MapSplitterOffset {
|
||||||
|
e.state.MapSplitterOffset = e.mapSplitter.Offset
|
||||||
|
needSaving = true
|
||||||
|
}
|
||||||
|
if e.accInfo.Open != e.state.AccordionInfoOpen {
|
||||||
|
e.state.AccordionInfoOpen = e.accInfo.Open
|
||||||
|
needSaving = true
|
||||||
|
}
|
||||||
|
if e.accCalc.Open != e.state.AccordionCalcOpen {
|
||||||
|
e.state.AccordionCalcOpen = e.accCalc.Open
|
||||||
|
needSaving = true
|
||||||
|
}
|
||||||
|
if needSaving {
|
||||||
|
if err := e.s.SaveState(*e.state); err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.stateMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (e *client) handlerError(err error) {
|
func (e *client) handlerError(err error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ERROR: %s\n", err)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case gerr.IsConnection(err):
|
case gerr.IsConnection(err):
|
||||||
e.OnConnectionError(err)
|
e.OnConnectionError(err)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
type interactiveRaster struct {
|
type interactiveRaster struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
edit *client
|
|
||||||
min fyne.Size
|
min fyne.Size
|
||||||
raster *canvas.Raster
|
raster *canvas.Raster
|
||||||
onLayout func(fyne.Size)
|
onLayout func(fyne.Size)
|
||||||
@@ -50,7 +49,6 @@ func (r *interactiveRaster) Tapped(ev *fyne.PointEvent) {
|
|||||||
func (r *interactiveRaster) TappedSecondary(*fyne.PointEvent) {}
|
func (r *interactiveRaster) TappedSecondary(*fyne.PointEvent) {}
|
||||||
|
|
||||||
func newInteractiveRaster(
|
func newInteractiveRaster(
|
||||||
edit *client,
|
|
||||||
raster *canvas.Raster,
|
raster *canvas.Raster,
|
||||||
onLayout func(fyne.Size),
|
onLayout func(fyne.Size),
|
||||||
onScrolled func(*fyne.ScrollEvent),
|
onScrolled func(*fyne.ScrollEvent),
|
||||||
@@ -60,7 +58,6 @@ func newInteractiveRaster(
|
|||||||
) *interactiveRaster {
|
) *interactiveRaster {
|
||||||
r := &interactiveRaster{
|
r := &interactiveRaster{
|
||||||
raster: raster,
|
raster: raster,
|
||||||
edit: edit,
|
|
||||||
onLayout: onLayout,
|
onLayout: onLayout,
|
||||||
onScrolled: onScrolled,
|
onScrolled: onScrolled,
|
||||||
onDragged: onDragged,
|
onDragged: onDragged,
|
||||||
|
|||||||
+92
-55
@@ -5,15 +5,16 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"galaxy/client/updater"
|
"galaxy/client/updater"
|
||||||
|
"galaxy/client/widget/calculator"
|
||||||
"galaxy/client/world"
|
"galaxy/client/world"
|
||||||
"galaxy/connector"
|
"galaxy/connector"
|
||||||
mc "galaxy/model/client"
|
mc "galaxy/model/client"
|
||||||
"galaxy/model/report"
|
|
||||||
"galaxy/storage"
|
"galaxy/storage"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
@@ -21,12 +22,22 @@ import (
|
|||||||
const version = "1.0.0"
|
const version = "1.0.0"
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
s storage.UIStorage
|
s storage.Storage
|
||||||
conn connector.UIConnector
|
conn connector.Connector
|
||||||
app fyne.App
|
app fyne.App
|
||||||
window fyne.Window
|
window fyne.Window
|
||||||
|
|
||||||
loadReportFunc func(uint)
|
state *mc.State
|
||||||
|
stateMu sync.RWMutex
|
||||||
|
|
||||||
|
reg *registry
|
||||||
|
|
||||||
|
calculator *calculator.Calculator
|
||||||
|
mapSplitter *container.Split
|
||||||
|
accInfo *widget.AccordionItem
|
||||||
|
accCalc *widget.AccordionItem
|
||||||
|
|
||||||
|
// loadReportFunc func(uint)
|
||||||
|
|
||||||
world *world.World
|
world *world.World
|
||||||
drawer *world.GGDrawer
|
drawer *world.GGDrawer
|
||||||
@@ -69,8 +80,6 @@ type client struct {
|
|||||||
|
|
||||||
hits []world.Hit
|
hits []world.Hit
|
||||||
|
|
||||||
fullStorage storage.Storage
|
|
||||||
fullConnector connector.Connector
|
|
||||||
updater *updater.Manager
|
updater *updater.Manager
|
||||||
backgroundStop chan struct{}
|
backgroundStop chan struct{}
|
||||||
backgroundOnce sync.Once
|
backgroundOnce sync.Once
|
||||||
@@ -81,32 +90,55 @@ type client struct {
|
|||||||
onServiceErrFn func(error)
|
onServiceErrFn func(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(s storage.UIStorage, conn connector.UIConnector, app fyne.App) (mc.Client, error) {
|
func NewClient(s storage.Storage, conn connector.Connector, app fyne.App) (mc.Client, error) {
|
||||||
e := &client{
|
e := &client{
|
||||||
s: s,
|
s: s,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
app: app,
|
app: app,
|
||||||
window: app.NewWindow("Galaxy Plus"),
|
window: app.NewWindow("Galaxy Plus"),
|
||||||
world: nil,
|
reg: newRegistry(),
|
||||||
wp: &world.RenderParams{
|
|
||||||
CameraZoom: 1.0,
|
|
||||||
Options: &world.RenderOptions{DisableWrapScroll: false},
|
|
||||||
},
|
|
||||||
lastCanvasScale: 1.0,
|
lastCanvasScale: 1.0,
|
||||||
|
world: nil,
|
||||||
hits: make([]world.Hit, 5),
|
hits: make([]world.Hit, 5),
|
||||||
backgroundStop: make(chan struct{}),
|
backgroundStop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
if fullStorage, ok := s.(storage.Storage); ok {
|
e.calculator = calculator.NewCaclulator(calculator.WithCreateHandler(e.createShipClass))
|
||||||
e.fullStorage = fullStorage
|
e.updater = updater.NewManager(e.s, e.conn)
|
||||||
}
|
|
||||||
if fullConnector, ok := conn.(connector.Connector); ok {
|
|
||||||
e.fullConnector = fullConnector
|
|
||||||
}
|
|
||||||
if e.fullStorage != nil && e.fullConnector != nil {
|
|
||||||
e.updater = updater.NewManager(e.fullStorage, e.fullConnector)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.loadReportFunc = e.loadReport
|
stateExists, err := e.s.StateExists()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if stateExists {
|
||||||
|
state, err := e.s.LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.state = &state
|
||||||
|
} else {
|
||||||
|
e.state = &mc.State{
|
||||||
|
ClientCurrentVersion: e.Version(),
|
||||||
|
CameraZoom: 1.0,
|
||||||
|
MapSplitterOffset: 0.5,
|
||||||
|
AccordionInfoOpen: false,
|
||||||
|
AccordionCalcOpen: false,
|
||||||
|
}
|
||||||
|
if err := e.s.SaveState(*e.state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.state.CameraZoom <= 0 {
|
||||||
|
e.state.CameraZoom = 1.0
|
||||||
|
}
|
||||||
|
if e.state.MapSplitterOffset <= 0 {
|
||||||
|
e.state.MapSplitterOffset = 0.5
|
||||||
|
}
|
||||||
|
e.wp = &world.RenderParams{
|
||||||
|
Options: &world.RenderOptions{DisableWrapScroll: false},
|
||||||
|
CameraZoom: e.state.CameraZoom,
|
||||||
|
CameraXWorldFp: e.state.CameraXFp,
|
||||||
|
CameraYWorldFp: e.state.CameraYFp,
|
||||||
|
}
|
||||||
|
|
||||||
e.drawer = &world.GGDrawer{DC: nil}
|
e.drawer = &world.GGDrawer{DC: nil}
|
||||||
|
|
||||||
@@ -127,40 +159,13 @@ func NewClient(s storage.UIStorage, conn connector.UIConnector, app fyne.App) (m
|
|||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *client) loadReport(t uint) {
|
|
||||||
e.conn.FetchReport("GAME_ID", t, func(r report.Report, err error) {
|
|
||||||
if err != nil {
|
|
||||||
e.handlerError(err)
|
|
||||||
} else {
|
|
||||||
e.setReport(r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *client) setReport(r report.Report) {
|
|
||||||
w := world.NewWorld(int(r.Width), int(r.Height))
|
|
||||||
for i := range r.LocalPlanet {
|
|
||||||
p := r.LocalPlanet[i]
|
|
||||||
w.AddCircle(p.X.F(), p.Y.F(), p.Size.F())
|
|
||||||
}
|
|
||||||
for i := range r.UnidentifiedPlanet {
|
|
||||||
p := r.UnidentifiedPlanet[i]
|
|
||||||
w.AddPoint(p.X.F(), p.Y.F())
|
|
||||||
}
|
|
||||||
e.loadWorld(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *client) BuildUI(w fyne.Window) {
|
func (e *client) BuildUI(w fyne.Window) {
|
||||||
mapCanvas := newInteractiveRaster(e, e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
|
mapCanvasObject := newInteractiveRaster(e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
|
||||||
mapCanvas.SetMinSize(fyne.NewSize(640, 480))
|
|
||||||
|
|
||||||
toolbar := widget.NewToolbar(
|
toolbar := widget.NewToolbar(
|
||||||
widget.NewToolbarAction(
|
widget.NewToolbarAction(
|
||||||
theme.FolderIcon(),
|
theme.FolderIcon(),
|
||||||
func() {
|
func() { e.initReportAsync("GAME_ID", 0) }),
|
||||||
e.loadReport(0)
|
|
||||||
// e.loadWorld(mockWorld())
|
|
||||||
}),
|
|
||||||
widget.NewToolbarSeparator(),
|
widget.NewToolbarSeparator(),
|
||||||
widget.NewToolbarAction(
|
widget.NewToolbarAction(
|
||||||
theme.NavigateBackIcon(),
|
theme.NavigateBackIcon(),
|
||||||
@@ -170,11 +175,24 @@ func (e *client) BuildUI(w fyne.Window) {
|
|||||||
func() {}),
|
func() {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
e.accInfo = widget.NewAccordionItem(lang.L("title.info"), container.NewStack())
|
||||||
|
e.accInfo.Open = e.state.AccordionInfoOpen
|
||||||
|
e.accCalc = widget.NewAccordionItem(lang.L("title.calculator"), e.calculator.CanvasObject)
|
||||||
|
e.accCalc.Open = e.state.AccordionCalcOpen
|
||||||
|
|
||||||
|
accordion := widget.NewAccordion()
|
||||||
|
accordion.MultiOpen = true
|
||||||
|
accordion.Append(e.accCalc)
|
||||||
|
accordion.Append(e.accInfo)
|
||||||
|
|
||||||
|
e.mapSplitter = container.NewHSplit(mapCanvasObject, container.NewHScroll(accordion))
|
||||||
|
e.mapSplitter.SetOffset(e.state.MapSplitterOffset)
|
||||||
|
|
||||||
tabs := container.NewAppTabs(
|
tabs := container.NewAppTabs(
|
||||||
container.NewTabItemWithIcon(
|
container.NewTabItemWithIcon(
|
||||||
"Map",
|
lang.L("title.map"),
|
||||||
theme.GridIcon(),
|
theme.GridIcon(),
|
||||||
mapCanvas),
|
e.mapSplitter),
|
||||||
container.NewTabItemWithIcon(
|
container.NewTabItemWithIcon(
|
||||||
"Calculator",
|
"Calculator",
|
||||||
theme.ComputerIcon(),
|
theme.ComputerIcon(),
|
||||||
@@ -182,9 +200,23 @@ func (e *client) BuildUI(w fyne.Window) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
th := tabs.Theme()
|
||||||
|
icon := canvas.NewImageFromResource(th.Icon(theme.IconNameInfo))
|
||||||
|
|
||||||
|
statusLeft := widget.NewTextGridFromString("Status")
|
||||||
|
statusAd := widget.NewTextGridFromString("")
|
||||||
|
|
||||||
|
statusBar := container.NewBorder(
|
||||||
|
nil, // top
|
||||||
|
nil, // bottom
|
||||||
|
container.NewHBox(statusLeft, widget.NewSeparator()), // left
|
||||||
|
container.NewHBox(widget.NewSeparator(), icon), // right
|
||||||
|
statusAd, // center
|
||||||
|
)
|
||||||
|
|
||||||
content := container.NewBorder(
|
content := container.NewBorder(
|
||||||
toolbar, // top
|
toolbar, // top
|
||||||
nil, // bottom
|
statusBar, // bottom
|
||||||
nil, // left
|
nil, // left
|
||||||
nil, // right
|
nil, // right
|
||||||
tabs, // center
|
tabs, // center
|
||||||
@@ -192,6 +224,9 @@ func (e *client) BuildUI(w fyne.Window) {
|
|||||||
|
|
||||||
w.CenterOnScreen()
|
w.CenterOnScreen()
|
||||||
w.SetContent(content)
|
w.SetContent(content)
|
||||||
|
s := statusBar.Size()
|
||||||
|
icon.SetMinSize(fyne.NewSize(s.Height, s.Height))
|
||||||
|
e.initLatestReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *client) loadWorld(w *world.World) {
|
func (e *client) loadWorld(w *world.World) {
|
||||||
@@ -215,16 +250,18 @@ func (e *client) Run() error {
|
|||||||
e.window.SetMaster()
|
e.window.SetMaster()
|
||||||
e.window.Resize(fyne.NewSize(800, 600))
|
e.window.Resize(fyne.NewSize(800, 600))
|
||||||
e.window.CenterOnScreen()
|
e.window.CenterOnScreen()
|
||||||
|
e.window.SetOnClosed(e.Shutdown)
|
||||||
e.window.ShowAndRun()
|
e.window.ShowAndRun()
|
||||||
e.stopBackground()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *client) Shutdown() {
|
func (e *client) Shutdown() {
|
||||||
e.stopBackground()
|
e.stopBackground()
|
||||||
|
e.ensureStatePersist()
|
||||||
e.window.Close()
|
e.window.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove func?
|
||||||
func (e *client) Version() string { return version }
|
func (e *client) Version() string { return version }
|
||||||
|
|
||||||
func (e *client) OnConnection(isGood bool) {
|
func (e *client) OnConnection(isGood bool) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"galaxy/client"
|
||||||
"galaxy/client/appmeta"
|
"galaxy/client/appmeta"
|
||||||
"galaxy/client/loader"
|
"galaxy/client/loader"
|
||||||
"galaxy/connector/http"
|
"galaxy/connector/http"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -19,7 +21,7 @@ func main() {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = errors.Join(err, fmt.Errorf("app panics: %v", r))
|
err = errors.Join(err, fmt.Errorf("panic: %v", r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -32,6 +34,9 @@ func main() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
app := app.NewWithID(appmeta.AppID)
|
app := app.NewWithID(appmeta.AppID)
|
||||||
|
if err = lang.AddTranslationsFS(client.Translations, "resource/lang"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
s, err := fs.NewFS(app.Storage().RootURI().Path())
|
s, err := fs.NewFS(app.Storage().RootURI().Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -44,6 +49,5 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.Run(ctx)
|
err = l.Run(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"galaxy/client/appmeta"
|
|
||||||
"galaxy/client"
|
"galaxy/client"
|
||||||
|
"galaxy/client/appmeta"
|
||||||
"galaxy/connector/http"
|
"galaxy/connector/http"
|
||||||
"galaxy/storage/fs"
|
"galaxy/storage/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -19,7 +20,7 @@ func main() {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = errors.Join(err, fmt.Errorf("app panics: %v", r))
|
err = errors.Join(err, fmt.Errorf("panic: %v", r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -31,6 +32,9 @@ func main() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
app := app.NewWithID(appmeta.AppID)
|
app := app.NewWithID(appmeta.AppID)
|
||||||
|
if err = lang.AddTranslationsFS(client.Translations, "resource/lang"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
s, err := fs.NewFS(app.Storage().RootURI().Path())
|
s, err := fs.NewFS(app.Storage().RootURI().Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed resource/lang
|
||||||
|
var Translations embed.FS
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"galaxy/client/world"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var m = func(v int) float64 { return float64(v) / float64(world.SCALE) }
|
||||||
|
|
||||||
|
func (e *client) onTapped(ev *fyne.PointEvent) {
|
||||||
|
if e.world == nil || ev == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xPx, yPx, ok := e.eventPosToPixel(ev.Position.X, ev.Position.Y)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := e.getLastRenderedParams()
|
||||||
|
hits, err := e.world.HitTest(e.hits, ¶ms, xPx, yPx)
|
||||||
|
if err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hits) == 0 {
|
||||||
|
e.calculator.UnloadPlanet()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range hits {
|
||||||
|
e.onHit(hits[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) onHit(hit world.Hit) {
|
||||||
|
// var coord string
|
||||||
|
// if hit.Kind == world.KindLine {
|
||||||
|
// coord = fmt.Sprintf("{%f,%f - %f,%f}", m(hit.X1), m(hit.Y1), m(hit.X2), m(hit.Y2))
|
||||||
|
// } else {
|
||||||
|
// coord = fmt.Sprintf("{%f,%f}", m(hit.X), m(hit.Y))
|
||||||
|
// }
|
||||||
|
// fmt.Println("hit:", hit.ID, "Coord:", coord)
|
||||||
|
switch hit.Kind {
|
||||||
|
case world.KindPoint:
|
||||||
|
case world.KindCircle:
|
||||||
|
e.onHitCircle(hit.ID)
|
||||||
|
case world.KindLine:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) onHitCircle(id world.PrimitiveID) {
|
||||||
|
p, ok := e.reg.localPlanet(id)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.calculator.LoadPlanet(p.Name, p.Number, p.FreeIndustry.F(), p.Material.F(), p.Resources.F())
|
||||||
|
e.calculator.Refresh()
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"galaxy/client/world"
|
||||||
|
"galaxy/model/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
entityClassUnknown int = iota - 1
|
||||||
|
entityClassLocalPlanet
|
||||||
|
entityClassOthersPlanet
|
||||||
|
entityClassFreePlanet
|
||||||
|
entityClassUnidentifiedPlanet
|
||||||
|
)
|
||||||
|
|
||||||
|
type registry struct {
|
||||||
|
report *report.Report
|
||||||
|
localPlanetIndex map[world.PrimitiveID]int
|
||||||
|
unidentifiedPlanetIndex map[world.PrimitiveID]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRegistry() *registry {
|
||||||
|
return ®istry{
|
||||||
|
localPlanetIndex: make(map[world.PrimitiveID]int),
|
||||||
|
unidentifiedPlanetIndex: make(map[world.PrimitiveID]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) clear(report *report.Report) {
|
||||||
|
r.report = report
|
||||||
|
clear(r.localPlanetIndex)
|
||||||
|
clear(r.unidentifiedPlanetIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) entityClass(id world.PrimitiveID) int {
|
||||||
|
if r.isLocalPlanet(id) {
|
||||||
|
return entityClassLocalPlanet
|
||||||
|
}
|
||||||
|
if r.isUnidentifiedPlanet(id) {
|
||||||
|
return entityClassUnidentifiedPlanet
|
||||||
|
}
|
||||||
|
return entityClassUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) registerLocalPlanet(id world.PrimitiveID, index int) {
|
||||||
|
r.localPlanetIndex[id] = index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) isLocalPlanet(id world.PrimitiveID) bool {
|
||||||
|
_, ok := r.localPlanetIndex[id]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) localPlanet(id world.PrimitiveID) (*report.LocalPlanet, bool) {
|
||||||
|
i, ok := r.localPlanetIndex[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if i > len(r.report.LocalPlanet)-1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &r.report.LocalPlanet[i], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) registerUnidentifiedPlanet(id world.PrimitiveID, index int) {
|
||||||
|
r.unidentifiedPlanetIndex[id] = index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) isUnidentifiedPlanet(id world.PrimitiveID) bool {
|
||||||
|
_, ok := r.unidentifiedPlanetIndex[id]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) createShipClass(n string, D float64, A uint, W float64, S float64, C float64) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"galaxy/client/widget/calculator"
|
||||||
|
"galaxy/client/world"
|
||||||
|
mc "galaxy/model/client"
|
||||||
|
"galaxy/model/report"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *client) initLatestReport() {
|
||||||
|
e.stateMu.Lock()
|
||||||
|
if e.state.ActiveGameID != nil {
|
||||||
|
if stateIdx := slices.IndexFunc(e.state.GameState, func(gs mc.GameState) bool { return gs.ID == *e.state.ActiveGameID }); stateIdx >= 0 {
|
||||||
|
e.initReportAsync(*e.state.ActiveGameID, e.state.GameState[stateIdx].ActiveTurn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.stateMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) initReportAsync(gid mc.GameID, t uint) {
|
||||||
|
e.s.ReportExistsAsync(gid, t, func(b bool, err error) { e.reportAtStorageExists(gid, t, b, err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) reportAtStorageExists(gid mc.GameID, t uint, exists bool, err error) {
|
||||||
|
if err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
e.s.LoadReportAsync(gid, t, func(r report.Report, err error) { e.loadReportHandler(gid, r, err) })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.conn.FetchReport(gid, t, func(r report.Report, err error) { e.fetchReportHandler(gid, r, err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) fetchReportHandler(gid mc.GameID, r report.Report, err error) {
|
||||||
|
if err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.s.SaveReportAsync(gid, r.Turn, r, func(err error) { e.loadReportHandler(gid, r, err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) loadReportHandler(gid mc.GameID, r report.Report, err error) {
|
||||||
|
if err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stateMu.Lock()
|
||||||
|
needSaveState := false
|
||||||
|
stateIdx := slices.IndexFunc(e.state.GameState, func(gs mc.GameState) bool { return gs.ID == gid })
|
||||||
|
if stateIdx < 0 {
|
||||||
|
e.state.GameState = append(e.state.GameState, mc.GameState{ID: gid, LastTurn: r.Turn, ActiveTurn: r.Turn})
|
||||||
|
stateIdx = len(e.state.GameState) - 1
|
||||||
|
needSaveState = true
|
||||||
|
}
|
||||||
|
if e.state.ActiveGameID == nil {
|
||||||
|
e.state.ActiveGameID = new(gid)
|
||||||
|
needSaveState = true
|
||||||
|
}
|
||||||
|
if e.state.GameState[stateIdx].LastTurn < r.Turn {
|
||||||
|
e.state.GameState[stateIdx].LastTurn = r.Turn
|
||||||
|
e.state.GameState[stateIdx].ActiveTurn = r.Turn
|
||||||
|
needSaveState = true
|
||||||
|
}
|
||||||
|
if needSaveState {
|
||||||
|
if err := e.s.SaveState(*e.state); err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.stateMu.Unlock()
|
||||||
|
|
||||||
|
e.setReport(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) setReport(r report.Report) {
|
||||||
|
w := world.NewWorld(int(r.Width), int(r.Height))
|
||||||
|
e.reg.clear(&r)
|
||||||
|
for i := range r.LocalPlanet {
|
||||||
|
p := r.LocalPlanet[i]
|
||||||
|
id, err := w.AddCircle(p.X.F(), p.Y.F(), p.Size.F(), world.CircleWithClass(world.CircleClassLocalPlanet))
|
||||||
|
if err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.reg.registerLocalPlanet(id, i)
|
||||||
|
}
|
||||||
|
for i := range r.UnidentifiedPlanet {
|
||||||
|
p := r.UnidentifiedPlanet[i]
|
||||||
|
id, err := w.AddPoint(p.X.F(), p.Y.F(), world.PointWithClass(world.PointClassTrackIncoming))
|
||||||
|
if err != nil {
|
||||||
|
e.handlerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.reg.registerUnidentifiedPlanet(id, i)
|
||||||
|
}
|
||||||
|
e.loadWorld(w)
|
||||||
|
|
||||||
|
selfIdx := slices.IndexFunc(r.Player, func(p report.Player) bool { return p.Name == r.Race })
|
||||||
|
if selfIdx >= 0 {
|
||||||
|
fyne.Do(func() {
|
||||||
|
e.calculator.Init(
|
||||||
|
calculator.WithPlayerDrives(r.Player[selfIdx].Drive.F()),
|
||||||
|
calculator.WithPlayerWeapons(r.Player[selfIdx].Weapons.F()),
|
||||||
|
calculator.WithPlayerShields(r.Player[selfIdx].Shields.F()),
|
||||||
|
calculator.WithPlayerCargo(r.Player[selfIdx].Cargo.F()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
e.OnServiceError(fmt.Errorf("race %q not found at report players list", r.Race))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"map": "Map",
|
||||||
|
"calculator": "Ship Calculator",
|
||||||
|
"info": "Info"
|
||||||
|
},
|
||||||
|
"planet": {
|
||||||
|
"title": "Planet #{{.Number}} '{{.Name}}' production fot this ship:",
|
||||||
|
"mat": "Materials",
|
||||||
|
"prod.mass": "Prod. Mass",
|
||||||
|
"prod.ships": "Ships"
|
||||||
|
},
|
||||||
|
"tech": {
|
||||||
|
"d": "Drive",
|
||||||
|
"w": "Weapons",
|
||||||
|
"s": "Shields",
|
||||||
|
"c": "Cargo"
|
||||||
|
},
|
||||||
|
"ship": {
|
||||||
|
"action.create": "Create",
|
||||||
|
"mass": "Mass",
|
||||||
|
"speed": "Speed",
|
||||||
|
"attack": "Attack",
|
||||||
|
"defense": "Defense",
|
||||||
|
"load": "Load"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"max": "Max."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
@@ -256,36 +255,6 @@ func (e *client) onDradEnd() {
|
|||||||
e.pan.DragEnd()
|
e.pan.DragEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *client) onTapped(ev *fyne.PointEvent) {
|
|
||||||
if e.world == nil || ev == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
xPx, yPx, ok := e.eventPosToPixel(ev.Position.X, ev.Position.Y)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
params := e.getLastRenderedParams()
|
|
||||||
hits, err := e.world.HitTest(e.hits, ¶ms, xPx, yPx)
|
|
||||||
if err != nil {
|
|
||||||
// In UI you probably don't want panic; keep your existing handling.
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := func(v int) float64 { return float64(v) / float64(world.SCALE) }
|
|
||||||
|
|
||||||
for _, hit := range hits {
|
|
||||||
var coord string
|
|
||||||
if hit.Kind == world.KindLine {
|
|
||||||
coord = fmt.Sprintf("{%f,%f - %f,%f}", m(hit.X1), m(hit.Y1), m(hit.X2), m(hit.Y2))
|
|
||||||
} else {
|
|
||||||
coord = fmt.Sprintf("{%f,%f}", m(hit.X), m(hit.Y))
|
|
||||||
}
|
|
||||||
fmt.Println("hit:", hit.ID, "Coord:", coord)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *client) onScrolled(s *fyne.ScrollEvent) {
|
func (e *client) onScrolled(s *fyne.ScrollEvent) {
|
||||||
if e.world == nil || s == nil {
|
if e.world == nil || s == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,629 @@
|
|||||||
|
package calculator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"galaxy/calc"
|
||||||
|
"galaxy/client/widget/numeric"
|
||||||
|
"galaxy/util"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CalculatorOpt func(*Calculator)
|
||||||
|
type ShipClass struct {
|
||||||
|
Name string
|
||||||
|
Drive float64
|
||||||
|
Armament uint
|
||||||
|
Weapons float64
|
||||||
|
Shields float64
|
||||||
|
Cargo float64
|
||||||
|
}
|
||||||
|
type ShipClassFn func(string, float64, uint, float64, float64, float64)
|
||||||
|
|
||||||
|
type Calculator struct {
|
||||||
|
CanvasObject fyne.CanvasObject
|
||||||
|
|
||||||
|
playerDrivesTech float64
|
||||||
|
playerWeaponsTech float64
|
||||||
|
playerShieldsTech float64
|
||||||
|
playerCargoTech float64
|
||||||
|
|
||||||
|
shipDriveEntry *numeric.FloatEntry
|
||||||
|
shipWeaponsEntry *numeric.FloatEntry
|
||||||
|
shipArmamentEntry *numeric.IntEntry
|
||||||
|
shipShieldsEntry *numeric.FloatEntry
|
||||||
|
shipCargoEntry *numeric.FloatEntry
|
||||||
|
|
||||||
|
playerDrivesTechEntry *numeric.FloatEntry
|
||||||
|
playerWeaponsTechEntry *numeric.FloatEntry
|
||||||
|
playerShieldsTechEntry *numeric.FloatEntry
|
||||||
|
playerCargoTechEntry *numeric.FloatEntry
|
||||||
|
|
||||||
|
drivesTechOverride *widget.Check
|
||||||
|
weaponsTechOverride *widget.Check
|
||||||
|
shieldsTechOverride *widget.Check
|
||||||
|
cargoTechOverride *widget.Check
|
||||||
|
|
||||||
|
massEntry *numeric.FloatEntry
|
||||||
|
speedEntry *numeric.FloatEntry
|
||||||
|
attackEntry *numeric.FloatEntry
|
||||||
|
defenseEntry *numeric.FloatEntry
|
||||||
|
cargoLoadEntry *numeric.FloatEntry
|
||||||
|
planetMatEntry *numeric.FloatEntry
|
||||||
|
|
||||||
|
massOverride *widget.Check
|
||||||
|
speedOverride *widget.Check
|
||||||
|
attackOverride *widget.Check
|
||||||
|
defenseOverride *widget.Check
|
||||||
|
cargoLoadMaximize *widget.Check
|
||||||
|
planetMatOverride *widget.Check
|
||||||
|
|
||||||
|
planetLabel *widget.Label
|
||||||
|
planetMassProdLabel *widget.Label
|
||||||
|
planetShipsProdLabel *widget.Label
|
||||||
|
planetContainer fyne.CanvasObject
|
||||||
|
planetProdContainer fyne.CanvasObject
|
||||||
|
|
||||||
|
shipSelector *widget.SelectEntry
|
||||||
|
shipCreateButton *widget.Button
|
||||||
|
|
||||||
|
onCreateHandler ShipClassFn
|
||||||
|
loader ShipClassFn
|
||||||
|
knownClasses []ShipClass
|
||||||
|
|
||||||
|
validateMu sync.RWMutex
|
||||||
|
|
||||||
|
l, mat, res float64
|
||||||
|
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlayerDrives(v float64) CalculatorOpt {
|
||||||
|
return func(c *Calculator) { c.playerDrivesTech = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlayerWeapons(v float64) CalculatorOpt {
|
||||||
|
return func(c *Calculator) { c.playerWeaponsTech = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlayerShields(v float64) CalculatorOpt {
|
||||||
|
return func(c *Calculator) { c.playerShieldsTech = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlayerCargo(v float64) CalculatorOpt {
|
||||||
|
return func(c *Calculator) { c.playerCargoTech = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCreateHandler(f ShipClassFn) CalculatorOpt {
|
||||||
|
return func(c *Calculator) { c.onCreateHandler = f }
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCaclulator(opts ...CalculatorOpt) *Calculator {
|
||||||
|
c := &Calculator{}
|
||||||
|
|
||||||
|
c.shipCreateButton = widget.NewButton(lang.L("ship.action.create"), c.onCreateShipClassButton)
|
||||||
|
c.shipCreateButton.Disable()
|
||||||
|
c.loader = c.LoadShipClass
|
||||||
|
|
||||||
|
c.planetMatEntry = numeric.NewFloatEntry(10, c.onPlanetMatChange)
|
||||||
|
c.planetMatOverride = widget.NewCheck("", c.overridePlanetMat)
|
||||||
|
c.planetMatOverride.Disable()
|
||||||
|
c.planetLabel = widget.NewLabel("")
|
||||||
|
c.planetMassProdLabel = bareLabel("")
|
||||||
|
c.planetShipsProdLabel = bareLabel("")
|
||||||
|
c.planetProdContainer = container.NewHBox(
|
||||||
|
label(lang.L("planet.prod.mass")+":"),
|
||||||
|
fixedLabel(c.planetMassProdLabel, 80),
|
||||||
|
label(lang.L("planet.prod.ships")+":"),
|
||||||
|
fixedLabel(c.planetShipsProdLabel, 80),
|
||||||
|
)
|
||||||
|
c.planetProdContainer.Hide()
|
||||||
|
|
||||||
|
c.planetContainer = container.NewVBox(
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(c.planetLabel),
|
||||||
|
rowForItem(lang.L("planet.mat")+":", floatEntry(c.planetMatEntry, 100), c.planetMatOverride),
|
||||||
|
c.planetProdContainer,
|
||||||
|
)
|
||||||
|
c.planetContainer.Hide()
|
||||||
|
|
||||||
|
c.shipSelector = widget.NewSelectEntry(nil)
|
||||||
|
c.shipSelector.OnChanged = c.onShipSelectorChange
|
||||||
|
|
||||||
|
c.shipDriveEntry = numeric.NewFloatEntry(7, c.onShipDriveChange)
|
||||||
|
c.shipWeaponsEntry = numeric.NewFloatEntry(7, c.onShipWeaponsChange)
|
||||||
|
c.shipArmamentEntry = numeric.NewIntEntry(7, c.onShipArmamentChange)
|
||||||
|
c.shipShieldsEntry = numeric.NewFloatEntry(7, c.onShipShieldsChange)
|
||||||
|
c.shipCargoEntry = numeric.NewFloatEntry(7, c.onShipCargoChange)
|
||||||
|
|
||||||
|
c.playerDrivesTechEntry = numeric.NewFloatEntry(7, c.onDrivesTechChange)
|
||||||
|
c.playerWeaponsTechEntry = numeric.NewFloatEntry(7, c.onWeaponsTechChange)
|
||||||
|
c.playerShieldsTechEntry = numeric.NewFloatEntry(7, c.onShieldsTechChange)
|
||||||
|
c.playerCargoTechEntry = numeric.NewFloatEntry(7, c.onCargoTechChange)
|
||||||
|
|
||||||
|
c.massEntry = numeric.NewFloatEntry(7, c.onMassChange)
|
||||||
|
c.speedEntry = numeric.NewFloatEntry(7, c.onSpeedChange)
|
||||||
|
c.attackEntry = numeric.NewFloatEntry(7, c.onAttackChange)
|
||||||
|
c.defenseEntry = numeric.NewFloatEntry(7, c.onDefenseChange)
|
||||||
|
c.cargoLoadEntry = numeric.NewFloatEntry(7, c.onCargoLoadChange)
|
||||||
|
|
||||||
|
c.drivesTechOverride = widget.NewCheck("", c.overrideDrivesTech)
|
||||||
|
c.drivesTechOverride.Disable()
|
||||||
|
c.weaponsTechOverride = widget.NewCheck("", c.overrideWeaponsTech)
|
||||||
|
c.weaponsTechOverride.Disable()
|
||||||
|
c.shieldsTechOverride = widget.NewCheck("", c.overrideShieldsTech)
|
||||||
|
c.shieldsTechOverride.Disable()
|
||||||
|
c.cargoTechOverride = widget.NewCheck("", c.overrideCargoTech)
|
||||||
|
c.cargoTechOverride.Disable()
|
||||||
|
|
||||||
|
c.massOverride = widget.NewCheck("", c.overrideMass)
|
||||||
|
c.massOverride.Disable()
|
||||||
|
c.speedOverride = widget.NewCheck("", c.overrideSpeed)
|
||||||
|
c.speedOverride.Disable()
|
||||||
|
c.attackOverride = widget.NewCheck("", c.overrideAttack)
|
||||||
|
c.attackOverride.Disable()
|
||||||
|
c.defenseOverride = widget.NewCheck("", c.overrideDefense)
|
||||||
|
c.defenseOverride.Disable()
|
||||||
|
c.cargoLoadMaximize = widget.NewCheck(lang.L("label.max"), c.maximizeCargoLoad)
|
||||||
|
c.cargoLoadMaximize.SetChecked(true)
|
||||||
|
|
||||||
|
createShip := container.NewBorder(
|
||||||
|
nil, // top
|
||||||
|
nil, // bottom
|
||||||
|
nil, // left
|
||||||
|
c.shipCreateButton, // right
|
||||||
|
c.shipSelector, // center
|
||||||
|
)
|
||||||
|
|
||||||
|
c.CanvasObject = container.NewVBox(
|
||||||
|
container.NewPadded(createShip),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
rowForTech(lang.L("tech.d")+":",
|
||||||
|
c.shipDriveEntry, floatEntry(c.playerDrivesTechEntry, 80), c.drivesTechOverride),
|
||||||
|
rowForWeapons(lang.L("tech.w")+":",
|
||||||
|
c.shipArmamentEntry, c.shipWeaponsEntry, floatEntry(c.playerWeaponsTechEntry, 80), c.weaponsTechOverride),
|
||||||
|
rowForTech(lang.L("tech.s")+":",
|
||||||
|
c.shipShieldsEntry, floatEntry(c.playerShieldsTechEntry, 80), c.shieldsTechOverride),
|
||||||
|
rowForTech(lang.L("tech.c")+":",
|
||||||
|
c.shipCargoEntry, floatEntry(c.playerCargoTechEntry, 80), c.cargoTechOverride),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
rowForItem(lang.L("ship.load")+":",
|
||||||
|
floatEntry(c.cargoLoadEntry, 80), c.cargoLoadMaximize),
|
||||||
|
rowForItem(lang.L("ship.mass")+":",
|
||||||
|
floatEntry(c.massEntry, 80), c.massOverride),
|
||||||
|
rowForItem(lang.L("ship.speed")+":",
|
||||||
|
floatEntry(c.speedEntry, 80), c.speedOverride),
|
||||||
|
rowForItem(lang.L("ship.attack")+":",
|
||||||
|
floatEntry(c.attackEntry, 80), c.attackOverride),
|
||||||
|
rowForItem(lang.L("ship.defense")+":",
|
||||||
|
floatEntry(c.defenseEntry, 80), c.defenseOverride),
|
||||||
|
c.planetContainer,
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Init(opts...)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) Init(opts ...CalculatorOpt) {
|
||||||
|
for i := range opts {
|
||||||
|
opts[i](c)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.playerDrivesTechEntry.SetOrigin(c.playerDrivesTech)
|
||||||
|
c.playerWeaponsTechEntry.SetOrigin(c.playerWeaponsTech)
|
||||||
|
c.playerShieldsTechEntry.SetOrigin(c.playerShieldsTech)
|
||||||
|
c.playerCargoTechEntry.SetOrigin(c.playerCargoTech)
|
||||||
|
|
||||||
|
c.CanvasObject.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) Refresh() {
|
||||||
|
c.validate()
|
||||||
|
c.CanvasObject.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) RegisterClasses(shipClass ...ShipClass) {
|
||||||
|
c.knownClasses = shipClass
|
||||||
|
names := make([]string, len(c.knownClasses))
|
||||||
|
for i := range c.knownClasses {
|
||||||
|
names[i] = c.knownClasses[i].Name
|
||||||
|
}
|
||||||
|
slices.Sort(names)
|
||||||
|
c.shipSelector = widget.NewSelectEntry(names)
|
||||||
|
c.shipSelector.OnChanged = c.onShipSelectorChange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onCreateShipClassButton() {
|
||||||
|
if c.onCreateHandler == nil || !c.Valid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) validate() {
|
||||||
|
fyne.Do(func() {
|
||||||
|
c.validateMu.Lock()
|
||||||
|
err := c.validateEntries()
|
||||||
|
c.Valid = err == nil
|
||||||
|
if err != nil {
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
c.shipClassNameValidate()
|
||||||
|
c.validateMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) validateEntries() (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
c.cargoLoadEntry.Clear()
|
||||||
|
if !c.massOverride.Checked {
|
||||||
|
c.massEntry.Clear()
|
||||||
|
}
|
||||||
|
if !c.speedOverride.Checked {
|
||||||
|
c.speedEntry.Clear()
|
||||||
|
}
|
||||||
|
if !c.attackOverride.Checked {
|
||||||
|
c.attackEntry.Clear()
|
||||||
|
}
|
||||||
|
if !c.defenseOverride.Checked {
|
||||||
|
c.defenseEntry.Clear()
|
||||||
|
}
|
||||||
|
// c.planetProdContainer.Hide()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
drive, ok := c.shipDriveEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Parameter Drive is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
driveTech, ok := c.playerDrivesTechEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Drive tech level is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
armament, ok := c.shipArmamentEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Parameter Armament is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
weapons, ok := c.shipWeaponsEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Parameter Weapons is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
weaponsTech, ok := c.playerWeaponsTechEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Weapons tech level is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shields, ok := c.shipShieldsEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Parameter Shields is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shieldsTech, ok := c.playerShieldsTechEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Shields tech level is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cargo, ok := c.shipCargoEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Parameter Cargo is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cargoTech, ok := c.playerCargoTechEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Cargo tech level is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = calc.ValidateShipTypeValues(drive, armament, weapons, shields, cargo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cargoLoad float64
|
||||||
|
if c.cargoLoadMaximize.Checked {
|
||||||
|
cargoLoad = calc.CargoCapacity(cargo, cargoTech)
|
||||||
|
c.cargoLoadEntry.SetOrigin(cargoLoad)
|
||||||
|
} else if cargoLoad, ok = c.cargoLoadEntry.Value(); !ok {
|
||||||
|
err = errors.New("Cargo load value is not valid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyMass, ok := calc.EmptyMass(drive, weapons, uint(armament), shields, cargo)
|
||||||
|
if !ok {
|
||||||
|
err = errors.New("Unable to calculate empty mass (check armament and weapons)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fullMass := calc.FullMass(emptyMass, cargoLoad)
|
||||||
|
speed := calc.Speed(calc.DriveEffective(drive, driveTech), fullMass)
|
||||||
|
effectiveAttack := calc.EffectiveAttack(weapons, weaponsTech)
|
||||||
|
effectiveDefense := calc.EffectiveDefence(shields, shieldsTech, fullMass)
|
||||||
|
|
||||||
|
c.massEntry.SetOrigin(emptyMass)
|
||||||
|
c.speedEntry.SetOrigin(speed)
|
||||||
|
c.attackEntry.SetOrigin(effectiveAttack)
|
||||||
|
c.defenseEntry.SetOrigin(effectiveDefense)
|
||||||
|
|
||||||
|
planetMat, ok := c.planetMatEntry.Value()
|
||||||
|
if !ok {
|
||||||
|
// c.planetProdContainer.Hide()
|
||||||
|
} else {
|
||||||
|
massProd := calc.PlanetProduceShipMass(c.l, planetMat, c.res)
|
||||||
|
c.planetMassProdLabel.SetText(strconv.FormatFloat(util.Fixed3(massProd), 'f', -1, 64))
|
||||||
|
ships := 0.
|
||||||
|
if emptyMass > 0 {
|
||||||
|
ships = massProd / emptyMass
|
||||||
|
}
|
||||||
|
c.planetShipsProdLabel.SetText(strconv.FormatFloat(util.Fixed3(ships), 'f', -1, 64))
|
||||||
|
c.planetProdContainer.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onOriginInputChange(cb *widget.Check, e *numeric.FloatEntry) {
|
||||||
|
if e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cb != nil {
|
||||||
|
cb.Checked = e.Overriden()
|
||||||
|
if !cb.Checked {
|
||||||
|
cb.Disable()
|
||||||
|
} else {
|
||||||
|
cb.Enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.onFloatEntryChange(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onFloatEntryChange(e *numeric.FloatEntry) {
|
||||||
|
if e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Validate()
|
||||||
|
c.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onIntEntryChange(e *numeric.IntEntry) {
|
||||||
|
if e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Validate()
|
||||||
|
c.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideChecked(cb *widget.Check, e *numeric.FloatEntry) {
|
||||||
|
if cb == nil || e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cb.Checked {
|
||||||
|
e.Reset()
|
||||||
|
cb.Disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShipDriveChange(string) {
|
||||||
|
c.onFloatEntryChange(c.shipDriveEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShipArmamentChange(string) {
|
||||||
|
defer c.onIntEntryChange(c.shipArmamentEntry)
|
||||||
|
if weapons, ok := c.shipWeaponsEntry.Value(); !ok || !c.shipWeaponsEntry.Valid {
|
||||||
|
return
|
||||||
|
} else if armament, ok := c.shipArmamentEntry.Value(); !ok || !c.shipArmamentEntry.Valid {
|
||||||
|
return
|
||||||
|
} else if armament > 0 && weapons == 0 {
|
||||||
|
c.shipWeaponsEntry.SetOrigin(1.0)
|
||||||
|
} else if armament == 0 && weapons > 0 {
|
||||||
|
c.shipWeaponsEntry.SetOrigin(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShipWeaponsChange(string) {
|
||||||
|
defer c.onFloatEntryChange(c.shipWeaponsEntry)
|
||||||
|
if weapons, ok := c.shipWeaponsEntry.Value(); !ok || !c.shipWeaponsEntry.Valid {
|
||||||
|
return
|
||||||
|
} else if armament, ok := c.shipArmamentEntry.Value(); !ok || !c.shipArmamentEntry.Valid {
|
||||||
|
return
|
||||||
|
} else if weapons > 0 && armament == 0 {
|
||||||
|
c.shipArmamentEntry.SetOrigin(1)
|
||||||
|
} else if weapons == 0 && armament > 0 {
|
||||||
|
c.shipArmamentEntry.SetOrigin(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShipShieldsChange(string) {
|
||||||
|
c.onFloatEntryChange(c.shipShieldsEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShipCargoChange(string) {
|
||||||
|
c.onFloatEntryChange(c.shipCargoEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onDrivesTechChange(string) {
|
||||||
|
c.onOriginInputChange(c.drivesTechOverride, c.playerDrivesTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideDrivesTech(bool) {
|
||||||
|
c.overrideChecked(c.drivesTechOverride, c.playerDrivesTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onWeaponsTechChange(string) {
|
||||||
|
c.onOriginInputChange(c.weaponsTechOverride, c.playerWeaponsTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideWeaponsTech(bool) {
|
||||||
|
c.overrideChecked(c.weaponsTechOverride, c.playerWeaponsTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShieldsTechChange(string) {
|
||||||
|
c.onOriginInputChange(c.shieldsTechOverride, c.playerShieldsTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideShieldsTech(bool) {
|
||||||
|
c.overrideChecked(c.shieldsTechOverride, c.playerShieldsTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onCargoTechChange(string) {
|
||||||
|
c.onOriginInputChange(c.cargoTechOverride, c.playerCargoTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideCargoTech(bool) {
|
||||||
|
c.overrideChecked(c.cargoTechOverride, c.playerCargoTechEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onCargoLoadChange(string) {
|
||||||
|
c.onFloatEntryChange(c.cargoLoadEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onMassChange(string) {
|
||||||
|
c.onOriginInputChange(c.massOverride, c.massEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideMass(bool) {
|
||||||
|
c.overrideChecked(c.massOverride, c.massEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onSpeedChange(string) {
|
||||||
|
c.onOriginInputChange(c.speedOverride, c.speedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideSpeed(bool) {
|
||||||
|
c.overrideChecked(c.speedOverride, c.speedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onAttackChange(string) {
|
||||||
|
c.onOriginInputChange(c.attackOverride, c.attackEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideAttack(bool) {
|
||||||
|
c.overrideChecked(c.attackOverride, c.attackEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onDefenseChange(string) {
|
||||||
|
c.onOriginInputChange(c.defenseOverride, c.defenseEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overrideDefense(bool) {
|
||||||
|
c.overrideChecked(c.defenseOverride, c.defenseEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) maximizeCargoLoad(bool) {
|
||||||
|
c.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onPlanetMatChange(string) {
|
||||||
|
c.onOriginInputChange(c.planetMatOverride, c.planetMatEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) overridePlanetMat(bool) {
|
||||||
|
c.overrideChecked(c.planetMatOverride, c.planetMatEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) onShipSelectorChange(v string) {
|
||||||
|
i, ok := c.shipClassNameValidate()
|
||||||
|
if i < 0 || !ok || c.loader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.loader(
|
||||||
|
c.knownClasses[i].Name,
|
||||||
|
c.knownClasses[i].Drive,
|
||||||
|
c.knownClasses[i].Armament,
|
||||||
|
c.knownClasses[i].Weapons,
|
||||||
|
c.knownClasses[i].Shields,
|
||||||
|
c.knownClasses[i].Cargo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) shipClassNameValidate() (int, bool) {
|
||||||
|
var canCreateShip bool
|
||||||
|
defer func() {
|
||||||
|
if canCreateShip && c.Valid {
|
||||||
|
c.shipCreateButton.Enable()
|
||||||
|
} else {
|
||||||
|
c.shipCreateButton.Disable()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
name, canCreateShip := util.ValidateTypeName(c.shipSelector.Text)
|
||||||
|
if canCreateShip {
|
||||||
|
c.shipSelector.Text = name
|
||||||
|
}
|
||||||
|
i := slices.IndexFunc(c.knownClasses, func(v ShipClass) bool { return v.Name == name })
|
||||||
|
canCreateShip = canCreateShip && i < 0
|
||||||
|
return i, canCreateShip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) LoadShipClass(n string, D float64, A uint, W float64, S float64, C float64) {
|
||||||
|
c.shipDriveEntry.SetOrigin(D)
|
||||||
|
c.shipArmamentEntry.SetOrigin(int(A))
|
||||||
|
c.shipWeaponsEntry.SetOrigin(W)
|
||||||
|
c.shipShieldsEntry.SetOrigin(S)
|
||||||
|
c.shipCargoEntry.SetOrigin(C)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rowForItem(l string, entry, override fyne.CanvasObject) fyne.CanvasObject {
|
||||||
|
i := []fyne.CanvasObject{label(l), entry}
|
||||||
|
if override != nil {
|
||||||
|
i = append(i, override)
|
||||||
|
}
|
||||||
|
return container.NewHBox(i...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rowForTech(l string, shipEntry, techEntry, btn fyne.CanvasObject) fyne.CanvasObject {
|
||||||
|
return container.NewHBox(
|
||||||
|
label(l),
|
||||||
|
floatEntry(shipEntry, 115),
|
||||||
|
widget.NewLabel("@"),
|
||||||
|
techEntry,
|
||||||
|
btn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rowForWeapons(l string, armamentEntry, weaponsEntry, techEntry, btn fyne.CanvasObject) fyne.CanvasObject {
|
||||||
|
return container.NewHBox(
|
||||||
|
label(l),
|
||||||
|
intEntry(armamentEntry, 35),
|
||||||
|
floatEntry(weaponsEntry, 75),
|
||||||
|
widget.NewLabel("@"),
|
||||||
|
techEntry,
|
||||||
|
btn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func label(l string) fyne.CanvasObject {
|
||||||
|
return fixedLabel(bareLabel(l), 110)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixedLabel(w *widget.Label, width float32) fyne.CanvasObject {
|
||||||
|
s := container.NewHScroll(w)
|
||||||
|
s.SetMinSize(fyne.NewSize(width, 1))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func bareLabel(l string) *widget.Label {
|
||||||
|
w := widget.NewLabelWithStyle(l, fyne.TextAlignTrailing, fyne.TextStyle{Monospace: true, Symbol: false})
|
||||||
|
w.Selectable = false
|
||||||
|
w.Truncation = fyne.TextTruncateOff
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func intEntry(content fyne.CanvasObject, width float32) fyne.CanvasObject {
|
||||||
|
s := container.NewHScroll(content)
|
||||||
|
s.SetMinSize(fyne.NewSize(width, 1))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatEntry(content fyne.CanvasObject, width float32) fyne.CanvasObject {
|
||||||
|
s := container.NewHScroll(content)
|
||||||
|
s.SetMinSize(fyne.NewSize(width, 1))
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package calculator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2/lang"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Calculator) UnloadPlanet() {
|
||||||
|
c.planetContainer.Hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calculator) LoadPlanet(name string, number uint, L, Mat, Res float64) {
|
||||||
|
c.l, c.mat, c.res = L, Mat, Res
|
||||||
|
c.planetLabel.SetText(lang.L("planet.title", map[string]any{"Number": number, "Name": name}))
|
||||||
|
c.planetMatEntry.SetOrigin(Mat)
|
||||||
|
c.planetContainer.Show()
|
||||||
|
}
|
||||||
@@ -1,30 +1,82 @@
|
|||||||
package numeric
|
package numeric
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"galaxy/client/widget/validator"
|
||||||
|
"galaxy/util"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/driver/mobile"
|
"fyne.io/fyne/v2/driver/mobile"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
type numericalEntry struct {
|
type FloatEntry struct {
|
||||||
widget.Entry
|
widget.Entry
|
||||||
|
origin float64
|
||||||
|
MaxValue float64
|
||||||
|
maxSize uint
|
||||||
|
validator fyne.StringValidator
|
||||||
|
Valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNumericalEntry() *numericalEntry {
|
type IntEntry struct {
|
||||||
entry := &numericalEntry{}
|
widget.Entry
|
||||||
entry.ExtendBaseWidget(entry)
|
origin uint
|
||||||
return entry
|
MaxValue uint
|
||||||
|
maxSize uint
|
||||||
|
validator fyne.StringValidator
|
||||||
|
Valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *numericalEntry) TypedRune(r rune) {
|
func NewFloatEntry(maxSize uint, onChanged func(string)) *FloatEntry {
|
||||||
if (r >= '0' && r <= '9') || r == '.' || r == ',' {
|
e := &FloatEntry{maxSize: maxSize, validator: validator.FloatEntryValidator}
|
||||||
e.Entry.TypedRune(r)
|
e.ExtendBaseWidget(e)
|
||||||
|
e.Entry.Scroll = fyne.ScrollNone
|
||||||
|
e.Entry.TextStyle = fyne.TextStyle{Monospace: true}
|
||||||
|
// e.Validator = validator.FloatEntryValidator
|
||||||
|
// e.AlwaysShowValidationError = true
|
||||||
|
e.Entry.ActionItem = nil
|
||||||
|
e.SetOrigin(0)
|
||||||
|
e.Validate()
|
||||||
|
e.Entry.OnChanged = onChanged
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntEntry(maxSize uint, onChanged func(string)) *IntEntry {
|
||||||
|
e := &IntEntry{maxSize: maxSize, validator: validator.IntEntryValidator}
|
||||||
|
e.ExtendBaseWidget(e)
|
||||||
|
e.Entry.Scroll = fyne.ScrollNone
|
||||||
|
e.Entry.TextStyle = fyne.TextStyle{Monospace: true}
|
||||||
|
// e.Validator = validator.IntEntryValidator
|
||||||
|
// e.AlwaysShowValidationError = true
|
||||||
|
e.Entry.ActionItem = nil
|
||||||
|
e.SetOrigin(0)
|
||||||
|
e.Validate()
|
||||||
|
e.Entry.OnChanged = onChanged
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
r := e.Entry.CreateRenderer()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) TypedRune(r rune) {
|
||||||
|
if !((r >= '0' && r <= '9') || r == '.') {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if !lengthBelowLimit(e.Entry.Text, e.maxSize) && e.Entry.SelectedText() == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r == '.' && strings.Contains(e.Entry.Text, ".") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Entry.TypedRune(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *numericalEntry) TypedShortcut(shortcut fyne.Shortcut) {
|
func (e *FloatEntry) TypedShortcut(shortcut fyne.Shortcut) {
|
||||||
paste, ok := shortcut.(*fyne.ShortcutPaste)
|
paste, ok := shortcut.(*fyne.ShortcutPaste)
|
||||||
if !ok {
|
if !ok {
|
||||||
e.Entry.TypedShortcut(shortcut)
|
e.Entry.TypedShortcut(shortcut)
|
||||||
@@ -37,6 +89,129 @@ func (e *numericalEntry) TypedShortcut(shortcut fyne.Shortcut) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *numericalEntry) Keyboard() mobile.KeyboardType {
|
func (e *FloatEntry) Keyboard() mobile.KeyboardType {
|
||||||
return mobile.NumberKeyboard
|
return mobile.NumberKeyboard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) SetOrigin(v float64) {
|
||||||
|
if v < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.origin = v
|
||||||
|
e.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) Reset() {
|
||||||
|
e.SetValue(e.origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) Clear() {
|
||||||
|
onChanged := e.Entry.OnChanged
|
||||||
|
e.Entry.OnChanged = nil
|
||||||
|
e.Entry.SetText("")
|
||||||
|
e.Entry.OnChanged = onChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) SetValue(v float64) {
|
||||||
|
if v < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Entry.SetText(strconv.FormatFloat(util.Fixed3(v), 'f', -1, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) Value() (float64, bool) {
|
||||||
|
if v, err := validator.ParseFloat(e.Entry.Text); err != nil {
|
||||||
|
return 0, false
|
||||||
|
} else {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) Overriden() bool {
|
||||||
|
if v, ok := e.Value(); !ok {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return util.Fixed3(v) != util.Fixed3(e.origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FloatEntry) Validate() {
|
||||||
|
if e.validator == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := e.validator(e.Entry.Text)
|
||||||
|
e.Valid = err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) TypedRune(r rune) {
|
||||||
|
if r >= '0' && r <= '9' {
|
||||||
|
if lengthBelowLimit(e.Entry.Text, e.maxSize) || e.Entry.SelectedText() != "" {
|
||||||
|
e.Entry.TypedRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) TypedShortcut(shortcut fyne.Shortcut) {
|
||||||
|
paste, ok := shortcut.(*fyne.ShortcutPaste)
|
||||||
|
if !ok {
|
||||||
|
e.Entry.TypedShortcut(shortcut)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := paste.Clipboard.Content()
|
||||||
|
if _, err := strconv.ParseInt(content, 10, 64); err == nil {
|
||||||
|
e.Entry.TypedShortcut(shortcut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) Keyboard() mobile.KeyboardType {
|
||||||
|
return mobile.NumberKeyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) SetOrigin(v int) {
|
||||||
|
if v < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.origin = uint(v)
|
||||||
|
e.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) Reset() {
|
||||||
|
e.SetValue(int(e.origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) SetValue(v int) {
|
||||||
|
if v < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Entry.SetText(strconv.Itoa(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) Value() (int, bool) {
|
||||||
|
if v, err := validator.ParseInt(e.Entry.Text); err != nil {
|
||||||
|
return 0, false
|
||||||
|
} else {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) Overriden() bool {
|
||||||
|
if v, ok := e.Value(); !ok {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return v != int(e.origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *IntEntry) Validate() {
|
||||||
|
if e.validator == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := e.validator(e.Entry.Text)
|
||||||
|
e.Valid = err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lengthBelowLimit(s string, max uint) bool {
|
||||||
|
return utf8.RuneCountInString(s) < int(max)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,26 @@ package validator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type floatValidator func(float64) error
|
||||||
|
|
||||||
|
var (
|
||||||
|
FloatEntryValidator = numericEntryValidator(
|
||||||
|
nonNegativeValidator,
|
||||||
|
minOrZeroValueValidator(1.),
|
||||||
|
)
|
||||||
|
IntEntryValidator = numericEntryValidator(
|
||||||
|
intValidator,
|
||||||
|
nonNegativeValidator,
|
||||||
|
minOrZeroValueValidator(1.),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
func NewStackValidator(first fyne.StringValidator, rest ...fyne.StringValidator) fyne.StringValidator {
|
func NewStackValidator(first fyne.StringValidator, rest ...fyne.StringValidator) fyne.StringValidator {
|
||||||
if first == nil {
|
if first == nil {
|
||||||
panic("first validator cannot be nil")
|
panic("first validator cannot be nil")
|
||||||
@@ -43,6 +58,44 @@ func NewMutualValidator(other func() float64, valid func(float64) bool) fyne.Str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func numericEntryValidator(other ...floatValidator) fyne.StringValidator {
|
||||||
|
return func(s string) error {
|
||||||
|
v, err := ParseFloat(s)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("not a float value")
|
||||||
|
}
|
||||||
|
for i := range other {
|
||||||
|
if err := other[i](v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nonNegativeValidator(v float64) error {
|
||||||
|
if v < 0 {
|
||||||
|
return errors.New("value must be greater of equal to zero")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func intValidator(v float64) error {
|
||||||
|
if float64(int(v)) != v {
|
||||||
|
return errors.New("value must be an integer")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func minOrZeroValueValidator(min float64) floatValidator {
|
||||||
|
return func(f float64) error {
|
||||||
|
if f > 0 && f < min {
|
||||||
|
return fmt.Errorf("value must be zero or >= %f", min)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func FloatValueValidator(s string) error {
|
func FloatValueValidator(s string) error {
|
||||||
if _, err := ParseFloat(s); err != nil {
|
if _, err := ParseFloat(s); err != nil {
|
||||||
return errors.New("not a float value")
|
return errors.New("not a float value")
|
||||||
@@ -50,6 +103,21 @@ func FloatValueValidator(s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IntValueValidator(s string) error {
|
||||||
|
if _, err := ParseInt(s); err != nil {
|
||||||
|
return errors.New("not an integer value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func ParseFloat(s string) (float64, error) {
|
func ParseFloat(s string) (float64, error) {
|
||||||
return strconv.ParseFloat(s, 64)
|
return strconv.ParseFloat(s, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseInt(s string) (int, error) {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 64); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return int(v), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+22
-24
@@ -415,6 +415,13 @@ func (LightTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
|
|||||||
PointRadiusPx: new(3.5),
|
PointRadiusPx: new(3.5),
|
||||||
}, true
|
}, true
|
||||||
|
|
||||||
|
case PointClassUnidentifiedPlanet:
|
||||||
|
// soft orange
|
||||||
|
return StyleOverride{
|
||||||
|
FillColor: cRGBA(192, 192, 192, 255),
|
||||||
|
PointRadiusPx: new(2.5),
|
||||||
|
}, true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return StyleOverride{}, false
|
return StyleOverride{}, false
|
||||||
}
|
}
|
||||||
@@ -457,15 +464,7 @@ func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
|
|||||||
case CircleClassDefault:
|
case CircleClassDefault:
|
||||||
return StyleOverride{}, false
|
return StyleOverride{}, false
|
||||||
|
|
||||||
case CircleClassHome:
|
case CircleClassLocalPlanet:
|
||||||
// teal-ish, a bit stronger stroke
|
|
||||||
return StyleOverride{
|
|
||||||
FillColor: cRGBA(32, 161, 145, 50),
|
|
||||||
StrokeColor: cRGBA(32, 161, 145, 210),
|
|
||||||
StrokeWidthPx: new(2.5),
|
|
||||||
}, true
|
|
||||||
|
|
||||||
case CircleClassAcquired:
|
|
||||||
// blue
|
// blue
|
||||||
return StyleOverride{
|
return StyleOverride{
|
||||||
FillColor: cRGBA(70, 108, 196, 45),
|
FillColor: cRGBA(70, 108, 196, 45),
|
||||||
@@ -473,7 +472,7 @@ func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
|
|||||||
StrokeWidthPx: new(2.2),
|
StrokeWidthPx: new(2.2),
|
||||||
}, true
|
}, true
|
||||||
|
|
||||||
case CircleClassOccupied:
|
case CircleClassOthersPlanet:
|
||||||
// orange
|
// orange
|
||||||
return StyleOverride{
|
return StyleOverride{
|
||||||
FillColor: cRGBA(222, 142, 70, 50),
|
FillColor: cRGBA(222, 142, 70, 50),
|
||||||
@@ -481,7 +480,7 @@ func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
|
|||||||
StrokeWidthPx: new(2.2),
|
StrokeWidthPx: new(2.2),
|
||||||
}, true
|
}, true
|
||||||
|
|
||||||
case CircleClassFree:
|
case CircleClassFreePlanet:
|
||||||
// green
|
// green
|
||||||
return StyleOverride{
|
return StyleOverride{
|
||||||
FillColor: cRGBA(76, 171, 107, 45),
|
FillColor: cRGBA(76, 171, 107, 45),
|
||||||
@@ -574,6 +573,12 @@ func (*DarkTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
|
|||||||
PointRadiusPx: new(3.5),
|
PointRadiusPx: new(3.5),
|
||||||
}, true
|
}, true
|
||||||
|
|
||||||
|
case PointClassUnidentifiedPlanet:
|
||||||
|
return StyleOverride{
|
||||||
|
FillColor: cRGBA(192, 192, 192, 255),
|
||||||
|
PointRadiusPx: new(2.5),
|
||||||
|
}, true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return StyleOverride{}, false
|
return StyleOverride{}, false
|
||||||
}
|
}
|
||||||
@@ -615,30 +620,23 @@ func (*DarkTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
|
|||||||
case CircleClassDefault:
|
case CircleClassDefault:
|
||||||
return StyleOverride{}, false
|
return StyleOverride{}, false
|
||||||
|
|
||||||
case CircleClassHome:
|
case CircleClassLocalPlanet:
|
||||||
return StyleOverride{
|
return StyleOverride{
|
||||||
FillColor: nil, // cRGBA(120, 214, 198, 255),
|
FillColor: cRGBA(155, 175, 235, 255),
|
||||||
StrokeColor: cRGBA(120, 214, 198, 255),
|
|
||||||
StrokeWidthPx: new(2.5),
|
|
||||||
}, true
|
|
||||||
|
|
||||||
case CircleClassAcquired:
|
|
||||||
return StyleOverride{
|
|
||||||
FillColor: nil, // cRGBA(155, 175, 235, 255),
|
|
||||||
StrokeColor: cRGBA(155, 175, 235, 255),
|
StrokeColor: cRGBA(155, 175, 235, 255),
|
||||||
StrokeWidthPx: new(2.2),
|
StrokeWidthPx: new(2.2),
|
||||||
}, true
|
}, true
|
||||||
|
|
||||||
case CircleClassOccupied:
|
case CircleClassOthersPlanet:
|
||||||
return StyleOverride{
|
return StyleOverride{
|
||||||
FillColor: nil, // cRGBA(245, 178, 120, 255),
|
FillColor: cRGBA(245, 178, 120, 255),
|
||||||
StrokeColor: cRGBA(245, 178, 120, 255),
|
StrokeColor: cRGBA(245, 178, 120, 255),
|
||||||
StrokeWidthPx: new(2.2),
|
StrokeWidthPx: new(2.2),
|
||||||
}, true
|
}, true
|
||||||
|
|
||||||
case CircleClassFree:
|
case CircleClassFreePlanet:
|
||||||
return StyleOverride{
|
return StyleOverride{
|
||||||
FillColor: nil, // cRGBA(132, 219, 162, 255),
|
FillColor: cRGBA(132, 219, 162, 255),
|
||||||
StrokeColor: cRGBA(132, 219, 162, 255),
|
StrokeColor: cRGBA(132, 219, 162, 255),
|
||||||
StrokeWidthPx: new(2.2),
|
StrokeWidthPx: new(2.2),
|
||||||
}, true
|
}, true
|
||||||
|
|||||||
@@ -688,6 +688,8 @@ const (
|
|||||||
PointClassTrackIncoming
|
PointClassTrackIncoming
|
||||||
// PointClassTrackOutgoing marks a point as an outgoing track marker.
|
// PointClassTrackOutgoing marks a point as an outgoing track marker.
|
||||||
PointClassTrackOutgoing
|
PointClassTrackOutgoing
|
||||||
|
// PointClassUnidentifiedPlanet marks an unidentified planet without visivle size.
|
||||||
|
PointClassUnidentifiedPlanet
|
||||||
)
|
)
|
||||||
|
|
||||||
// LineClassID classifies Line primitives for theme-level style overrides.
|
// LineClassID classifies Line primitives for theme-level style overrides.
|
||||||
@@ -711,14 +713,12 @@ type CircleClassID uint8
|
|||||||
const (
|
const (
|
||||||
// CircleClassDefault selects the theme's default circle styling.
|
// CircleClassDefault selects the theme's default circle styling.
|
||||||
CircleClassDefault CircleClassID = iota
|
CircleClassDefault CircleClassID = iota
|
||||||
// CircleClassHome marks a circle as a home-world area.
|
// CircleClassLocalPlanet marks a circle as a player-owned planet.
|
||||||
CircleClassHome
|
CircleClassLocalPlanet
|
||||||
// CircleClassAcquired marks a circle as an acquired world area.
|
// CircleClassOthersPlanet marks a circle as an occupied planet.
|
||||||
CircleClassAcquired
|
CircleClassOthersPlanet
|
||||||
// CircleClassOccupied marks a circle as an occupied world area.
|
// CircleClassFreePlanet marks a circle as a free planet.
|
||||||
CircleClassOccupied
|
CircleClassFreePlanet
|
||||||
// CircleClassFree marks a circle as a free world area.
|
|
||||||
CircleClassFree
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrimitiveID is a compact stable identifier for primitives stored in the World.
|
// PrimitiveID is a compact stable identifier for primitives stored in the World.
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"iter"
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"galaxy/calc"
|
||||||
"galaxy/game/internal/model/game"
|
"galaxy/game/internal/model/game"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -65,7 +65,7 @@ func FilterBattleOpponents(c *Cache, attIdx, defIdx int, cacheProbability map[in
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
p := DestructionProbability(
|
p := calc.DestructionProbability(
|
||||||
c.ShipGroupShipClass(attIdx).Weapons.F(),
|
c.ShipGroupShipClass(attIdx).Weapons.F(),
|
||||||
c.ShipGroup(attIdx).TechLevel(game.TechWeapons).F(),
|
c.ShipGroup(attIdx).TechLevel(game.TechWeapons).F(),
|
||||||
c.ShipGroupShipClass(defIdx).Shields.F(),
|
c.ShipGroupShipClass(defIdx).Shields.F(),
|
||||||
@@ -216,16 +216,6 @@ func SingleBattle(c *Cache, b *Battle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 {
|
|
||||||
effAttack := attWeapons * attWeaponsTech
|
|
||||||
effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass)
|
|
||||||
return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 {
|
|
||||||
return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.)
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomValue(v iter.Seq[int]) int {
|
func randomValue(v iter.Seq[int]) int {
|
||||||
ids := slices.Collect(v)
|
ids := slices.Collect(v)
|
||||||
return ids[rand.IntN(len(ids))]
|
return ids[rand.IntN(len(ids))]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"galaxy/calc"
|
||||||
"galaxy/game/internal/controller"
|
"galaxy/game/internal/controller"
|
||||||
"galaxy/game/internal/model/game"
|
"galaxy/game/internal/model/game"
|
||||||
|
|
||||||
@@ -39,25 +40,25 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDestructionProbability(t *testing.T) {
|
func TestDestructionProbability(t *testing.T) {
|
||||||
probability := controller.DestructionProbability(ship.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
|
probability := calc.DestructionProbability(ship.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
|
||||||
assert.Equal(t, .5, probability)
|
assert.Equal(t, .5, probability)
|
||||||
|
|
||||||
undefeatedShip := ship
|
undefeatedShip := ship
|
||||||
undefeatedShip.Shields = 55
|
undefeatedShip.Shields = 55
|
||||||
probability = controller.DestructionProbability(ship.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass())
|
probability = calc.DestructionProbability(ship.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass())
|
||||||
assert.LessOrEqual(t, probability, 0.)
|
assert.LessOrEqual(t, probability, 0.)
|
||||||
|
|
||||||
disruptiveShip := ship
|
disruptiveShip := ship
|
||||||
disruptiveShip.Weapons = 40
|
disruptiveShip.Weapons = 40
|
||||||
probability = controller.DestructionProbability(disruptiveShip.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
|
probability = calc.DestructionProbability(disruptiveShip.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
|
||||||
assert.GreaterOrEqual(t, probability, 1.)
|
assert.GreaterOrEqual(t, probability, 1.)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEffectiveDefence(t *testing.T) {
|
func TestEffectiveDefence(t *testing.T) {
|
||||||
assert.Equal(t, 10., controller.EffectiveDefence(ship.Shields.F(), 1, ship.EmptyMass()))
|
assert.Equal(t, 10., calc.EffectiveDefence(ship.Shields.F(), 1, ship.EmptyMass()))
|
||||||
|
|
||||||
attackerEffectiveDefence := controller.EffectiveDefence(attacker.Shields.F(), 1, attacker.EmptyMass())
|
attackerEffectiveDefence := calc.EffectiveDefence(attacker.Shields.F(), 1, attacker.EmptyMass())
|
||||||
defenderEffectiveDefence := controller.EffectiveDefence(defender.Shields.F(), 1, defender.EmptyMass())
|
defenderEffectiveDefence := calc.EffectiveDefence(defender.Shields.F(), 1, defender.EmptyMass())
|
||||||
|
|
||||||
// attacker's effective shields must be 'just' 4 times greater than defender's
|
// attacker's effective shields must be 'just' 4 times greater than defender's
|
||||||
assert.InDelta(t, defenderEffectiveDefence*4, attackerEffectiveDefence, 0)
|
assert.InDelta(t, defenderEffectiveDefence*4, attackerEffectiveDefence, 0)
|
||||||
@@ -123,7 +124,7 @@ func TestFilterBattleOpponents(t *testing.T) {
|
|||||||
assert.True(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability))
|
assert.True(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability))
|
||||||
assert.NoError(t, c.UpdateRelation(Race_0_idx, Race_1_idx, game.RelationWar))
|
assert.NoError(t, c.UpdateRelation(Race_0_idx, Race_1_idx, game.RelationWar))
|
||||||
|
|
||||||
assert.LessOrEqual(t, controller.DestructionProbability(Cruiser.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass()), 0.)
|
assert.LessOrEqual(t, calc.DestructionProbability(Cruiser.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass()), 0.)
|
||||||
assert.True(t, controller.FilterBattleOpponents(c, 1, 3, cacheProbability))
|
assert.True(t, controller.FilterBattleOpponents(c, 1, 3, cacheProbability))
|
||||||
assert.NotContains(t, cacheProbability[1], 3)
|
assert.NotContains(t, cacheProbability[1], 3)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"galaxy/calc"
|
||||||
"galaxy/util"
|
"galaxy/util"
|
||||||
|
|
||||||
e "galaxy/error"
|
e "galaxy/error"
|
||||||
@@ -272,7 +273,7 @@ func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
|
|||||||
}
|
}
|
||||||
ships := uint(0)
|
ships := uint(0)
|
||||||
pa := productionAvailable
|
pa := productionAvailable
|
||||||
PRODcost := ShipProductionCost(shipMass)
|
PRODcost := calc.ShipProductionCost(shipMass)
|
||||||
var MATneed, MATfarm, totalCost float64
|
var MATneed, MATfarm, totalCost float64
|
||||||
for {
|
for {
|
||||||
MATneed = shipMass - float64(p.Material)
|
MATneed = shipMass - float64(p.Material)
|
||||||
@@ -281,8 +282,6 @@ func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
|
|||||||
}
|
}
|
||||||
MATfarm = MATneed / float64(p.Resources)
|
MATfarm = MATneed / float64(p.Resources)
|
||||||
totalCost = PRODcost + MATfarm
|
totalCost = PRODcost + MATfarm
|
||||||
// fmt.Printf("PRODcost: %3.03f MATcost: %3.03f MAThave: %3.03f MATneed: %3.03f MATfarm: %3.03f total: %3.03f \n",
|
|
||||||
// PRODcost, shipMass, float64(p.Material), MATneed, MATfarm, totalCost)
|
|
||||||
if pa < totalCost {
|
if pa < totalCost {
|
||||||
progress := pa / totalCost
|
progress := pa / totalCost
|
||||||
pval := game.F(progress)
|
pval := game.F(progress)
|
||||||
@@ -292,7 +291,6 @@ func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
|
|||||||
p.Production.Progress = &pval
|
p.Production.Progress = &pval
|
||||||
fval := game.F(pa)
|
fval := game.F(pa)
|
||||||
p.Production.ProdUsed = &fval
|
p.Production.ProdUsed = &fval
|
||||||
// fmt.Println("pa", pa, "progress", progress, "MAT:", progress*shipMass)
|
|
||||||
return ships
|
return ships
|
||||||
} else {
|
} else {
|
||||||
pa -= totalCost
|
pa -= totalCost
|
||||||
@@ -301,10 +299,3 @@ func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShipProductionCost(shipMass float64) float64 {
|
|
||||||
return shipMass * 10.
|
|
||||||
}
|
|
||||||
func ShipMaterialCost(shipMass, planetResource float64) float64 {
|
|
||||||
return shipMass / planetResource
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"galaxy/calc"
|
||||||
mr "galaxy/model/report"
|
mr "galaxy/model/report"
|
||||||
|
|
||||||
"galaxy/util"
|
"galaxy/util"
|
||||||
@@ -540,7 +541,7 @@ func (c *Cache) ReportShipProduction(ri int, rep *mr.Report) {
|
|||||||
sliceIndexValidate(&rep.ShipProduction, i)
|
sliceIndexValidate(&rep.ShipProduction, i)
|
||||||
rep.ShipProduction[pi].Planet = p.Number
|
rep.ShipProduction[pi].Planet = p.Number
|
||||||
rep.ShipProduction[pi].Class = st.Name
|
rep.ShipProduction[pi].Class = st.Name
|
||||||
rep.ShipProduction[pi].Cost = mr.F(ShipProductionCost(st.EmptyMass()))
|
rep.ShipProduction[pi].Cost = mr.F(calc.ShipProductionCost(st.EmptyMass()))
|
||||||
rep.ShipProduction[pi].Free = mr.F(c.PlanetProductionCapacity(p.Number))
|
rep.ShipProduction[pi].Free = mr.F(c.PlanetProductionCapacity(p.Number))
|
||||||
rep.ShipProduction[pi].ProdUsed = mr.F((*p.Production.ProdUsed).F())
|
rep.ShipProduction[pi].ProdUsed = mr.F((*p.Production.ProdUsed).F())
|
||||||
rep.ShipProduction[pi].Percent = mr.F((*p.Production.Progress).F())
|
rep.ShipProduction[pi].Percent = mr.F((*p.Production.Progress).F())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"galaxy/calc"
|
||||||
"galaxy/util"
|
"galaxy/util"
|
||||||
|
|
||||||
e "galaxy/error"
|
e "galaxy/error"
|
||||||
@@ -16,7 +17,7 @@ import (
|
|||||||
|
|
||||||
func (c *Cache) ShipClassCreate(ri int, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error {
|
func (c *Cache) ShipClassCreate(ri int, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error {
|
||||||
c.validateRaceIndex(ri)
|
c.validateRaceIndex(ri)
|
||||||
if err := validateShipTypeValues(drive, ammo, weapons, shileds, cargo); err != nil {
|
if err := calc.ValidateShipTypeValues(drive, ammo, weapons, shileds, cargo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n, ok := util.ValidateTypeName(typeName)
|
n, ok := util.ValidateTypeName(typeName)
|
||||||
@@ -159,32 +160,3 @@ func (c *Cache) MustShipType(ri int, ID uuid.UUID) *game.ShipType {
|
|||||||
}
|
}
|
||||||
panic(fmt.Sprintf("ship class not found: race_idx=%d id=%v", ri, ID))
|
panic(fmt.Sprintf("ship class not found: race_idx=%d id=%v", ri, ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateShipTypeValues(d float64, a int, w, s, c float64) error {
|
|
||||||
if !checkShipTypeValueDWSC(d) {
|
|
||||||
return e.NewDriveValueError(d)
|
|
||||||
}
|
|
||||||
if !checkShipTypeValueDWSC(w) {
|
|
||||||
return e.NewWeaponsValueError(w)
|
|
||||||
}
|
|
||||||
if !checkShipTypeValueDWSC(s) {
|
|
||||||
return e.NewShieldsValueError(s)
|
|
||||||
}
|
|
||||||
if !checkShipTypeValueDWSC(c) {
|
|
||||||
return e.NewCargoValueError(s)
|
|
||||||
}
|
|
||||||
if a < 0 {
|
|
||||||
return e.NewShipTypeArmamentValueError(a)
|
|
||||||
}
|
|
||||||
if (w == 0 && a > 0) || (a == 0 && w > 0) {
|
|
||||||
return e.NewShipTypeArmamentAndWeaponsValueError("A=%d W=%.0f", a, w)
|
|
||||||
}
|
|
||||||
if d == 0 && w == 0 && s == 0 && c == 0 && a == 0 {
|
|
||||||
return e.NewShipTypeShipTypeZeroValuesError()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkShipTypeValueDWSC(v float64) bool {
|
|
||||||
return v == 0 || v >= 1
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"galaxy/calc"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -179,18 +180,13 @@ func (sg ShipGroup) Equal(other ShipGroup) bool {
|
|||||||
sg.State() == other.State()
|
sg.State() == other.State()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Грузоподъёмность
|
// Грузоподъёмность группы
|
||||||
func (sg ShipGroup) CargoCapacity(st *ShipType) float64 {
|
func (sg ShipGroup) CargoCapacity(st *ShipType) float64 {
|
||||||
return sg.TechLevel(TechCargo).F() * (st.Cargo.F() + (st.Cargo.F()*st.Cargo.F())/20) * float64(sg.Number)
|
return calc.CargoCapacity(st.Cargo.F(), sg.TechLevel(TechCargo).F()) * float64(sg.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Масса перевозимого груза -
|
|
||||||
// общее количество единиц груза, деленное на технологический уровень Грузоперевозок
|
|
||||||
func (sg ShipGroup) CarryingMass() float64 {
|
func (sg ShipGroup) CarryingMass() float64 {
|
||||||
if sg.Load.F() == 0 {
|
return calc.CarryingMass(sg.Load.F(), sg.TechLevel(TechCargo).F())
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return sg.Load.F() / sg.TechLevel(TechCargo).F()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Масса группы без учёта груза
|
// Масса группы без учёта груза
|
||||||
@@ -198,22 +194,16 @@ func (sg ShipGroup) EmptyMass(st *ShipType) float64 {
|
|||||||
return st.EmptyMass() * float64(sg.Number)
|
return st.EmptyMass() * float64(sg.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Полная масса -
|
|
||||||
// массу корабля самого по себе плюс масса перевозимого груза
|
|
||||||
func (sg ShipGroup) FullMass(st *ShipType) float64 {
|
func (sg ShipGroup) FullMass(st *ShipType) float64 {
|
||||||
return sg.EmptyMass(st) + sg.CarryingMass()
|
return calc.FullMass(sg.EmptyMass(st), sg.CarryingMass())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Эффективность двигателя -
|
|
||||||
// равна мощности Двигателей, умноженной на технологический уровень блока Двигателей
|
|
||||||
func (sg ShipGroup) DriveEffective(st *ShipType) float64 {
|
func (sg ShipGroup) DriveEffective(st *ShipType) float64 {
|
||||||
return st.Drive.F() * sg.TechLevel(TechDrive).F()
|
return calc.DriveEffective(st.Drive.F(), sg.TechLevel(TechDrive).F())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Корабли перемещаются за один ход на количество световых лет, равное
|
|
||||||
// эффективности двигателя, умноженной на 20 и деленной на "Полную массу" корабля
|
|
||||||
func (sg ShipGroup) Speed(st *ShipType) float64 {
|
func (sg ShipGroup) Speed(st *ShipType) float64 {
|
||||||
return sg.DriveEffective(st) * 20 / sg.FullMass(st)
|
return calc.Speed(sg.DriveEffective(st), sg.FullMass(st))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Мощность бомбардировки
|
// Мощность бомбардировки
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"galaxy/calc"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -44,10 +45,11 @@ func (st ShipType) DriveBlockMass() float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (st ShipType) WeaponsBlockMass() float64 {
|
func (st ShipType) WeaponsBlockMass() float64 {
|
||||||
if (st.Armament == 0 && st.Weapons != 0) || (st.Armament != 0 && st.Weapons == 0) {
|
if v, ok := calc.WeaponsBlockMass(st.Weapons.F(), st.Armament); !ok {
|
||||||
panic(fmt.Sprintf("ship class invalid design: A=%d W=%.03f", st.Armament, st.Weapons))
|
panic(fmt.Sprintf("ship class invalid design: A=%d W=%.03f", st.Armament, st.Weapons))
|
||||||
|
} else {
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
return float64(st.Armament+1) * (st.Weapons.F() / 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st ShipType) ShieldsBlockMass() float64 {
|
func (st ShipType) ShieldsBlockMass() float64 {
|
||||||
@@ -59,6 +61,10 @@ func (st ShipType) CargoBlockMass() float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (st ShipType) EmptyMass() float64 {
|
func (st ShipType) EmptyMass() float64 {
|
||||||
shipMass := st.DriveBlockMass() + st.ShieldsBlockMass() + st.CargoBlockMass() + st.WeaponsBlockMass()
|
if v, ok := calc.EmptyMass(st.Drive.F(), st.Weapons.F(), st.Armament, st.Shields.F(), st.Cargo.F()); !ok {
|
||||||
return shipMass
|
panic(fmt.Sprintf("ship class invalid design: D=%.03f A=%d W=%.03f S=%.03f C=%.03f",
|
||||||
|
st.Drive, st.Armament, st.Weapons, st.Shields, st.Cargo))
|
||||||
|
} else {
|
||||||
|
return v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"races": [
|
||||||
|
{"name": "Race_01"},
|
||||||
|
{"name": "Race_02"},
|
||||||
|
{"name": "Race_03"},
|
||||||
|
{"name": "Race_04"},
|
||||||
|
{"name": "Race_05"},
|
||||||
|
{"name": "Race_06"},
|
||||||
|
{"name": "Race_07"},
|
||||||
|
{"name": "Race_08"},
|
||||||
|
{"name": "Race_09"},
|
||||||
|
{"name": "Race_10"},
|
||||||
|
{"name": "Race_11"},
|
||||||
|
{"name": "Race_12"},
|
||||||
|
{"name": "Race_13"},
|
||||||
|
{"name": "Race_14"},
|
||||||
|
{"name": "Race_15"},
|
||||||
|
{"name": "Race_16"},
|
||||||
|
{"name": "Race_17"},
|
||||||
|
{"name": "Race_18"},
|
||||||
|
{"name": "Race_19"},
|
||||||
|
{"name": "Race_20"}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
curl -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data @init.json \
|
||||||
|
http://127.0.0.1:8080/api/v1/init
|
||||||
@@ -3,6 +3,7 @@ go 1.26.0
|
|||||||
use (
|
use (
|
||||||
./client
|
./client
|
||||||
./game
|
./game
|
||||||
|
./pkg/calc
|
||||||
./pkg/connector
|
./pkg/connector
|
||||||
./pkg/error
|
./pkg/error
|
||||||
./pkg/model
|
./pkg/model
|
||||||
@@ -12,6 +13,7 @@ use (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
galaxy/calc v0.0.0 => ./pkg/calc
|
||||||
galaxy/connector v0.0.0 => ./pkg/connector
|
galaxy/connector v0.0.0 => ./pkg/connector
|
||||||
galaxy/error v0.0.0 => ./pkg/error
|
galaxy/error v0.0.0 => ./pkg/error
|
||||||
galaxy/model v0.0.0 => ./pkg/model
|
galaxy/model v0.0.0 => ./pkg/model
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module galaxy/calc
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package calc
|
||||||
|
|
||||||
|
func ShipProductionCost(shipEmptyMass float64) float64 {
|
||||||
|
return shipEmptyMass * 10.
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlanetProduceShipMass(L, Mat, Res float64) float64 {
|
||||||
|
result := L / 10
|
||||||
|
if result <= Mat {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return (L + Mat/Res) / (10 + 1/Res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package calc
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// Эффективность двигателя -
|
||||||
|
// равна мощности Двигателей, умноженной на технологический уровень блока Двигателей
|
||||||
|
func DriveEffective(drive, driveTech float64) float64 {
|
||||||
|
return drive * driveTech
|
||||||
|
}
|
||||||
|
|
||||||
|
// Масса перевозимого груза -
|
||||||
|
// общее количество единиц груза, деленное на технологический уровень Грузоперевозок
|
||||||
|
func CarryingMass(load, cargoTech float64) float64 {
|
||||||
|
if load <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return load / cargoTech
|
||||||
|
}
|
||||||
|
|
||||||
|
// Грузоподъёмность одного корабля
|
||||||
|
func CargoCapacity(cargo, cargoTech float64) float64 {
|
||||||
|
return cargoTech * (cargo + (cargo*cargo)/20)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Корабли перемещаются за один ход на количество световых лет, равное
|
||||||
|
// эффективности двигателя, умноженной на 20 и деленной на "Полную массу" корабля
|
||||||
|
func Speed(driveEffective, fullMass float64) float64 {
|
||||||
|
if fullMass <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return driveEffective * 20 / fullMass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Полная масса -
|
||||||
|
// массу корабля самого по себе плюс масса перевозимого груза
|
||||||
|
func FullMass(emptyMass, carryingMass float64) float64 {
|
||||||
|
return emptyMass + carryingMass
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmptyMass(drive, weapons float64, armament uint, shields, cargo float64) (float64, bool) {
|
||||||
|
wm, ok := WeaponsBlockMass(weapons, armament)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return drive + shields + cargo + wm, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func WeaponsBlockMass(weapons float64, armament uint) (float64, bool) {
|
||||||
|
if (armament == 0 && weapons != 0) || (armament != 0 && weapons == 0) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return float64(armament+1) * (weapons / 2), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func DestructionProbability(
|
||||||
|
attackingWeapons,
|
||||||
|
attackingWeaponsTech,
|
||||||
|
defendingShields,
|
||||||
|
defendingShiledsTech,
|
||||||
|
defendingFullMass float64,
|
||||||
|
) float64 {
|
||||||
|
return DestructionProbabilityEffective(
|
||||||
|
EffectiveAttack(attackingWeapons, attackingWeaponsTech),
|
||||||
|
EffectiveDefence(defendingShields, defendingShiledsTech, defendingFullMass),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DestructionProbabilityEffective(effectiveAttack, effectiveDefence float64) float64 {
|
||||||
|
return (math.Log10(effectiveAttack/effectiveDefence)/math.Log10(4) + 1) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func EffectiveAttack(weapons, weaponsTech float64) float64 {
|
||||||
|
return weapons * weaponsTech
|
||||||
|
}
|
||||||
|
|
||||||
|
func EffectiveDefence(
|
||||||
|
defendingShields,
|
||||||
|
defendingShiledsTech,
|
||||||
|
defendingFullMass float64,
|
||||||
|
) float64 {
|
||||||
|
if defendingFullMass <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return defendingShields * defendingShiledsTech / math.Pow(defendingFullMass, 1./3.) * math.Pow(30., 1./3.)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package calc
|
||||||
|
|
||||||
|
import (
|
||||||
|
e "galaxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateShipTypeValues(d float64, a int, w, s, c float64) error {
|
||||||
|
if !CheckShipTypeValueDWSC(d) {
|
||||||
|
return e.NewDriveValueError(d)
|
||||||
|
}
|
||||||
|
if !CheckShipTypeValueDWSC(w) {
|
||||||
|
return e.NewWeaponsValueError(w)
|
||||||
|
}
|
||||||
|
if !CheckShipTypeValueDWSC(s) {
|
||||||
|
return e.NewShieldsValueError(s)
|
||||||
|
}
|
||||||
|
if !CheckShipTypeValueDWSC(c) {
|
||||||
|
return e.NewCargoValueError(s)
|
||||||
|
}
|
||||||
|
if a < 0 {
|
||||||
|
return e.NewShipTypeArmamentValueError(a)
|
||||||
|
}
|
||||||
|
if (w == 0 && a > 0) || (a == 0 && w > 0) {
|
||||||
|
return e.NewShipTypeArmamentAndWeaponsValueError("A=%d W=%.0f", a, w)
|
||||||
|
}
|
||||||
|
if d == 0 && w == 0 && s == 0 && c == 0 && a == 0 {
|
||||||
|
return e.NewShipTypeShipTypeZeroValuesError()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckShipTypeValueDWSC(v float64) bool {
|
||||||
|
return v == 0 || v >= 1
|
||||||
|
}
|
||||||
@@ -41,6 +41,13 @@ type State struct {
|
|||||||
ClientNextVersion *string `json:"clientNextVersion,omitempty"`
|
ClientNextVersion *string `json:"clientNextVersion,omitempty"`
|
||||||
GameState []GameState `json:"gameState,omitempty"`
|
GameState []GameState `json:"gameState,omitempty"`
|
||||||
ActiveGameID *GameID `json:"activeGameId,omitempty"`
|
ActiveGameID *GameID `json:"activeGameId,omitempty"`
|
||||||
|
|
||||||
|
CameraZoom float64 `json:"cameraZoom"`
|
||||||
|
CameraXFp int `json:"cameraXFp"`
|
||||||
|
CameraYFp int `json:"cameraYFp"`
|
||||||
|
MapSplitterOffset float64 `json:"mapSplitterOffset"`
|
||||||
|
AccordionInfoOpen bool `json:"accInfoOpen"`
|
||||||
|
AccordionCalcOpen bool `json:"accCalcOpen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameState struct {
|
type GameState struct {
|
||||||
|
|||||||
@@ -128,6 +128,15 @@ func (s *fsStorage) SaveStateAsync(state client.State, callback func(error)) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *fsStorage) ReportExistsAsync(id client.GameID, turn uint, callback func(bool, error)) {
|
||||||
|
go func() {
|
||||||
|
exists, err := s.gameDataExistsSync(id, turn)
|
||||||
|
if callback != nil {
|
||||||
|
callback(exists, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *fsStorage) LoadReportAsync(id client.GameID, turn uint, callback func(report.Report, error)) {
|
func (s *fsStorage) LoadReportAsync(id client.GameID, turn uint, callback func(report.Report, error)) {
|
||||||
go func() {
|
go func() {
|
||||||
rep, err := s.loadReportSync(id, turn)
|
rep, err := s.loadReportSync(id, turn)
|
||||||
@@ -343,6 +352,20 @@ func (s *fsStorage) saveOrderSync(id client.GameID, turn uint, o order.Order) er
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *fsStorage) gameDataExistsSync(id client.GameID, turn uint) (bool, error) {
|
||||||
|
absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
|
||||||
|
if err != nil {
|
||||||
|
return false, classifyStorageError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := s.fileExistsUnlocked(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, classifyStorageError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *fsStorage) loadGameDataSync(id client.GameID, turn uint) (client.GameData, error) {
|
func (s *fsStorage) loadGameDataSync(id client.GameID, turn uint) (client.GameData, error) {
|
||||||
absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
|
absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ type UIStorage interface {
|
|||||||
// I/O or encoding error may occur, it that case callback func will be called with non-nil error.
|
// I/O or encoding error may occur, it that case callback func will be called with non-nil error.
|
||||||
SaveStateAsync(client.State, func(error))
|
SaveStateAsync(client.State, func(error))
|
||||||
|
|
||||||
|
// ReportExistsAsync asynchronously checks whether given [model.GameID] and turn number exists in the Storage.
|
||||||
|
// Passed callback func will will accept non-nil error in case of I/O or decoding errors occuried,
|
||||||
|
// otherwise callback func accepts boolean result.
|
||||||
|
ReportExistsAsync(client.GameID, uint, func(bool, error))
|
||||||
|
|
||||||
// LoadReportAsync loads a [report.Report] for a given [model.GameID] and turn number from filesystem asynchronously.
|
// LoadReportAsync loads a [report.Report] for a given [model.GameID] and turn number from filesystem asynchronously.
|
||||||
// Passed callback func will will accept non-nil error in case of I/O or decoding errors occuried,
|
// Passed callback func will will accept non-nil error in case of I/O or decoding errors occuried,
|
||||||
// otherwise callback func accepts loaded [report.Report].
|
// otherwise callback func accepts loaded [report.Report].
|
||||||
|
|||||||
Reference in New Issue
Block a user