File size: 6,548 Bytes
3304d77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""Dessin du plateau avec pygame (vue de dessus, ou inclinée quand un tir part en 3D)."""
from __future__ import annotations

import math

import numpy as np
import pygame

from . import config as C

PAPER = (244, 246, 241)
GRID_MINOR = (226, 232, 233)
GRID_MAJOR = (196, 208, 214)
AXIS = (128, 140, 150)
INK = (34, 38, 50)
INK_SIDE = (70, 76, 92)
DEAD = (150, 150, 150)
SHADOW = (175, 180, 188)
MAX_TILT = math.radians(52)


def font(size, bold=False):
    return pygame.font.SysFont("cambriamath,cambria,segoeuisymbol,dejavusans,arial", size, bold=bold)


class Camera:
    def __init__(self, rect: pygame.Rect):
        self.rect = pygame.Rect(rect)
        self.s = min(self.rect.w / (2 * C.BOARD_W), self.rect.h / (2 * C.BOARD_H))
        self.tilt = 0.0                      # 0 = vue de dessus, 1 = inclinée au maximum

    def p(self, x, y, z=0.0):
        th = self.tilt * MAX_TILT
        cx, cy = self.rect.centerx, self.rect.centery + self.tilt * 0.12 * self.rect.h
        return (cx + x * self.s, cy - (y * math.cos(th) + z * math.sin(th)) * self.s)

    def pts(self, xs, ys, zs):
        th = self.tilt * MAX_TILT
        cx, cy = self.rect.centerx, self.rect.centery + self.tilt * 0.12 * self.rect.h
        X = cx + xs * self.s
        Y = cy - (ys * math.cos(th) + zs * math.sin(th)) * self.s
        return np.column_stack([X, Y])


def _cyl(surf, cam, x, y, r, h, top, side, off=(0, 0)):
    """Un cylindre vu en biais : flanc puis dessus (off = décalage du calque)."""
    th = cam.tilt * MAX_TILT
    rw, rh = r * cam.s, max(1.0, r * cam.s * math.cos(th))
    bx, by = cam.p(x, y, 0)
    tx, ty = cam.p(x, y, h)
    bx, by, tx, ty = bx - off[0], by - off[1], tx - off[0], ty - off[1]
    pygame.draw.polygon(surf, side, [(bx - rw, by), (bx + rw, by), (tx + rw, ty), (tx - rw, ty)])
    pygame.draw.ellipse(surf, side, (bx - rw, by - rh, 2 * rw, 2 * rh))
    pygame.draw.ellipse(surf, top, (tx - rw, ty - rh, 2 * rw, 2 * rh))


def _obstacle(surf, cam, ob, craters):
    """Dessine un obstacle avec ses cratères (chaque trou ne creuse que son propre obstacle)."""
    x, y, r, h = ob
    tilted = cam.tilt > 0.01
    th = cam.tilt * MAX_TILT
    rw = r * cam.s
    rh = max(1.0, rw * math.cos(th))
    bx, by = cam.p(x, y, 0)
    ty = cam.p(x, y, h)[1] if tilted else by
    box = pygame.Rect(int(bx - rw) - 2, int(ty - rh) - 2, int(2 * rw) + 5, int(by - ty + 2 * rh) + 5)
    spr = pygame.Surface(box.size, pygame.SRCALPHA)
    off = box.topleft
    if tilted:
        _cyl(spr, cam, x, y, r, h, INK, INK_SIDE, off)
    else:
        c = cam.p(x, y)
        pygame.draw.circle(spr, INK, (c[0] - off[0], c[1] - off[1]), rw)
    clear = (0, 0, 0, 0)
    for cx, cy, cr in craters:
        if math.hypot(cx - x, cy - y) > r + cr:
            continue
        if tilted:
            _cyl(spr, cam, cx, cy, cr, h + 0.05, clear, clear, off)
        else:
            c = cam.p(cx, cy)
            pygame.draw.circle(spr, clear, (c[0] - off[0], c[1] - off[1]), cr * cam.s)
    surf.blit(spr, off)


def draw_board(surf, cam, world, colors=((60, 90, 200), (200, 70, 60)), highlight=None):
    """Papier millimétré, obstacles, cratères, soldats."""
    r = cam.rect
    pygame.draw.rect(surf, PAPER, r.inflate(4, 4))
    # quadrillage
    for x in range(-25, 26):
        col = AXIS if x == 0 else GRID_MAJOR if x % 5 == 0 else GRID_MINOR
        pygame.draw.line(surf, col, cam.p(x, -C.BOARD_H), cam.p(x, C.BOARD_H), 2 if x == 0 else 1)
    for y in range(-15, 16):
        col = AXIS if y == 0 else GRID_MAJOR if y % 5 == 0 else GRID_MINOR
        pygame.draw.line(surf, col, cam.p(-C.BOARD_W, y), cam.p(C.BOARD_W, y), 2 if y == 0 else 1)
    corners = [cam.p(-25, -15), cam.p(25, -15), cam.p(25, 15), cam.p(-25, 15)]
    pygame.draw.polygon(surf, AXIS, corners, 2)

    # obstacles (on ne dessine pas hors du plateau en vue de dessus)
    clip = surf.get_clip()
    if cam.tilt <= 0.01:
        surf.set_clip(r)
    order = np.argsort(-world.obstacles[:, 1]) if cam.tilt > 0.01 else range(len(world.obstacles))
    for i in order:
        _obstacle(surf, cam, world.obstacles[i], world.craters)
    surf.set_clip(clip)

    # soldats
    soldiers = sorted(enumerate(world.soldiers), key=lambda t: -t[1].y)
    for i, s in soldiers:
        col = colors[s.team] if s.alive else DEAD
        rad = C.SOLDIER_R * cam.s
        if cam.tilt > 0.01:
            b, t = cam.p(s.x, s.y, 0), cam.p(s.x, s.y, C.SOLDIER_H)
            pygame.draw.line(surf, col, b, t, max(3, int(rad * 0.8)))
            pygame.draw.circle(surf, col, t, rad * 0.7)
            pygame.draw.circle(surf, INK, t, rad * 0.7, 1)
        else:
            pygame.draw.circle(surf, col, cam.p(s.x, s.y), rad)
            pygame.draw.circle(surf, INK, cam.p(s.x, s.y), rad, 2 if i == highlight else 1)
        if not s.alive:
            c = cam.p(s.x, s.y, C.SOLDIER_H if cam.tilt > 0.01 else 0)
            d = rad * 0.7
            pygame.draw.line(surf, INK, (c[0] - d, c[1] - d), (c[0] + d, c[1] + d), 2)
            pygame.draw.line(surf, INK, (c[0] - d, c[1] + d), (c[0] + d, c[1] - d), 2)
        if i == highlight and s.alive:
            pygame.draw.circle(surf, INK, cam.p(s.x, s.y), rad + 5, 1)


def draw_shot(surf, cam, res, color, frac=1.0, width=3, faded=False):
    n = len(res.xs)
    if n < 2:
        return None
    k = max(2, int(n * frac))
    step = max(1, k // 900)
    xs, ys, zs = res.xs[:k:step], res.ys[:k:step], res.zs[:k:step]
    xs, ys, zs = np.append(xs, res.xs[k - 1]), np.append(ys, res.ys[k - 1]), np.append(zs, res.zs[k - 1])
    col = tuple(int(c + (255 - c) * 0.6) for c in color) if faded else color
    if cam.tilt > 0.01 and (zs > C.SHOT_Z + 0.05).any():                   # ombre au sol
        sh = cam.pts(xs, ys, np.zeros_like(zs))
        pygame.draw.lines(surf, SHADOW, False, sh.tolist(), 1)
    pts = cam.pts(xs, ys, zs if cam.tilt > 0.01 else np.zeros_like(zs))
    pygame.draw.lines(surf, col, False, pts.tolist(), width)
    head = pts[-1]
    if not faded:
        pygame.draw.circle(surf, col, head, width + 3)
        pygame.draw.circle(surf, INK, head, width + 3, 1)
    return head


def draw_boom(surf, center, t, color):
    """Petite explosion qui grossit puis s'efface (t de 0 à 1)."""
    r = 6 + 34 * t
    a = int(255 * (1 - t))
    s = pygame.Surface((int(2 * r + 4), int(2 * r + 4)), pygame.SRCALPHA)
    pygame.draw.circle(s, (*color, a), (r + 2, r + 2), r, 4)
    pygame.draw.circle(s, (*INK, a // 2), (r + 2, r + 2), r * 0.55, 2)
    surf.blit(s, (center[0] - r - 2, center[1] - r - 2))